-
Notifications
You must be signed in to change notification settings - Fork 0
Convert json.RawMessage to JSON strings instead of byte arrays (#41) #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 33 commits
32fb1d3
4687661
a8be24d
c777998
eb6a679
4d8ab4c
24a2c20
e60bcff
be7a811
ab7ea01
74aeefc
d6800db
7cba29c
5e1402b
b6123b8
be5dc1a
b6ab2be
222dab6
44c4a63
a77b8aa
90b7b9c
d9d468c
80289dd
3868624
684686d
5471a3c
a4ee1da
b6e0935
5571de3
7a85328
937606c
a1ac86c
3c8c682
c680026
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,3 +71,6 @@ clicky | |
| formatters/.playwright-mcp/ | ||
| __debug* | ||
|
|
||
| ginkgo.report | ||
| *.bak | ||
| examples/uber_demo/uber_demo | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package ai | |
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "time" | ||
|
|
||
| "github.com/flanksource/clicky/api" | ||
|
|
@@ -40,27 +41,63 @@ type AgentConfig struct { | |
| NoCache bool `json:"no_cache,omitempty"` | ||
| } | ||
|
|
||
| func (a AgentConfig) Pretty() api.Text { | ||
| t := api.Text{}.Append(string(a.Type)) | ||
| if a.Model != "" { | ||
| t = t.Space().Append(a.Model, "font-mono") | ||
| } | ||
| if a.NoCache { | ||
| t = t.Space().Append(" No Cache", "text-orange-500") | ||
| } | ||
| return t | ||
| } | ||
|
|
||
| // PromptRequest represents a request to process a prompt | ||
| type PromptRequest struct { | ||
| Context map[string]string `json:"context,omitempty"` | ||
| Name string `json:"name"` | ||
| Prompt string `json:"prompt"` | ||
| StructuredOutput interface{} `json:"structured_output,omitempty"` // Schema for structured JSON output | ||
| } | ||
|
|
||
| // TypedPromptRequest represents a type-safe request with structured output | ||
| type TypedPromptRequest[T any] struct { | ||
| Context map[string]string `json:"context,omitempty"` | ||
| Name string `json:"name"` | ||
| Prompt string `json:"prompt"` | ||
| } | ||
|
|
||
| // PromptResponse represents the response from processing a prompt | ||
| type PromptResponse struct { | ||
| Request PromptRequest `json:"request,omitempty"` | ||
| Result string `json:"result"` | ||
| Costs Costs `json:"costs,omitempty"` | ||
| Model string `json:"model,omitempty"` | ||
| Error string `json:"error,omitempty"` | ||
| Request PromptRequest `json:"request,omitempty"` | ||
| Result string `json:"result"` | ||
| StructuredData interface{} `json:"structured_data,omitempty"` // Populated if structured output was requested | ||
| Costs Costs `json:"costs,omitempty"` | ||
| Model string `json:"model,omitempty"` | ||
| Error string `json:"error,omitempty"` | ||
| // Total wall-clock duration of the request | ||
| Duration time.Duration `json:"duration,omitempty"` | ||
| // Duration spent in the model processing as reported by the API | ||
| DurationModel time.Duration `json:"duration_model,omitempty"` | ||
| CacheHit bool `json:"cache_hit,omitempty"` | ||
| } | ||
|
|
||
| // TypedPromptResponse represents a type-safe response with structured output | ||
| type TypedPromptResponse[T any] struct { | ||
| Request TypedPromptRequest[T] `json:"request,omitempty"` | ||
| Data T `json:"data"` | ||
| Costs Costs `json:"costs,omitempty"` | ||
| Model string `json:"model,omitempty"` | ||
| Error string `json:"error,omitempty"` | ||
| Duration time.Duration `json:"duration,omitempty"` | ||
| DurationModel time.Duration `json:"duration_model,omitempty"` | ||
| CacheHit bool `json:"cache_hit,omitempty"` | ||
| } | ||
|
|
||
| func (tr TypedPromptResponse[T]) IsOK() bool { | ||
| return tr.Error == "" | ||
| } | ||
|
|
||
| func (pr PromptResponse) IsOK() bool { | ||
| return pr.Error == "" | ||
| } | ||
|
|
@@ -111,3 +148,49 @@ type Agent interface { | |
| // Close cleans up resources | ||
| Close() error | ||
| } | ||
|
|
||
| // ExecutePromptTyped is a generic helper that executes a prompt with type-safe structured output. | ||
| // It automatically cleans up the JSON response before unmarshaling into the target type. | ||
| func ExecutePromptTyped[T any](ctx context.Context, agent Agent, request TypedPromptRequest[T]) (*TypedPromptResponse[T], error) { | ||
| // Convert to regular PromptRequest | ||
| var schema T | ||
| promptReq := PromptRequest{ | ||
| Context: request.Context, | ||
| Name: request.Name, | ||
| Prompt: request.Prompt, | ||
| StructuredOutput: &schema, | ||
| } | ||
|
|
||
| // Execute the request | ||
| resp, err := agent.ExecutePrompt(ctx, promptReq) | ||
| if err != nil { | ||
| return &TypedPromptResponse[T]{ | ||
| Request: request, | ||
| Error: err.Error(), | ||
| }, err | ||
| } | ||
|
|
||
| // Convert to typed response | ||
| typedResp := &TypedPromptResponse[T]{ | ||
| Request: request, | ||
| Costs: resp.Costs, | ||
| Model: resp.Model, | ||
| Duration: resp.Duration, | ||
| DurationModel: resp.DurationModel, | ||
| CacheHit: resp.CacheHit, | ||
| Error: resp.Error, | ||
| } | ||
|
|
||
| // If there's structured data, convert it | ||
| if resp.StructuredData != nil { | ||
| if data, ok := resp.StructuredData.(*T); ok { | ||
| typedResp.Data = *data | ||
| } else { | ||
| err := json.Unmarshal([]byte("failed to convert"), &typedResp.Data) | ||
| typedResp.Error = "failed to convert structured data to target type" | ||
| return typedResp, err | ||
| } | ||
| } | ||
|
|
||
| return typedResp, nil | ||
| } | ||
|
Comment on lines
+152
to
+203
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainFix missing Two points here:
🏁 Script executed: #!/bin/bash
# Check imports at the top of ai/api.go
head -50 ai/api.go | grep -E "^import|^[[:space:]]*\"" | head -20Length of output: 124 🏁 Script executed: #!/bin/bash
# Search for StructuredData field definition and PromptRequest/TypedPromptResponse types
rg -n "StructuredData|PromptRequest|TypedPromptResponse" ai/api.go -A2 -B2 | head -100Length of output: 2892 🏁 Script executed: #!/bin/bash
# Search for where StructuredData is assigned or populated in the codebase
rg -n "StructuredData\s*=" --type=go -C3 | head -80Length of output: 0 🏁 Script executed: #!/bin/bash
# Check if fmt is used elsewhere in ai/api.go
rg -n "fmt\." ai/api.goLength of output: 200 🏁 Script executed: #!/bin/bash
# Find Agent interface implementations (implementations of ExecutePrompt)
rg -n "func.*ExecutePrompt" --type=go -B5 -A15 | head -150Length of output: 5395 🏁 Script executed: #!/bin/bash
# Search for any code that creates PromptResponse and sets StructuredData
rg -n "StructuredData:" --type=go -B3 -A3Length of output: 0 🏁 Script executed: #!/bin/bash
# Check if json.RawMessage is imported or used anywhere in the codebase
rg -n "json\.RawMessage|RawMessage" --type=goLength of output: 0 Fix missing The code uses import (
"context"
"encoding/json"
"fmt"
"time"
// ...
)StructuredData conversion edge cases (optional) The conversion logic handles 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package ai | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "regexp" | ||
| "strings" | ||
| ) | ||
|
|
||
| var ( | ||
| // Match markdown code blocks with optional language specifier | ||
| codeBlockRegex = regexp.MustCompile("(?s)```(?:json)?\\s*(.+?)```") | ||
|
|
||
| // Match JSON objects or arrays | ||
| jsonObjectRegex = regexp.MustCompile(`(?s)\{.*\}`) | ||
| jsonArrayRegex = regexp.MustCompile(`(?s)\[.*\]`) | ||
| ) | ||
|
|
||
| // CleanupJSONResponse attempts to extract and clean JSON from LLM responses | ||
| // that may contain markdown formatting, explanatory text, or other noise. | ||
| // | ||
| // It tries the following strategies in order: | ||
| // 1. Extract JSON from markdown code blocks (```json or ```) | ||
| // 2. Extract the first JSON object {...} | ||
| // 3. Extract the first JSON array [...] | ||
| // 4. Return the trimmed original string | ||
| // | ||
| // After extraction, it validates that the result is valid JSON. | ||
| func CleanupJSONResponse(response string) string { | ||
| // Trim whitespace | ||
| response = strings.TrimSpace(response) | ||
|
|
||
| // If it's already valid JSON, return as-is | ||
| if isValidJSON(response) { | ||
| return response | ||
| } | ||
|
|
||
| // Strategy 1: Extract from markdown code blocks | ||
| if matches := codeBlockRegex.FindStringSubmatch(response); len(matches) > 1 { | ||
| extracted := strings.TrimSpace(matches[1]) | ||
| if isValidJSON(extracted) { | ||
| return extracted | ||
| } | ||
| } | ||
|
|
||
| // Strategy 2: Extract first JSON object | ||
| if match := jsonObjectRegex.FindString(response); match != "" { | ||
| if isValidJSON(match) { | ||
| return match | ||
| } | ||
| } | ||
|
|
||
| // Strategy 3: Extract first JSON array | ||
| if match := jsonArrayRegex.FindString(response); match != "" { | ||
| if isValidJSON(match) { | ||
| return match | ||
| } | ||
| } | ||
|
|
||
| // Strategy 4: Return original trimmed | ||
| return response | ||
| } | ||
|
|
||
| // isValidJSON checks if a string is valid JSON | ||
| func isValidJSON(s string) bool { | ||
| var js interface{} | ||
| return json.Unmarshal([]byte(s), &js) == nil | ||
| } | ||
|
|
||
| // UnmarshalWithCleanup attempts to unmarshal JSON with automatic cleanup | ||
| func UnmarshalWithCleanup(data string, v interface{}) error { | ||
| // First try direct unmarshal | ||
| if err := json.Unmarshal([]byte(data), v); err == nil { | ||
| return nil | ||
| } | ||
|
|
||
| // If that fails, try with cleanup | ||
| cleaned := CleanupJSONResponse(data) | ||
| return json.Unmarshal([]byte(cleaned), v) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.