a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
175 lines
6.6 KiB
Go
175 lines
6.6 KiB
Go
//go:build llama
|
|
|
|
// agentdemo: drive the structured tool-calling agent against either
|
|
// canned mock data or the real gortex daemon. Same agent loop, the
|
|
// only variable is the backend.
|
|
//
|
|
// go build -tags llama -o /tmp/agentdemo ./internal/llm/cmd/agentdemo
|
|
// /tmp/agentdemo -model ~/models/qwen2.5-3b-instruct-q4_k_m.gguf \
|
|
// -question 'who calls LoadConfig?'
|
|
// /tmp/agentdemo -backend daemon -repo gortex \
|
|
// -model ~/models/qwen2.5-3b-instruct-q4_k_m.gguf \
|
|
// -question 'who calls handleSearchSymbols?'
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/zzet/gortex/internal/llm"
|
|
"github.com/zzet/gortex/internal/llm/agent"
|
|
"github.com/zzet/gortex/internal/llm/provider"
|
|
)
|
|
|
|
const promptP1 = `RULES (follow these exactly):
|
|
- If the user gives you only a bare name (not a path-qualified id like "pkg/x.Foo"), you MUST first call search_symbols to resolve it to an id before calling get_callers.
|
|
- For search_symbols, pass ONLY the bare symbol name as "query" — no prepositions, no package qualifiers, no extra words. Do not write "Foo in pkg/bar" — write just "Foo".
|
|
- search_symbols returns ranked matches; the FIRST few are best. Do not walk every match — pick at most the top 1-3 that look like functions or methods and ignore the rest (fields, params, strings).
|
|
- Make at least one real tool call before final_answer.
|
|
- Never call the same tool with the same args twice in a row. If a tool result is empty, try a DIFFERENT tool or DIFFERENT args.
|
|
- When you have gathered enough information, call final_answer summarising what you found.`
|
|
|
|
const promptP2 = promptP1 + `
|
|
|
|
EXAMPLE — answer "who calls Foo?":
|
|
Step 1 emit: {"tool":"search_symbols","args":{"query":"Foo"}}
|
|
Step 1 result: {"matches":[{"id":"pkg/x.Foo","kind":"function","path":"pkg/x/foo.go"}]}
|
|
Step 2 emit: {"tool":"get_callers","args":{"id":"pkg/x.Foo"}}
|
|
Step 2 result: {"callers":[{"id":"pkg/y.bar","file":"pkg/y/bar.go"}]}
|
|
Step 3 emit: {"tool":"final_answer","args":{"text":"Foo is called by pkg/y.bar in pkg/y/bar.go."}}`
|
|
|
|
const promptChain = `RULES (follow these exactly):
|
|
- You are tracing a cross-system call chain. Output one tool call per turn.
|
|
- DIRECTION MATTERS. Only these tools are correct in chain mode:
|
|
* contracts — find producer↔consumer pairs across repos
|
|
* get_dependencies — FORWARD direction: what does this symbol call/import?
|
|
* final_answer — emit the chain
|
|
Do NOT use get_callers. get_callers walks BACKWARDS (who calls X), which is
|
|
the WRONG direction for chain tracing and will lead you astray.
|
|
- For search_symbols and contracts, pass clean values (no extra words).
|
|
- Typical flow for "trace request X across systems":
|
|
1) contracts({"role":"consumer","path":"<path>"}) — find the caller side.
|
|
2) contracts({"role":"provider","path":"<path>"}) — find the handler.
|
|
3) get_dependencies({"id":"<provider symbol_id>"}) — see what the handler calls.
|
|
4) For deeper hops, call get_dependencies AGAIN on the most interesting result's id.
|
|
Repeat until you reach a symbol in a third repo OR you have enough to answer.
|
|
5) Look for deps whose repo prefix differs from the handler's repo —
|
|
those are the cross-repo downstream calls.
|
|
6) Call final_answer with the chain as numbered steps.
|
|
- Never call the same tool with the same args twice in a row.
|
|
- final_answer.text should list each system hop with its symbol id and repo.`
|
|
|
|
func promptByName(name string) (string, error) {
|
|
switch name {
|
|
case "", "p0":
|
|
return "", nil
|
|
case "p1":
|
|
return promptP1, nil
|
|
case "p2":
|
|
return promptP2, nil
|
|
case "chain":
|
|
return promptChain, nil
|
|
}
|
|
return "", fmt.Errorf("unknown prompt variant %q (use p0|p1|p2|chain)", name)
|
|
}
|
|
|
|
func main() {
|
|
modelPath := flag.String("model", "", "path to .gguf model (required)")
|
|
question := flag.String("question", "who calls LoadConfig?", "question for the agent")
|
|
maxSteps := flag.Int("steps", 16, "max agent steps before giving up")
|
|
nCtx := flag.Int("ctx", 4096, "context size")
|
|
gpu := flag.Int("gpu", 999, "GPU layers to offload (Metal); 0 = CPU only")
|
|
tmplName := flag.String("template", "chatml", "chat template: chatml | llama3")
|
|
backendName := flag.String("backend", "mock", "backend: mock | daemon")
|
|
gortexBin := flag.String("gortex-bin", "", "path to gortex binary (default: gortex on PATH)")
|
|
repo := flag.String("repo", "", "restrict queries to this repo prefix")
|
|
project := flag.String("project", "", "restrict queries to this project")
|
|
ref := flag.String("ref", "", "restrict queries to this ref tag")
|
|
promptName := flag.String("prompt", "p2", "prompt variant: p0 | p1 | p2 | chain")
|
|
chainMode := flag.Bool("chain", false, "register contract + dependency tools for cross-system tracing")
|
|
flag.Parse()
|
|
|
|
systemExtras, err := promptByName(*promptName)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(2)
|
|
}
|
|
if *modelPath == "" {
|
|
fmt.Fprintln(os.Stderr, "error: -model is required")
|
|
os.Exit(2)
|
|
}
|
|
|
|
var backend llm.Backend
|
|
switch *backendName {
|
|
case "mock":
|
|
backend = llm.MockBackend{}
|
|
case "daemon":
|
|
db, err := llm.NewDaemonBackend(*gortexBin, nil)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "daemon backend: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer db.Close()
|
|
backend = db
|
|
fmt.Fprintf(os.Stderr, "[daemon backend: gortex mcp subprocess]\n")
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown -backend %q (use mock | daemon)\n", *backendName)
|
|
os.Exit(2)
|
|
}
|
|
scope := llm.Scope{Repo: *repo, Project: *project, Ref: *ref}
|
|
var tools []agent.Tool
|
|
if *chainMode {
|
|
tools = agent.GortexChainTools(backend, scope)
|
|
} else {
|
|
tools = agent.GortexTools(backend, scope)
|
|
}
|
|
|
|
cfg := llm.Config{
|
|
Provider: "local",
|
|
Local: llm.LocalConfig{
|
|
Model: *modelPath,
|
|
Ctx: *nCtx,
|
|
GPULayers: *gpu,
|
|
Template: *tmplName,
|
|
},
|
|
}.ApplyDefaults()
|
|
prov, err := provider.New(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "provider: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer prov.Close()
|
|
|
|
ag, err := agent.New(prov, tools)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "agent: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
t0 := time.Now()
|
|
answer, transcript, runErr := ag.Run(context.Background(), systemExtras, *question, *maxSteps)
|
|
|
|
fmt.Println("=== TRANSCRIPT ===")
|
|
for i, st := range transcript {
|
|
switch st.Kind {
|
|
case "call":
|
|
fmt.Printf("[%d] CALL %s\n", i, st.Raw)
|
|
case "result":
|
|
fmt.Printf("[%d] RESULT %s\n", i, st.Raw)
|
|
case "final":
|
|
fmt.Printf("[%d] FINAL %s\n", i, st.Raw)
|
|
}
|
|
}
|
|
fmt.Println()
|
|
if runErr != nil {
|
|
fmt.Fprintf(os.Stderr, "run error: %v\n", runErr)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("=== ANSWER ===")
|
|
fmt.Println(answer)
|
|
fmt.Fprintf(os.Stderr, "\n[elapsed %s]\n", time.Since(t0).Round(time.Millisecond))
|
|
}
|