package reasoner import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) // HF IDs named in docs. No Qwen3.6-9B exists. const ( HFQwen35_9B = "Qwen/Qwen3.5-9B" HFQwen36_27B = "Qwen/Qwen3.6-27B" HFBonsai27B = "prism-ml/Bonsai-27B-gguf" OllamaRAM = "qwen3.5:9b" OllamaQuality = "MichelRosselli/bonsai-27b:Q1_0" ) type ToolCall struct { Name string Arguments string } type Result struct { Model string `json:"model"` OK bool `json:"ok"` ToolName string `json:"tool_name,omitempty"` XMLLeak bool `json:"xml_leak"` Err string `json:"error,omitempty"` LatencyMS int64 `json:"latency_ms"` RSSMB int `json:"rss_mb,omitempty"` Device string `json:"device"` WantedTool string `json:"wanted_tool"` } type Prompt struct { Name string Want string User string } var BakePrompts = []Prompt{ { Name: "search-before-claim", Want: "search", User: "Use tools. Search the 2dph brain for LadybugDB before you answer. Call search.", }, { Name: "get-leaf", Want: "get", User: "Use tools. Fetch leaf id leaf-demo with get. Do not invent the body.", }, { Name: "audit-index", Want: "audit", User: "Use tools. Call audit on the brain index health.", }, } func MCPTools() []map[string]any { return []map[string]any{ openaiTool("search", "deduction search (facts → info → web)", map[string]any{ "type": "object", "properties": map[string]any{ "q": map[string]any{"type": "string", "description": "search query"}, "n": map[string]any{"type": "integer"}, }, "required": []string{"q"}, }), openaiTool("get", "read one leaf by id", map[string]any{ "type": "object", "properties": map[string]any{ "id": map[string]any{"type": "string"}, "body": map[string]any{"type": "boolean"}, }, "required": []string{"id"}, }), openaiTool("audit", "facts confidence histogram", map[string]any{ "type": "object", "properties": map[string]any{}, }), } } func openaiTool(name, desc string, schema map[string]any) map[string]any { return map[string]any{ "type": "function", "function": map[string]any{ "name": name, "description": desc, "parameters": schema, }, } } type Client struct { BaseURL string Model string HTTP *http.Client Device string ToolChoice string } type Report struct { Model string `json:"model"` HF string `json:"hf_id"` Device string `json:"device"` ToolCallOK int `json:"tool_call_ok"` ToolCallN int `json:"tool_call_n"` XMLLeak int `json:"xml_leak"` RSSMB int `json:"rss_mb"` VRAMMB int `json:"vram_mb"` LatencyP50MS float64 `json:"latency_p50_ms,omitempty"` LatencyP95MS float64 `json:"latency_p95_ms,omitempty"` Prompts []Result `json:"prompts"` } func HFFor(model string) string { switch model { case OllamaRAM: return HFQwen35_9B case OllamaQuality: return HFBonsai27B default: if strings.Contains(model, "qwen3.6") || strings.Contains(model, "Qwen3.6") { return HFQwen36_27B } return "" } } func (c *Client) httpc() *http.Client { if c.HTTP == nil { c.HTTP = &http.Client{Timeout: 10 * time.Minute} } return c.HTTP } func Origin(base string) string { s := strings.TrimRight(base, "/") return strings.TrimSuffix(s, "/v1") } func (c Client) ChatTools(user string) (ToolCall, string, error) { choice := c.ToolChoice if choice == "" { choice = "required" } body, _ := json.Marshal(map[string]any{ "model": c.Model, "messages": []map[string]string{ {"role": "system", "content": "You are PicoClaw talking to 2dph MCP. Always call a tool before a factual claim. search then get then audit."}, {"role": "user", "content": user}, }, "tools": MCPTools(), "tool_choice": choice, }) base := strings.TrimRight(c.BaseURL, "/") if !strings.HasSuffix(base, "/v1") { base += "/v1" } url := base + "/chat/completions" req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { return ToolCall{}, "", err } req.Header.Set("Content-Type", "application/json") res, err := c.httpc().Do(req) if err != nil { return ToolCall{}, "", err } defer res.Body.Close() raw, _ := io.ReadAll(res.Body) if res.StatusCode >= 300 { return ToolCall{}, string(raw), fmt.Errorf("http %d", res.StatusCode) } return ParseToolResponse(raw) } func ParseToolResponse(raw []byte) (ToolCall, string, error) { var wrap struct { Choices []struct { Message struct { Content string `json:"content"` ToolCalls []struct { Function struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } `json:"function"` } `json:"tool_calls"` } `json:"message"` } `json:"choices"` } if err := json.Unmarshal(raw, &wrap); err != nil { return ToolCall{}, "", err } content := "" if len(wrap.Choices) > 0 { content = wrap.Choices[0].Message.Content if n := len(wrap.Choices[0].Message.ToolCalls); n > 0 { fn := wrap.Choices[0].Message.ToolCalls[0].Function return ToolCall{Name: fn.Name, Arguments: rawArgs(fn.Arguments)}, content, nil } } return ToolCall{}, content, fmt.Errorf("no tool_calls") } func rawArgs(raw json.RawMessage) string { if len(raw) == 0 { return "" } var s string if err := json.Unmarshal(raw, &s); err == nil { return s } return string(raw) } func XMLLeak(content string) bool { s := strings.ToLower(content) return strings.Contains(s, "") || strings.Contains(s, "= 300 { return nil } return ParsePS(raw) } func Run(c Client) Report { if c.Device == "" { c.Device = "cpu" } rep := Report{ Model: c.Model, HF: HFFor(c.Model), Device: c.Device, } for _, p := range BakePrompts { r := RunPrompt(c, p) rep.Prompts = append(rep.Prompts, r) rep.ToolCallN++ if r.OK { rep.ToolCallOK++ } if r.XMLLeak { rep.XMLLeak++ } } if mems := c.FetchPS(); len(mems) > 0 { rep.RSSMB = mems[0].SizeMB rep.VRAMMB = mems[0].VRAMMB if c.Device == "cpu" && mems[0].VRAMMB > 0 { rep.Device = "gpu" } for i := range rep.Prompts { rep.Prompts[i].RSSMB = rep.RSSMB } } return rep }