chore: auto-commit uncommitted changes
This commit is contained in:
parent
9d6ad09b53
commit
00d8f7c94a
103
ai.go
103
ai.go
|
|
@ -170,6 +170,7 @@ Respond in JSON ONLY:
|
|||
"model": "accounts/fireworks/models/kimi-k2p5",
|
||||
"max_tokens": 4096,
|
||||
"messages": []map[string]interface{}{
|
||||
{"role": "system", "content": "You are a document analysis API. You MUST respond with raw JSON only. No markdown, no code fences, no explanation text. Start your response with { and end with }."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": []map[string]interface{}{
|
||||
|
|
@ -180,7 +181,28 @@ Respond in JSON ONLY:
|
|||
},
|
||||
}
|
||||
|
||||
return callFireworks(reqBody)
|
||||
analysis, err := callFireworks(reqBody)
|
||||
if err != nil {
|
||||
// Retry once with a simpler prompt that's harder for the model to misinterpret
|
||||
log.Printf(" [AI] First attempt failed, retrying with simplified prompt...")
|
||||
retryBody := map[string]interface{}{
|
||||
"model": "accounts/fireworks/models/kimi-k2p5",
|
||||
"max_tokens": 4096,
|
||||
"messages": []map[string]interface{}{
|
||||
{"role": "system", "content": "Output valid JSON only. No other text."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": []map[string]interface{}{
|
||||
{"type": "image_url", "image_url": map[string]string{"url": "data:image/png;base64," + b64}},
|
||||
{"type": "text", "text": `Look at this document. Return ONLY this JSON (fill in values):
|
||||
{"category":"uncategorized","doc_type":"unknown","date":"","vendor":"","amount":"","title":"Short Title Here","summary":"One sentence.","full_text":"All visible text here"}`},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return callFireworks(retryBody)
|
||||
}
|
||||
return analysis, nil
|
||||
}
|
||||
|
||||
// AnalyzeText uses K2 text model for plain text files
|
||||
|
|
@ -241,14 +263,17 @@ func callFireworks(reqBody map[string]interface{}) (*DocumentAnalysis, error) {
|
|||
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -257,6 +282,28 @@ func callFireworks(reqBody map[string]interface{}) (*DocumentAnalysis, error) {
|
|||
}
|
||||
|
||||
content := result.Choices[0].Message.Content
|
||||
reasoning := result.Choices[0].Message.ReasoningContent
|
||||
|
||||
// K2.5 reasoning mode: actual JSON may be in content or reasoning_content
|
||||
// Try content first, if it doesn't look like JSON, try reasoning_content
|
||||
if !strings.Contains(content, "{") && reasoning != "" && strings.Contains(reasoning, "{") {
|
||||
log.Printf(" [AI] Using reasoning_content (content had no JSON)")
|
||||
content = reasoning
|
||||
}
|
||||
|
||||
// Strip markdown code fences (```json ... ``` or ``` ... ```)
|
||||
content = strings.TrimSpace(content)
|
||||
if strings.HasPrefix(content, "```") {
|
||||
// Remove opening fence (```json or ```)
|
||||
if idx := strings.Index(content, "\n"); idx >= 0 {
|
||||
content = content[idx+1:]
|
||||
}
|
||||
// Remove closing fence
|
||||
if idx := strings.LastIndex(content, "```"); idx >= 0 {
|
||||
content = content[:idx]
|
||||
}
|
||||
content = strings.TrimSpace(content)
|
||||
}
|
||||
|
||||
// Extract JSON from response
|
||||
if idx := strings.Index(content, "{"); idx >= 0 {
|
||||
|
|
@ -267,7 +314,17 @@ func callFireworks(reqBody map[string]interface{}) (*DocumentAnalysis, error) {
|
|||
|
||||
var analysis DocumentAnalysis
|
||||
if err := json.Unmarshal([]byte(content), &analysis); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
// Last resort: try to find a JSON object with braces matching
|
||||
cleaned := extractJSONObject(content)
|
||||
if cleaned != "" {
|
||||
if err2 := json.Unmarshal([]byte(cleaned), &analysis); err2 != nil {
|
||||
log.Printf(" [AI debug] Failed to parse even after cleanup. Content starts: %.200s", content)
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf(" [AI debug] No JSON object found in response. Content starts: %.200s", content)
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate category
|
||||
|
|
@ -279,6 +336,44 @@ func callFireworks(reqBody map[string]interface{}) (*DocumentAnalysis, error) {
|
|||
return &analysis, nil
|
||||
}
|
||||
|
||||
// extractJSONObject tries to find a balanced JSON object in a string
|
||||
func extractJSONObject(s string) string {
|
||||
start := strings.Index(s, "{")
|
||||
if start < 0 {
|
||||
return ""
|
||||
}
|
||||
depth := 0
|
||||
inString := false
|
||||
escaped := false
|
||||
for i := start; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
if c == '\\' && inString {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
if c == '"' {
|
||||
inString = !inString
|
||||
continue
|
||||
}
|
||||
if inString {
|
||||
continue
|
||||
}
|
||||
if c == '{' {
|
||||
depth++
|
||||
} else if c == '}' {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
return s[start : i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GenerateEmbedding creates a vector embedding using Fireworks
|
||||
func GenerateEmbedding(text string) ([]float32, error) {
|
||||
if fireworksAPIKey == "" {
|
||||
|
|
|
|||
Loading…
Reference in New Issue