Skip to content

Commit 8f20d9f

Browse files
committed
feat: add DeepSeek AI provider support
- Add DeepSeekProvider class with OpenAI-compatible API format - Support deepseek-chat (default) and deepseek-reasoner (thinking mode) models - Add PDODB_AI_DEEPSEEK_KEY environment variable support - Add PDODB_AI_DEEPSEEK_MODEL for model selection - Register DeepSeek provider in AiAnalysisService - Update documentation with DeepSeek configuration examples - Update README.md with DeepSeek in supported providers list DeepSeek API is compatible with OpenAI format, making integration straightforward. API documentation: https://api-docs.deepseek.com/
1 parent 582d9f3 commit 8f20d9f

File tree

5 files changed

+236
-3
lines changed

5 files changed

+236
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ pdodb ai analyze "SELECT * FROM users WHERE email = '[email protected]'" \
498498
--table=users
499499
```
500500

501-
**Supported Providers:** OpenAI, Anthropic, Google, Microsoft, Ollama (local, no API key)
501+
**Supported Providers:** OpenAI, Anthropic, Google, Microsoft, DeepSeek, Ollama (local, no API key)
502502

503503
**Model Selection:** Configure models via environment variables (`PDODB_AI_<PROVIDER>_MODEL`) or config array (`ai.providers.<provider>.model`)
504504

documentation/05-advanced-features/23-ai-analysis.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ PDOdb supports multiple AI providers, each with their own strengths:
3232
| **Anthropic** | claude-3-5-sonnet, claude-3-opus | Detailed analysis, long context | Yes |
3333
| **Google** | gemini-2.5-flash, gemini-2.5-pro, gemini-2.0-flash-001, gemini-flash-latest, gemini-pro-latest | Multimodal analysis, large context | Yes |
3434
| **Microsoft** | Azure OpenAI models | Enterprise environments | Yes |
35+
| **DeepSeek** | deepseek-chat, deepseek-reasoner | Cost-effective, OpenAI-compatible | Yes |
3536
| **Ollama** | Any local model (llama2, deepseek-coder, etc.) | Privacy, offline use, no API costs | No |
3637

3738
## Configuration
@@ -59,6 +60,10 @@ export PDODB_AI_MICROSOFT_KEY=...
5960
export PDODB_AI_MICROSOFT_MODEL=gpt-4 # Optional: model name
6061
# Also configure endpoint via config array (see below)
6162

63+
# DeepSeek
64+
export PDODB_AI_DEEPSEEK_KEY=...
65+
export PDODB_AI_DEEPSEEK_MODEL=deepseek-chat # Optional: deepseek-reasoner (thinking mode)
66+
6267
# Ollama (local, no API key needed)
6368
export PDODB_AI_OLLAMA_URL=http://localhost:11434
6469
export PDODB_AI_OLLAMA_MODEL=llama3.2 # Optional: model name
@@ -86,6 +91,7 @@ $db = new PdoDb('mysql', [
8691
'anthropic_key' => 'sk-ant-...',
8792
'google_key' => '...',
8893
'microsoft_key' => '...',
94+
'deepseek_key' => '...',
8995
'ollama_url' => 'http://localhost:11434',
9096
'providers' => [
9197
'openai' => [
@@ -106,6 +112,11 @@ $db = new PdoDb('mysql', [
106112
'endpoint' => 'https://your-resource.openai.azure.com',
107113
'deployment' => 'gpt-4',
108114
],
115+
'deepseek' => [
116+
'model' => 'deepseek-chat', # or deepseek-reasoner (thinking mode)
117+
'temperature' => 0.7,
118+
'max_tokens' => 2000,
119+
],
109120
'ollama' => [
110121
'model' => 'llama3.2', # or any local model name
111122
],

src/ai/AiAnalysisService.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace tommyknocker\pdodb\ai;
66

77
use tommyknocker\pdodb\ai\providers\AnthropicProvider;
8+
use tommyknocker\pdodb\ai\providers\DeepSeekProvider;
89
use tommyknocker\pdodb\ai\providers\GoogleProvider;
910
use tommyknocker\pdodb\ai\providers\MicrosoftProvider;
1011
use tommyknocker\pdodb\ai\providers\OllamaProvider;
@@ -82,8 +83,9 @@ protected function createProvider(string $providerName): AiProviderInterface
8283
'google' => new GoogleProvider($this->config),
8384
'microsoft' => new MicrosoftProvider($this->config),
8485
'ollama' => new OllamaProvider($this->config),
86+
'deepseek' => new DeepSeekProvider($this->config),
8587
default => throw new QueryException(
86-
"Unsupported AI provider: {$providerName}. Supported: openai, anthropic, google, microsoft, ollama",
88+
"Unsupported AI provider: {$providerName}. Supported: openai, anthropic, google, microsoft, ollama, deepseek",
8789
0
8890
),
8991
};

src/ai/AiConfig.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ protected function loadFromConfig(?array $config): void
5858
$this->apiKeys['microsoft'] = (string)$aiConfig['microsoft_key'];
5959
}
6060

61+
if (isset($aiConfig['deepseek_key'])) {
62+
$this->apiKeys['deepseek'] = (string)$aiConfig['deepseek_key'];
63+
}
64+
6165
if (isset($aiConfig['ollama_url'])) {
6266
$this->ollamaUrl = (string)$aiConfig['ollama_url'];
6367
}
@@ -120,7 +124,7 @@ protected function loadFromEnvironment(): void
120124
*/
121125
protected function loadProviderModelsFromEnvironment(): void
122126
{
123-
$providers = ['openai', 'anthropic', 'google', 'microsoft', 'ollama'];
127+
$providers = ['openai', 'anthropic', 'google', 'microsoft', 'ollama', 'deepseek'];
124128
foreach ($providers as $provider) {
125129
$envVar = 'PDODB_AI_' . strtoupper($provider) . '_MODEL';
126130
$model = getenv($envVar);
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace tommyknocker\pdodb\ai\providers;
6+
7+
use tommyknocker\pdodb\ai\BaseAiProvider;
8+
use tommyknocker\pdodb\exceptions\QueryException;
9+
10+
/**
11+
* DeepSeek provider implementation.
12+
* API is compatible with OpenAI format.
13+
*
14+
* @see https://api-docs.deepseek.com/
15+
*/
16+
class DeepSeekProvider extends BaseAiProvider
17+
{
18+
private const string API_URL = 'https://api.deepseek.com/chat/completions';
19+
private const string DEFAULT_MODEL = 'deepseek-chat';
20+
private const float DEFAULT_TEMPERATURE = 0.7;
21+
private const int DEFAULT_MAX_TOKENS = 2000;
22+
private const string HEADER_AUTHORIZATION = 'Authorization';
23+
private const string HEADER_BEARER_PREFIX = 'Bearer ';
24+
private const string MESSAGE_ROLE_SYSTEM = 'system';
25+
private const string MESSAGE_ROLE_USER = 'user';
26+
private const string RESPONSE_KEY_CHOICES = 'choices';
27+
private const string RESPONSE_KEY_MESSAGE = 'message';
28+
private const string RESPONSE_KEY_CONTENT = 'content';
29+
30+
protected string $apiUrl = self::API_URL;
31+
32+
protected function initializeDefaults(): void
33+
{
34+
$this->model = $this->config->getProviderSetting('deepseek', 'model', self::DEFAULT_MODEL);
35+
$this->temperature = (float)$this->config->getProviderSetting('deepseek', 'temperature', self::DEFAULT_TEMPERATURE);
36+
$this->maxTokens = (int)$this->config->getProviderSetting('deepseek', 'max_tokens', self::DEFAULT_MAX_TOKENS);
37+
}
38+
39+
public function getProviderName(): string
40+
{
41+
return 'deepseek';
42+
}
43+
44+
public function isAvailable(): bool
45+
{
46+
return $this->config->hasApiKey('deepseek');
47+
}
48+
49+
public function analyzeQuery(string $sql, array $context = []): string
50+
{
51+
$this->ensureAvailable();
52+
53+
$systemPrompt = $this->buildSystemPrompt('query');
54+
$userPrompt = $this->buildQueryPrompt($sql, $context);
55+
56+
return $this->callApi($systemPrompt, $userPrompt);
57+
}
58+
59+
public function analyzeSchema(array $schema, array $context = []): string
60+
{
61+
$this->ensureAvailable();
62+
63+
$systemPrompt = $this->buildSystemPrompt('schema');
64+
$userPrompt = $this->buildSchemaPrompt($schema, $context);
65+
66+
return $this->callApi($systemPrompt, $userPrompt);
67+
}
68+
69+
public function suggestOptimizations(array $analysis, array $context = []): string
70+
{
71+
$this->ensureAvailable();
72+
73+
$systemPrompt = $this->buildSystemPrompt('optimization');
74+
$userPrompt = $this->buildOptimizationPrompt($analysis, $context);
75+
76+
return $this->callApi($systemPrompt, $userPrompt);
77+
}
78+
79+
/**
80+
* Call DeepSeek API.
81+
*
82+
* @param string $systemPrompt System prompt
83+
* @param string $userPrompt User prompt
84+
*
85+
* @return string AI response
86+
*/
87+
protected function callApi(string $systemPrompt, string $userPrompt): string
88+
{
89+
$apiKey = $this->config->getApiKey('deepseek');
90+
if ($apiKey === null) {
91+
throw new QueryException('DeepSeek API key not configured', 0);
92+
}
93+
94+
$data = [
95+
'model' => $this->model,
96+
'messages' => [
97+
[
98+
'role' => self::MESSAGE_ROLE_SYSTEM,
99+
'content' => $systemPrompt,
100+
],
101+
[
102+
'role' => self::MESSAGE_ROLE_USER,
103+
'content' => $userPrompt,
104+
],
105+
],
106+
'temperature' => $this->temperature,
107+
'max_tokens' => $this->maxTokens,
108+
];
109+
110+
$headers = [
111+
self::HEADER_AUTHORIZATION => self::HEADER_BEARER_PREFIX . $apiKey,
112+
];
113+
114+
$response = $this->makeRequest($this->apiUrl, $data, $headers);
115+
116+
if (!isset($response[self::RESPONSE_KEY_CHOICES][0][self::RESPONSE_KEY_MESSAGE][self::RESPONSE_KEY_CONTENT])) {
117+
throw new QueryException(
118+
'Invalid response format from DeepSeek API',
119+
0
120+
);
121+
}
122+
123+
return (string)$response[self::RESPONSE_KEY_CHOICES][0][self::RESPONSE_KEY_MESSAGE][self::RESPONSE_KEY_CONTENT];
124+
}
125+
126+
/**
127+
* Build system prompt based on analysis type.
128+
*/
129+
protected function buildSystemPrompt(string $type): string
130+
{
131+
$basePrompt = 'You are an expert database performance analyst. Provide clear, actionable recommendations for database optimization.';
132+
133+
$typePrompts = [
134+
'query' => 'Analyze SQL queries and provide optimization suggestions. Focus on index usage, query structure, and performance bottlenecks.',
135+
'schema' => 'Analyze database schema and provide recommendations for indexes, constraints, and table structure improvements.',
136+
'optimization' => 'Review existing analysis results and provide additional optimization suggestions. Build upon the existing recommendations.',
137+
];
138+
139+
return $basePrompt . ' ' . ($typePrompts[$type] ?? '');
140+
}
141+
142+
/**
143+
* Build prompt for query analysis.
144+
*
145+
* @param array<string, mixed> $context
146+
*/
147+
protected function buildQueryPrompt(string $sql, array $context): string
148+
{
149+
$prompt = "Analyze the following SQL query and provide optimization recommendations:\n\n";
150+
$prompt .= "SQL Query:\n```sql\n{$sql}\n```\n\n";
151+
152+
if (!empty($context)) {
153+
$prompt .= $this->formatContext($context);
154+
}
155+
156+
$prompt .= "\n\nProvide specific, actionable recommendations including:\n";
157+
$prompt .= "- Index suggestions\n";
158+
$prompt .= "- Query structure improvements\n";
159+
$prompt .= "- Performance bottlenecks\n";
160+
$prompt .= '- Estimated impact of optimizations';
161+
162+
return $prompt;
163+
}
164+
165+
/**
166+
* Build prompt for schema analysis.
167+
*
168+
* @param array<string, mixed> $schema
169+
* @param array<string, mixed> $context
170+
*/
171+
protected function buildSchemaPrompt(array $schema, array $context): string
172+
{
173+
$prompt = "Analyze the following database schema and provide optimization recommendations:\n\n";
174+
$prompt .= $this->formatContext(array_merge(['schema' => $schema], $context));
175+
176+
$prompt .= "\n\nProvide specific recommendations for:\n";
177+
$prompt .= "- Missing indexes\n";
178+
$prompt .= "- Redundant indexes\n";
179+
$prompt .= "- Table structure improvements\n";
180+
$prompt .= '- Foreign key optimizations';
181+
182+
return $prompt;
183+
}
184+
185+
/**
186+
* Build prompt for optimization suggestions.
187+
*
188+
* @param array<string, mixed> $analysis
189+
* @param array<string, mixed> $context
190+
*/
191+
protected function buildOptimizationPrompt(array $analysis, array $context): string
192+
{
193+
$prompt = "Review the following database analysis and provide additional optimization suggestions:\n\n";
194+
$prompt .= $this->formatContext(array_merge(['existing_analysis' => $analysis], $context));
195+
196+
$prompt .= "\n\nProvide additional recommendations that complement the existing analysis.";
197+
198+
return $prompt;
199+
}
200+
201+
/**
202+
* Ensure provider is available.
203+
*
204+
* @throws QueryException If provider is not available
205+
*/
206+
protected function ensureAvailable(): void
207+
{
208+
if (!$this->isAvailable()) {
209+
throw new QueryException(
210+
'DeepSeek provider is not available. Please configure PDODB_AI_DEEPSEEK_KEY environment variable.',
211+
0
212+
);
213+
}
214+
}
215+
}
216+

0 commit comments

Comments
 (0)