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",
|
"model": "accounts/fireworks/models/kimi-k2p5",
|
||||||
"max_tokens": 4096,
|
"max_tokens": 4096,
|
||||||
"messages": []map[string]interface{}{
|
"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",
|
"role": "user",
|
||||||
"content": []map[string]interface{}{
|
"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
|
// 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))
|
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
Choices []struct {
|
Choices []struct {
|
||||||
Message struct {
|
Message struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
ReasoningContent string `json:"reasoning_content"`
|
||||||
} `json:"message"`
|
} `json:"message"`
|
||||||
} `json:"choices"`
|
} `json:"choices"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,6 +282,28 @@ func callFireworks(reqBody map[string]interface{}) (*DocumentAnalysis, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
content := result.Choices[0].Message.Content
|
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
|
// Extract JSON from response
|
||||||
if idx := strings.Index(content, "{"); idx >= 0 {
|
if idx := strings.Index(content, "{"); idx >= 0 {
|
||||||
|
|
@ -267,7 +314,17 @@ func callFireworks(reqBody map[string]interface{}) (*DocumentAnalysis, error) {
|
||||||
|
|
||||||
var analysis DocumentAnalysis
|
var analysis DocumentAnalysis
|
||||||
if err := json.Unmarshal([]byte(content), &analysis); err != nil {
|
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
|
// Validate category
|
||||||
|
|
@ -279,6 +336,44 @@ func callFireworks(reqBody map[string]interface{}) (*DocumentAnalysis, error) {
|
||||||
return &analysis, nil
|
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
|
// GenerateEmbedding creates a vector embedding using Fireworks
|
||||||
func GenerateEmbedding(text string) ([]float32, error) {
|
func GenerateEmbedding(text string) ([]float32, error) {
|
||||||
if fireworksAPIKey == "" {
|
if fireworksAPIKey == "" {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue