Initial commit
This commit is contained in:
213
llm/client.go
Normal file
213
llm/client.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/tell-me/tools"
|
||||
)
|
||||
|
||||
// Client wraps the OpenAI client for LLM interactions
|
||||
type Client struct {
|
||||
client *openai.Client
|
||||
model string
|
||||
contextSize int
|
||||
searxngURL string
|
||||
}
|
||||
|
||||
// NewClient creates a new LLM client
|
||||
func NewClient(apiURL, apiKey, model string, contextSize int, searxngURL string) *Client {
|
||||
config := openai.DefaultConfig(apiKey)
|
||||
config.BaseURL = apiURL
|
||||
|
||||
client := openai.NewClientWithConfig(config)
|
||||
|
||||
return &Client{
|
||||
client: client,
|
||||
model: model,
|
||||
contextSize: contextSize,
|
||||
searxngURL: searxngURL,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSystemPrompt returns the system prompt that enforces search-first behavior
|
||||
func GetSystemPrompt() string {
|
||||
currentDate := time.Now().Format("2006-01-02")
|
||||
|
||||
return fmt.Sprintf(`You are a helpful AI research assistant with access to web search and article fetching capabilities.
|
||||
|
||||
RESEARCH WORKFLOW - MANDATORY STEPS:
|
||||
1. For questions requiring current information, facts, or knowledge beyond your training data:
|
||||
- Perform MULTIPLE searches (typically 2-3) with DIFFERENT query angles to gather comprehensive information
|
||||
- Vary your search terms to capture different perspectives and sources
|
||||
|
||||
2. After completing ALL searches, analyze the combined results:
|
||||
- Review ALL search results from your multiple searches together
|
||||
- Identify the 3-5 MOST relevant and authoritative URLs across ALL searches
|
||||
- Prioritize: official sources, reputable news sites, technical documentation, expert reviews
|
||||
- Look for sources that complement each other (e.g., official specs + expert analysis + user reviews)
|
||||
|
||||
3. Fetch the selected articles:
|
||||
- Use fetch_articles with the 3-5 best URLs you identified from ALL your searches
|
||||
- Read all fetched content thoroughly before formulating your answer
|
||||
- Synthesize information from multiple sources for a comprehensive response
|
||||
|
||||
HANDLING USER CORRECTIONS - CRITICAL:
|
||||
When a user indicates your answer is incorrect, incomplete, or needs clarification:
|
||||
1. NEVER argue or defend your previous answer
|
||||
2. IMMEDIATELY acknowledge the correction: "Let me search for more accurate information"
|
||||
3. Perform NEW searches with DIFFERENT queries based on the user's feedback
|
||||
4. Fetch NEW sources that address the specific correction or clarification needed
|
||||
5. Provide an updated answer based on the new research
|
||||
6. If the user provides specific information, incorporate it and verify with additional searches
|
||||
|
||||
Remember: The user may have more current or specific knowledge. Your role is to research and verify, not to argue.
|
||||
|
||||
OUTPUT FORMATTING RULES:
|
||||
- NEVER include source URLs or citations in your response
|
||||
- DO NOT use Markdown formatting (no **, ##, -, *, [], etc.)
|
||||
- Write in plain text only - use natural language without any special formatting
|
||||
- For emphasis, use CAPITAL LETTERS instead of bold or italics
|
||||
- For lists, use simple numbered lines (1., 2., 3.) or write as flowing paragraphs
|
||||
- Keep output clean and readable for terminal display
|
||||
|
||||
Available tools:
|
||||
- web_search: Search the internet (can be used multiple times with different queries)
|
||||
- fetch_articles: Fetch and read content from 1-5 URLs at once
|
||||
|
||||
CURRENT DATE: %s`, currentDate)
|
||||
}
|
||||
|
||||
// GetTools returns the tool definitions for the LLM
|
||||
func GetTools() []openai.Tool {
|
||||
return []openai.Tool{
|
||||
{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: &openai.FunctionDefinition{
|
||||
Name: "web_search",
|
||||
Description: "Search the internet for information using SearXNG. Use this tool to find current information, facts, news, or any knowledge you need to answer the user's question.",
|
||||
Parameters: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query to find relevant information"
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: &openai.FunctionDefinition{
|
||||
Name: "fetch_articles",
|
||||
Description: "Fetch and read content from 1-5 articles at once. Provide both titles and URLs from search results. The HTML will be converted to clean text format and combined. Use this after searching to read the most relevant pages together.",
|
||||
Parameters: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"articles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The title of the article from search results"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The URL to fetch (must start with http:// or https://)"
|
||||
}
|
||||
},
|
||||
"required": ["title", "url"]
|
||||
},
|
||||
"description": "Array of articles with titles and URLs (1-5 recommended, max 5)"
|
||||
}
|
||||
},
|
||||
"required": ["articles"]
|
||||
}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Chat sends a message and handles tool calls
|
||||
func (c *Client) Chat(ctx context.Context, messages []openai.ChatCompletionMessage) (string, []openai.ChatCompletionMessage, error) {
|
||||
req := openai.ChatCompletionRequest{
|
||||
Model: c.model,
|
||||
Messages: messages,
|
||||
Tools: GetTools(),
|
||||
}
|
||||
|
||||
resp, err := c.client.CreateChatCompletion(ctx, req)
|
||||
if err != nil {
|
||||
return "", messages, fmt.Errorf("chat completion failed: %w", err)
|
||||
}
|
||||
|
||||
choice := resp.Choices[0]
|
||||
messages = append(messages, choice.Message)
|
||||
|
||||
// Handle tool calls
|
||||
if len(choice.Message.ToolCalls) > 0 {
|
||||
for _, toolCall := range choice.Message.ToolCalls {
|
||||
var result string
|
||||
|
||||
switch toolCall.Function.Name {
|
||||
case "web_search":
|
||||
var args struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
|
||||
result = fmt.Sprintf("Error parsing arguments: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Searching: %s\n", args.Query)
|
||||
result, err = tools.WebSearch(c.searxngURL, args.Query)
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("Search error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case "fetch_articles":
|
||||
var args struct {
|
||||
Articles []struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
} `json:"articles"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
|
||||
result = fmt.Sprintf("Error parsing arguments: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Reading %d articles:\n", len(args.Articles))
|
||||
urls := make([]string, len(args.Articles))
|
||||
for i, article := range args.Articles {
|
||||
fmt.Printf(" - %s\n", article.Title)
|
||||
urls[i] = article.URL
|
||||
}
|
||||
result, err = tools.FetchArticles(urls)
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("Fetch error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
result = fmt.Sprintf("Unknown tool: %s", toolCall.Function.Name)
|
||||
}
|
||||
|
||||
// Add tool response to messages
|
||||
messages = append(messages, openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleTool,
|
||||
Content: result,
|
||||
ToolCallID: toolCall.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// Make another call with tool results
|
||||
return c.Chat(ctx, messages)
|
||||
}
|
||||
|
||||
return choice.Message.Content, messages, nil
|
||||
}
|
||||
Reference in New Issue
Block a user