internal/llm/deepseek.go (view raw)
1package llm
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "strings"
8 "time"
9
10 "github.com/openai/openai-go/v3"
11 "github.com/openai/openai-go/v3/option"
12)
13
14// DeepseekClient is a client for interacting with Deepseek API.
15type DeepseekClient struct {
16 client openai.Client
17 model string
18 maxTokens int
19 temperature float64
20}
21
22// NewDeepseekClient creates a new Deepseek client.
23// apiKey: Deepseek API key (or read from DEEPSEEK_API_KEY env var)
24// baseURL: API base URL (defaults to "https://api.deepseek.com")
25// model: Model to use (defaults to "deepseek-chat")
26// maxTokens: Maximum tokens in response (defaults to 4000)
27// temperature: Sampling temperature (0.0 to 1.0, defaults to 0.7)
28func NewDeepseekClient(apiKey, baseURL, model string, maxTokens int, temperature float64) (*DeepseekClient, error) {
29 // Get API key from parameter or environment variable
30 if apiKey == "" {
31 apiKey = os.Getenv("DEEPSEEK_API_KEY")
32 }
33 if apiKey == "" || strings.TrimSpace(apiKey) == "" {
34 return nil, fmt.Errorf(
35 "Deepseek API key is required. " +
36 "Set DEEPSEEK_API_KEY environment variable or pass apiKey parameter",
37 )
38 }
39
40 // Set defaults
41 if baseURL == "" {
42 baseURL = "https://api.deepseek.com"
43 }
44 if model == "" {
45 model = "deepseek-chat"
46 }
47 if maxTokens == 0 {
48 maxTokens = 4000
49 }
50 if temperature == 0 {
51 temperature = 0.7
52 }
53
54 // Create OpenAI client configured for Deepseek
55 client := openai.NewClient(
56 option.WithAPIKey(apiKey),
57 option.WithBaseURL(baseURL),
58 )
59
60 fmt.Printf("Initialised Deepseek client with model: %s\n", model)
61
62 return &DeepseekClient{
63 client: client,
64 model: model,
65 maxTokens: maxTokens,
66 temperature: temperature,
67 }, nil
68}
69
70// SummariseArticles summarises a list of articles based on user prompt.
71// articles: List of article maps
72// userPrompt: User's query/summarisation request
73// systemPrompt: Optional system prompt (defaults to helpful assistant)
74func (dc *DeepseekClient) SummariseArticles(
75 articles []map[string]any,
76 userPrompt string,
77 systemPrompt string,
78) (string, error) {
79 if len(articles) == 0 {
80 return "No articles to summarise.", nil
81 }
82
83 // Prepare context from articles
84 context := dc.prepareContext(articles)
85
86 // Create messages for chat completion API
87 messages := dc.createMessages(context, userPrompt, systemPrompt)
88
89 // Call API with messages
90 return dc.callAPIWithMessages(messages)
91}
92
93func (dc *DeepseekClient) prepareContext(articles []map[string]any) string {
94 contextParts := []string{}
95
96 for i, article := range articles {
97 contextParts = append(contextParts, fmt.Sprintf("--- ARTICLE %d ---", i+1))
98 contextParts = append(contextParts, fmt.Sprintf("Title: %s", article["title"]))
99
100 if source, ok := article["source_feed"].(string); ok && source != "" {
101 contextParts = append(contextParts, fmt.Sprintf("Source: %s", source))
102 }
103
104 if published, ok := article["published"]; ok {
105 switch pub := published.(type) {
106 case time.Time:
107 if !pub.IsZero() {
108 contextParts = append(contextParts, fmt.Sprintf("Published: %s", pub.Format(time.RFC3339)))
109 }
110 case string:
111 contextParts = append(contextParts, fmt.Sprintf("Published: %s", pub))
112 default:
113 contextParts = append(contextParts, fmt.Sprintf("Published: %v", pub))
114 }
115 }
116
117 if author, ok := article["author"].(string); ok && author != "" {
118 contextParts = append(contextParts, fmt.Sprintf("Author: %s", author))
119 }
120
121 if link, ok := article["link"].(string); ok && link != "" {
122 contextParts = append(contextParts, fmt.Sprintf("Link: %s", link))
123 }
124
125 if content, ok := article["content"].(string); ok && content != "" {
126 // Truncate very long content
127 maxContentLen := 3000
128 if len(content) > maxContentLen {
129 content = content[:maxContentLen] + "... [truncated]"
130 }
131 contextParts = append(contextParts, fmt.Sprintf("Content: %s", content))
132 }
133
134 contextParts = append(contextParts, "") // Empty line between articles
135 }
136
137 return strings.Join(contextParts, "\n")
138}
139
140func (dc *DeepseekClient) createMessages(context, userPrompt, systemPrompt string) []openai.ChatCompletionMessageParamUnion {
141 if systemPrompt == "" {
142 systemPrompt = `You are an expert analyst and summariser.
143You analyse content from multiple sources and provide
144concise, insightful summaries based on user requests.
145Focus on key points, trends, and important information.`
146 }
147
148 // Combine context with user prompt
149 fullUserContent := fmt.Sprintf(`Here are articles from various RSS feeds:
150
151%s
152
153User request: %s
154
155Please provide a comprehensive summary/analysis addressing the user's request.
156Focus on key insights, trends, and important information from the articles.
157If relevant, note any patterns, contradictions, or notable developments.`,
158 context, userPrompt)
159
160 messages := []openai.ChatCompletionMessageParamUnion{
161 openai.SystemMessage(systemPrompt),
162 openai.UserMessage(fullUserContent),
163 }
164
165 return messages
166}
167
168func (dc *DeepseekClient) callAPIWithMessages(messages []openai.ChatCompletionMessageParamUnion) (string, error) {
169 ctx := context.Background()
170
171 fmt.Printf("Calling Deepseek API with model: %s\n", dc.model)
172
173 response, err := dc.client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
174 Model: dc.model,
175 Messages: messages,
176 MaxTokens: openai.Int(int64(dc.maxTokens)),
177 Temperature: openai.Float(dc.temperature),
178 })
179
180 if err != nil {
181 // Provide more specific error messages
182 errStr := err.Error()
183 if strings.Contains(errStr, "401") {
184 return "", fmt.Errorf("invalid API key. Please check your Deepseek API key")
185 } else if strings.Contains(errStr, "429") {
186 return "", fmt.Errorf("rate limit exceeded. Please try again later")
187 } else if strings.Contains(errStr, "500") {
188 return "", fmt.Errorf("Deepseek API server error. Please try again later")
189 } else if strings.Contains(errStr, "404") {
190 return "", fmt.Errorf("API endpoint not found. Please check the base URL and endpoint. Deepseek uses /chat/completions")
191 }
192 return "", fmt.Errorf("failed to connect to Deepseek API: %w", err)
193 }
194
195 // Extract text from response
196 if len(response.Choices) == 0 {
197 return "", fmt.Errorf("no response choices returned from API")
198 }
199
200 outputText := response.Choices[0].Message.Content
201
202 // Print token usage
203 fmt.Printf(
204 "Deepseek API response: %d prompt tokens, %d completion tokens\n",
205 response.Usage.PromptTokens,
206 response.Usage.CompletionTokens,
207 )
208
209 return outputText, nil
210}
211
212// AnalyseWithCustomPrompt analyses articles with custom system and user prompts.
213func (dc *DeepseekClient) AnalyseWithCustomPrompt(
214 articles []map[string]any,
215 systemPrompt string,
216 userPrompt string,
217) (string, error) {
218 context := dc.prepareContext(articles)
219
220 // Create messages for chat completion API
221 messages := []openai.ChatCompletionMessageParamUnion{
222 openai.SystemMessage(systemPrompt),
223 openai.UserMessage(fmt.Sprintf(`Here are articles from various RSS feeds:
224
225%s
226
227%s`, context, userPrompt)),
228 }
229
230 return dc.callAPIWithMessages(messages)
231}