all repos — llm_aggregator @ 7b6943f1a3f252697b0e16d94c9635a5e064090b

A CLI tool to aggregate RSS feeds and summarise them with LLMs

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}