From a6900e6b5d175db7c953b213f88775c3b8a5cb3f Mon Sep 17 00:00:00 2001 From: grantdfoster Date: Mon, 3 Nov 2025 18:40:00 +0100 Subject: [PATCH 01/21] feat: web scraping v1 --- agent/agent.go | 153 ++++++++++++++++++++++++++++++++++---------- agent/agent_test.go | 13 ++-- agent/twitter.go | 4 +- agent/web.go | 86 +++++++++++++++++++++++++ go.mod | 11 ++-- go.sum | 15 +++++ 6 files changed, 234 insertions(+), 48 deletions(-) create mode 100644 agent/web.go diff --git a/agent/agent.go b/agent/agent.go index f90e51f..7aa5b0e 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -15,8 +15,8 @@ import ( ) type Agent struct { - llm cogito.LLM - c *client.Client + llm cogito.LLM + Client *client.Client } var ( @@ -29,13 +29,83 @@ const ( DefaultPromptSuffix = "If no date range is specified, search the last 7 days." ) +// QueryOptions configures the behavior of the Query method +type QueryOptions struct { + Schema jsonschema.Definition + Instructions string + PromptSuffix string + FinalPrompt string +} + +// QueryOption modifies QueryOptions +type QueryOption func(*QueryOptions) + +// WithSchema sets a custom schema for structured extraction +func WithSchema(schema jsonschema.Definition) QueryOption { + return func(opts *QueryOptions) { + opts.Schema = schema + } +} + +// WithInstructions sets custom query instructions +func WithInstructions(instructions string) QueryOption { + return func(opts *QueryOptions) { + opts.Instructions = instructions + } +} + +// WithPromptSuffix sets a custom prompt suffix +func WithPromptSuffix(suffix string) QueryOption { + return func(opts *QueryOptions) { + opts.PromptSuffix = suffix + } +} + +// WithFinalPrompt sets a custom final prompt instruction +func WithFinalPrompt(prompt string) QueryOption { + return func(opts *QueryOptions) { + opts.FinalPrompt = prompt + } +} + +// DefaultSchema returns the default schema for topic sentiment analysis +func DefaultSchema() jsonschema.Definition { + return jsonschema.Definition{ + Type: jsonschema.Object, + AdditionalProperties: false, + Properties: map[string]jsonschema.Definition{ + "topics": { + Type: jsonschema.Array, + Description: "Trending topics with sentiment and influencers", + Items: &jsonschema.Definition{ + Type: jsonschema.Object, + AdditionalProperties: false, + Properties: map[string]jsonschema.Definition{ + "topic": {Type: jsonschema.String, Description: "Topic"}, + "reasoning": {Type: jsonschema.String, Description: "Reasoning about the topic"}, + "sentiment": {Type: jsonschema.String, Description: "bullish, bearish, or neutral"}, + "top_influencers": {Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.String}}, + }, + Required: []string{"topic", "sentiment", "top_influencers"}, + }, + }, + }, + Required: []string{"topics"}, + } +} + +// DefaultFinalPrompt returns the default final prompt instruction +func DefaultFinalPrompt() string { + return "Return now only a JSON object with fields: topics (ordered by most relevant, each with topic, sentiment as bullish/bearish/neutral, and top_influencers array)." +} + // New creates a new Agent with the provided OpenAI token and model. Model defaults to gpt-5-nano. func New(c *client.Client, openAIToken string) (*Agent, error) { if openAIToken == "" { return nil, ErrOpenAITokenRequired } llm := cogito.NewOpenAILLM(DefaultModel, openAIToken, DefaultOpenAIApiUrl) - return &Agent{llm: llm, c: c}, nil + return &Agent{llm: llm, Client: c}, nil } // NewFromConfig creates a new Agent from config, defaulting the model to gpt-5-nano. @@ -47,11 +117,49 @@ func NewFromConfig(c *client.Client) (*Agent, error) { return New(c, cfg.OpenAIToken) } +// NewAgentFromConfig creates an Agent from config in a single call. +// This convenience function creates both the underlying Client and Agent automatically, +// eliminating the need to manually create a client first. +func NewAgentFromConfig() (*Agent, error) { + cfg, err := config.LoadConfig() + if err != nil { + return nil, err + } + c, err := client.NewClientFromConfig() + if err != nil { + return nil, err + } + ag, err := New(c, cfg.OpenAIToken) + if err != nil { + return nil, err + } + return ag, nil +} + // Query runs the agent with the provided natural language instruction. -// It uses Cogito with the TwitterSearch tool and extracts a structured Output. -func (a *Agent) Query(ctx context.Context, query string) (*types.Output, error) { - // Include instruction supplement: operators/help and default date guidance - fullPrompt := query + "\n\n" + TwitterQueryInstructions + "\n\n" + DefaultPromptSuffix +// It uses Cogito with the TwitterSearch and WebSearch tools and extracts a structured Output. +// Query options can be provided to customize schema, instructions, and prompts. +func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (*types.Output, error) { + // Apply options with defaults + options := &QueryOptions{ + Schema: DefaultSchema(), + Instructions: TwitterQueryInstructions, + PromptSuffix: DefaultPromptSuffix, + FinalPrompt: DefaultFinalPrompt(), + } + + for _, opt := range opts { + opt(options) + } + + // Build full prompt with query, instructions, and suffix + fullPrompt := query + if options.Instructions != "" { + fullPrompt += "\n\n" + options.Instructions + } + if options.PromptSuffix != "" { + fullPrompt += "\n\n" + options.PromptSuffix + } fragment := cogito.NewEmptyFragment(). AddMessage("user", fullPrompt) @@ -64,45 +172,22 @@ func (a *Agent) Query(ctx context.Context, query string) (*types.Output, error) // cogito.EnableToolReasoner, cogito.WithIterations(1), cogito.WithMaxAttempts(1), - cogito.WithTools(&TwitterSearch{Client: a.c}), + cogito.WithTools(&TwitterSearch{Client: a.Client}, &WebSearch{Client: a.Client}), ) if err != nil { return nil, err } - // Ask the model to return only JSON for topics with sentiments and influencers - result, err := a.llm.Ask(ctx, improved.AddMessage("user", "Return now only a JSON object with fields: topics (ordered by most relevant, each with topic, sentiment as bullish/bearish/neutral, and top_influencers array).")) + // Ask the model to return structured JSON according to the final prompt + result, err := a.llm.Ask(ctx, improved.AddMessage("user", options.FinalPrompt)) if err != nil { return nil, err } out := &types.Output{} - // Define schema for structured extraction - schema := jsonschema.Definition{ - Type: jsonschema.Object, - AdditionalProperties: false, - Properties: map[string]jsonschema.Definition{ - "topics": { - Type: jsonschema.Array, - Description: "Trending topics with sentiment and influencers", - Items: &jsonschema.Definition{ - Type: jsonschema.Object, - AdditionalProperties: false, - Properties: map[string]jsonschema.Definition{ - "topic": {Type: jsonschema.String}, - "sentiment": {Type: jsonschema.String, Description: "bullish, bearish, or neutral"}, - "top_influencers": {Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.String}}, - }, - Required: []string{"topic", "sentiment", "top_influencers"}, - }, - }, - }, - Required: []string{"topics"}, - } - s := structures.Structure{ - Schema: schema, + Schema: options.Schema, Object: out, } diff --git a/agent/agent_test.go b/agent/agent_test.go index 8d12371..8c7fd74 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -7,7 +7,6 @@ import ( "time" "github.com/gopher-lab/gopher-client/agent" - "github.com/gopher-lab/gopher-client/client" "github.com/gopher-lab/gopher-client/config" . "github.com/onsi/ginkgo/v2" @@ -20,26 +19,26 @@ func prettyPrint(v any) string { } var _ = Describe("Agent integration", func() { - It("creates client+agent from config and answers a query", func() { + It("creates client+agent from config and performs sentiment analysis on Bitcoin and Ethereum", func() { cfg := config.MustLoadConfig() if cfg.OpenAIToken == "" { Skip("OPENAI_TOKEN not set; skipping agent integration test") } - c, err := client.NewClientFromConfig() - Expect(err).ToNot(HaveOccurred()) - - ag, err := agent.NewFromConfig(c) + ag, err := agent.NewAgentFromConfig() Expect(err).ToNot(HaveOccurred()) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - query := "You have to find out a list of the most trending topics related to crypto and altcoins worldwide on Twitter for the last 3 days. Use the search_twitter tool." + query := "use search_twitter and search_web tools to understand current sentiment on the following coins: bitcoin, ethereum. use farside.co.uk/btc/ website to scrape web data for bitcoin, and farside.co.uk/eth/ website to scrape web data for ethereum. focus on tweets / influencers with a big following or likes / views on their tweets." out, err := ag.Query(ctx, query) Expect(err).ToNot(HaveOccurred()) Expect(out).ToNot(BeNil()) + // Verify we got multiple topics (at least Bitcoin and Ethereum) + Expect(out.Topics).ToNot(BeEmpty(), "Expected at least one topic in the output") + fmt.Println(prettyPrint(out)) }) }) diff --git a/agent/twitter.go b/agent/twitter.go index 7e4b597..9b195f8 100644 --- a/agent/twitter.go +++ b/agent/twitter.go @@ -77,9 +77,9 @@ func (t *TwitterSearch) Tool() openai.Tool { } } -// Run executes the tool. Signature follows Cogito's Tool interface expectations. +// Execute executes the tool. Signature follows Cogito's ToolDefinitionInterface expectations. // Expects params to include either {"query": "..."} or raw query under a heuristic. -func (t *TwitterSearch) Run(params map[string]any) (string, error) { +func (t *TwitterSearch) Execute(params map[string]any) (string, error) { var query string if q, ok := params["query"].(string); ok { query = q diff --git a/agent/web.go b/agent/web.go new file mode 100644 index 0000000..f5c4d70 --- /dev/null +++ b/agent/web.go @@ -0,0 +1,86 @@ +package agent + +import ( + "encoding/json" + + "github.com/gopher-lab/gopher-client/client" + "github.com/masa-finance/tee-worker/v2/api/args/web" + openai "github.com/sashabaranov/go-openai" + "github.com/sashabaranov/go-openai/jsonschema" +) + +// WebSearch is a Cogito tool that bridges to the client's SearchTwitterWithArgs +type WebSearch struct { + Client *client.Client +} + +func (t *WebSearch) Name() string { + return "search_web" +} + +func (t *WebSearch) Description() string { + return "Web search using the provided url" +} + +// Tool describes the tool for the underlying LLM provider (OpenAI-compatible) +func (t *WebSearch) Tool() openai.Tool { + return openai.Tool{ + Type: openai.ToolTypeFunction, + Function: &openai.FunctionDefinition{ + Name: t.Name(), + Description: t.Description(), + Parameters: jsonschema.Definition{ + Type: jsonschema.Object, + Properties: map[string]jsonschema.Definition{ + "url": {Type: jsonschema.String, Description: "Web scrape url"}, + }, + Required: []string{"url"}, + }, + }, + } +} + +// Execute executes the tool. Signature follows Cogito's ToolDefinitionInterface expectations. +// Expects params to include either {"url": "..."} or raw url under a heuristic. +func (t *WebSearch) Execute(params map[string]any) (string, error) { + var url string + if q, ok := params["url"].(string); ok { + url = q + } else if q, ok := params["url"].(string); ok { + url = q + } else { + b, _ := json.Marshal(params) + url = string(b) + } + + args := web.NewScraperArguments() + args.URL = url + + docs, err := t.Client.ScrapeWebWithArgs(args) + if err != nil { + return "", err + } + + // Extract both content and metadata.markdown from documents + type docResult struct { + Content string `json:"content"` + Markdown string `json:"markdown,omitempty"` + } + + results := make([]docResult, 0, len(docs)) + for _, d := range docs { + markdown := "" + if d.Metadata != nil { + if md, ok := d.Metadata["markdown"].(string); ok { + markdown = md + } + } + results = append(results, docResult{ + Content: d.Content, // llm summary + Markdown: markdown, + }) + } + + b, _ := json.Marshal(results) + return string(b), nil +} diff --git a/go.mod b/go.mod index 55cc413..2e7f229 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,14 @@ require ( github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/masa-finance/tee-worker/v2 v2.0.1 - github.com/mudler/cogito v0.3.3 + github.com/mudler/cogito v0.5.0 github.com/onsi/ginkgo/v2 v2.26.0 github.com/onsi/gomega v1.38.2 github.com/sashabaranov/go-openai v1.41.2 ) require ( - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect @@ -26,10 +26,10 @@ require ( github.com/huandu/xstrings v1.5.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/modelcontextprotocol/go-sdk v1.0.0 // indirect + github.com/modelcontextprotocol/go-sdk v1.1.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/spf13/cast v1.7.0 // indirect - github.com/tmc/langchaingo v0.1.13 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/tmc/langchaingo v0.1.14 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect @@ -37,6 +37,7 @@ require ( golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.28.0 // indirect golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect diff --git a/go.sum b/go.sum index 3b821fd..51b7042 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= @@ -107,12 +111,17 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74= github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs= +github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA= +github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mudler/cogito v0.3.3 h1:hs69DsNdbXHeB0heZhJpqi4JWjj4uW7kRmjhTw5KwUc= github.com/mudler/cogito v0.3.3/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= +github.com/mudler/cogito v0.5.0 h1:A2zAj5PY9rriRfczM+bnSwe2qAEvJSh9eK9hNUPx++E= +github.com/mudler/cogito v0.5.0/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw= github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE= github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= @@ -141,6 +150,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= @@ -159,6 +170,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= +github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc= +github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= @@ -185,6 +198,8 @@ golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= From 74b85ad1083afd27abaaf8d5285eecd2afaa569e Mon Sep 17 00:00:00 2001 From: grantdfoster Date: Mon, 3 Nov 2025 18:43:12 +0100 Subject: [PATCH 02/21] fix: agent to latest cogito sig --- agent/agent.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 7aa5b0e..ff05875 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -164,12 +164,11 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* fragment := cogito.NewEmptyFragment(). AddMessage("user", fullPrompt) - improved, err := cogito.ContentReview( + // Execute tools with the LLM + improved, err := cogito.ExecuteTools( a.llm, fragment, - // cogito.EnableDeepContext, - // cogito.EnableToolReEvaluator, - // cogito.EnableToolReasoner, + cogito.WithContext(ctx), cogito.WithIterations(1), cogito.WithMaxAttempts(1), cogito.WithTools(&TwitterSearch{Client: a.Client}, &WebSearch{Client: a.Client}), From 27282d0fd8454d69f7d532021f8f07ffd9d4bb54 Mon Sep 17 00:00:00 2001 From: grantdfoster Date: Mon, 3 Nov 2025 18:44:16 +0100 Subject: [PATCH 03/21] fix: tool selection --- agent/agent.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index ff05875..758a280 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -153,7 +153,8 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* } // Build full prompt with query, instructions, and suffix - fullPrompt := query + // Make it explicit that tools MUST be used + fullPrompt := query + "\n\nIMPORTANT: You MUST use the available tools (search_twitter and search_web) to gather data. Do not provide an answer without using these tools first." if options.Instructions != "" { fullPrompt += "\n\n" + options.Instructions } @@ -169,8 +170,9 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* a.llm, fragment, cogito.WithContext(ctx), - cogito.WithIterations(1), - cogito.WithMaxAttempts(1), + cogito.WithIterations(3), // Increase iterations to allow multiple tool calls + cogito.WithMaxAttempts(3), // Allow multiple attempts + cogito.WithForceReasoning(), // Force LLM to reason about tool usage cogito.WithTools(&TwitterSearch{Client: a.Client}, &WebSearch{Client: a.Client}), ) if err != nil { From a0f07a3f3a2bcc07a23fef33eaeaffd605198368 Mon Sep 17 00:00:00 2001 From: grantdfoster Date: Mon, 3 Nov 2025 18:55:16 +0100 Subject: [PATCH 04/21] fix: adds updated cogito --- agent/agent.go | 4 ++-- go.mod | 2 ++ go.sum | 57 +++++++++++++++++++------------------------------- 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 758a280..cc9c87a 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -170,8 +170,8 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* a.llm, fragment, cogito.WithContext(ctx), - cogito.WithIterations(3), // Increase iterations to allow multiple tool calls - cogito.WithMaxAttempts(3), // Allow multiple attempts + cogito.WithIterations(3), // Allow multiple tool calls in sequence + cogito.WithMaxAttempts(3), // Allow multiple attempts for tool selection cogito.WithForceReasoning(), // Force LLM to reason about tool usage cogito.WithTools(&TwitterSearch{Client: a.Client}, &WebSearch{Client: a.Client}), ) diff --git a/go.mod b/go.mod index 2e7f229..fa6ca8f 100644 --- a/go.mod +++ b/go.mod @@ -43,3 +43,5 @@ require ( golang.org/x/text v0.30.0 // indirect golang.org/x/tools v0.37.0 // indirect ) + +replace github.com/mudler/cogito => github.com/grantdfoster/cogito v0.5.1-0.20251103175146-d2e5d548359f diff --git a/go.sum b/go.sum index 51b7042..203f77b 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,9 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= @@ -15,8 +11,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -27,8 +23,8 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= @@ -53,8 +49,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -69,6 +65,8 @@ github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grantdfoster/cogito v0.5.1-0.20251103175146-d2e5d548359f h1:Nh6j7RbbKM+FNP8zHePBS1CH0iGb6Zf2BGRseZJ3mfA= +github.com/grantdfoster/cogito v0.5.1-0.20251103175146-d2e5d548359f/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -83,8 +81,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= @@ -109,19 +107,12 @@ github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74= -github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA= github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mudler/cogito v0.3.3 h1:hs69DsNdbXHeB0heZhJpqi4JWjj4uW7kRmjhTw5KwUc= -github.com/mudler/cogito v0.3.3/go.mod h1:abMwl+CUjCp87IufA2quZdZt0bbLaHHN79o17HbUKxU= -github.com/mudler/cogito v0.5.0 h1:A2zAj5PY9rriRfczM+bnSwe2qAEvJSh9eK9hNUPx++E= -github.com/mudler/cogito v0.5.0/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw= github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE= github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= @@ -132,10 +123,10 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -148,8 +139,6 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -164,12 +153,10 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= -github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc= github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= @@ -178,8 +165,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= From e36a107d9fec636ff8916350f0a54bedf55b7cdc Mon Sep 17 00:00:00 2001 From: grantdfoster Date: Tue, 4 Nov 2025 19:32:02 +0100 Subject: [PATCH 05/21] feat: refactors agent functionality with default options, improves error handling --- agent/agent.go | 44 ++++++++++++------- agent/agent_test.go | 9 ++-- agent/assets.go | 7 +++ agent/errors.go | 20 +++++++++ agent/kols.go | 104 ++++++++++++++++++++++++++++++++++++++++++++ agent/twitter.go | 43 ++++++------------ agent/web.go | 34 +++++---------- agent/websites.go | 11 +++++ go.mod | 3 +- go.sum | 2 - types/agent.go | 22 ++++------ 11 files changed, 208 insertions(+), 91 deletions(-) create mode 100644 agent/assets.go create mode 100644 agent/errors.go create mode 100644 agent/kols.go create mode 100644 agent/websites.go diff --git a/agent/agent.go b/agent/agent.go index cc9c87a..0847d8a 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -3,6 +3,7 @@ package agent import ( "context" "errors" + "fmt" "time" "github.com/mudler/cogito" @@ -74,29 +75,28 @@ func DefaultSchema() jsonschema.Definition { Type: jsonschema.Object, AdditionalProperties: false, Properties: map[string]jsonschema.Definition{ - "topics": { + "assets": { Type: jsonschema.Array, - Description: "Trending topics with sentiment and influencers", + Description: "Track the market sentiment of assets, such as Bitcoin, Ethereum, and other cryptocurrencies.", Items: &jsonschema.Definition{ Type: jsonschema.Object, AdditionalProperties: false, Properties: map[string]jsonschema.Definition{ - "topic": {Type: jsonschema.String, Description: "Topic"}, - "reasoning": {Type: jsonschema.String, Description: "Reasoning about the topic"}, - "sentiment": {Type: jsonschema.String, Description: "bullish, bearish, or neutral"}, - "top_influencers": {Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.String}}, + "asset": {Type: jsonschema.String, Description: "Asset name"}, + "reasoning": {Type: jsonschema.String, Description: "Brief reasoning about the sentiment of the asset"}, + "sentiment": {Type: jsonschema.Integer, Description: "Numeric sentiment score from 1-100, where 100 is the most bullish and 1 is the most bearish"}, }, - Required: []string{"topic", "sentiment", "top_influencers"}, + Required: []string{"asset", "reasoning", "sentiment"}, }, }, }, - Required: []string{"topics"}, + Required: []string{"assets"}, } } // DefaultFinalPrompt returns the default final prompt instruction func DefaultFinalPrompt() string { - return "Return now only a JSON object with fields: topics (ordered by most relevant, each with topic, sentiment as bullish/bearish/neutral, and top_influencers array)." + return "Return now only a JSON object with fields that match the supplied schema." } // New creates a new Agent with the provided OpenAI token and model. Model defaults to gpt-5-nano. @@ -153,8 +153,7 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* } // Build full prompt with query, instructions, and suffix - // Make it explicit that tools MUST be used - fullPrompt := query + "\n\nIMPORTANT: You MUST use the available tools (search_twitter and search_web) to gather data. Do not provide an answer without using these tools first." + fullPrompt := query if options.Instructions != "" { fullPrompt += "\n\n" + options.Instructions } @@ -170,10 +169,10 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* a.llm, fragment, cogito.WithContext(ctx), - cogito.WithIterations(3), // Allow multiple tool calls in sequence - cogito.WithMaxAttempts(3), // Allow multiple attempts for tool selection + cogito.WithIterations(2), // Allow multiple tool calls in sequence + cogito.WithMaxAttempts(2), // Allow multiple attempts for tool selection cogito.WithForceReasoning(), // Force LLM to reason about tool usage - cogito.WithTools(&TwitterSearch{Client: a.Client}, &WebSearch{Client: a.Client}), + cogito.WithTools(&WebSearch{Client: a.Client}, &TwitterSearch{Client: a.Client}), ) if err != nil { return nil, err @@ -185,6 +184,11 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* return nil, err } + // Log the raw LLM response for debugging + if lastMsg := result.LastMessage(); lastMsg != nil { + fmt.Printf("DEBUG: LLM response before extraction: %s\n", lastMsg.Content) + } + out := &types.Output{} s := structures.Structure{ @@ -197,8 +201,16 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* defer cancel() if err := result.ExtractStructure(ctxExtract, a.llm, s); err != nil { - // If extraction fails, still return empty Output to avoid nil - // no-op + // If extraction fails, still return what we have (might be partial data) + // Log the error but don't fail completely - we might have some data + fmt.Printf("DEBUG: Extraction error (but returning partial results): %v\n", err) + // Continue to return out even if extraction failed - it might have partial data + } + + // Check if we got any data + if len(out.Assets) == 0 { + // If no topics extracted, log this for debugging + fmt.Printf("DEBUG: No topics extracted. Tools called: %d\n", len(improved.Status.ToolsCalled)) } return out, nil diff --git a/agent/agent_test.go b/agent/agent_test.go index 8c7fd74..023bb70 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" "github.com/gopher-lab/gopher-client/agent" @@ -19,7 +20,7 @@ func prettyPrint(v any) string { } var _ = Describe("Agent integration", func() { - It("creates client+agent from config and performs sentiment analysis on Bitcoin and Ethereum", func() { + It("creates Agent from config and performs sentiment analysis on assets", func() { cfg := config.MustLoadConfig() if cfg.OpenAIToken == "" { Skip("OPENAI_TOKEN not set; skipping agent integration test") @@ -28,16 +29,16 @@ var _ = Describe("Agent integration", func() { ag, err := agent.NewAgentFromConfig() Expect(err).ToNot(HaveOccurred()) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) defer cancel() - query := "use search_twitter and search_web tools to understand current sentiment on the following coins: bitcoin, ethereum. use farside.co.uk/btc/ website to scrape web data for bitcoin, and farside.co.uk/eth/ website to scrape web data for ethereum. focus on tweets / influencers with a big following or likes / views on their tweets." + query := fmt.Sprintf("use all available tools to determine the market sentiment of the following assets: %s, using the following websites: %s, and the following twitter accounts: %s", strings.Join(agent.Assets, ", "), strings.Join(agent.Websites, ", "), strings.Join(agent.Kols, ", ")) out, err := ag.Query(ctx, query) Expect(err).ToNot(HaveOccurred()) Expect(out).ToNot(BeNil()) // Verify we got multiple topics (at least Bitcoin and Ethereum) - Expect(out.Topics).ToNot(BeEmpty(), "Expected at least one topic in the output") + Expect(out.Assets).ToNot(BeEmpty(), "Expected at least one asset in the output") fmt.Println(prettyPrint(out)) }) diff --git a/agent/assets.go b/agent/assets.go new file mode 100644 index 0000000..67395c1 --- /dev/null +++ b/agent/assets.go @@ -0,0 +1,7 @@ +package agent + +var Assets = []string{ + "bitcoin", + "ethereum", + "solana", +} diff --git a/agent/errors.go b/agent/errors.go new file mode 100644 index 0000000..f8f8ad3 --- /dev/null +++ b/agent/errors.go @@ -0,0 +1,20 @@ +package agent + +import ( + "strings" +) + +// isTimeoutError checks if an error is a timeout error by examining the error message +// This helps identify timeout errors from WaitForJobCompletion and other timeout scenarios +func isTimeoutError(err error) bool { + if err == nil { + return false + } + errStr := err.Error() + // Check for common timeout error patterns + return strings.Contains(errStr, "timed out") || + strings.Contains(errStr, "timeout") || + strings.Contains(errStr, "context deadline exceeded") || + strings.Contains(errStr, "deadline exceeded") +} + diff --git a/agent/kols.go b/agent/kols.go new file mode 100644 index 0000000..bc774ed --- /dev/null +++ b/agent/kols.go @@ -0,0 +1,104 @@ +package agent + +var Kols = []string{ + "JamesWynnReal", + "buzzdefi0x", + "crypto_birb", + "cryptojack", + "cryptomanran", + "IvanOnTech", + "TeddyCleps", + "BigCheds", + "Nebraskangooner", + "Ashcryptoreal", + "CredibleCrypto", + "_ColesTrading", + "Livercoin", + "CastilloTrading", + "legen_eth", + "DaanCrypto", + "kyle_chasse", + "MikybullCrypto", + "CryptoTony__", + "Crypto_Chase", + "martypartymusic", + "eliz883", + "Trader_XO", + "SatoshiFlipper", + "CryptoTea_", + "CryptoMichNL", + "scottmelker", + "PeterLBrandt", + "CryptoDonAlt", + "TheCryptoDog", + "CryptoKaleo", + "woonomic", + "100trillionUSD", + "RaoulGMI", + "APompliano", + "CryptoWendyO", + "iamjosephyoung", + "intocryptoverse", + "elliotrades", + "WClementeIII", + "CryptoHayes", + "PaikCapital", + "Bitdealer_", + "_Checkmatey_", + "AltcoinGordon", + "MadelonVos__", + "TheCryptoLark", + "AltcoinDaily", + "lookonchain", + "cobie", + "GoingParabolic", + "LynAldenContact", + "CathieDWood", + "TuurDemeester", + "ercwl", + "AriDavidPaul", + "cburniske", + "hasufl", + "scupytrooples", + "Arthur_0x", + "RyanWatkins_", + "PeterMcCormack", + "danheld", + "tatianakoffman", + "notsofast", + "saylor", + "VitalikButerin", + "ethereumJoseph", + "cz_binance", + "adam3us", + "CryptoJack", + "maxkeiser", + "novogratz", + "jackmallers", + "Excellion", + "brian_armstrong", + "justinsuntron", + "CarlRunefelt", + "LukeBelmar", + "viveksen", + "MustStopMurad", + "JackTheRippler", + "naiive", + "Gordon", + "lynk", + "mattwalker", + "SergeyNazarov", + "paoloardoino", + "fundstrat", + "anatolyakovenko", + "jerallaire", + "CaitlinLong_", + "starkness", + "robin_linus", + "_RichardTeng", + "ASvanevik", + "Melt_Dem", + "nic__carter", + "naval", + "laurashin", +} diff --git a/agent/twitter.go b/agent/twitter.go index 9b195f8..740e0ec 100644 --- a/agent/twitter.go +++ b/agent/twitter.go @@ -2,9 +2,10 @@ package agent import ( "encoding/json" + "fmt" "github.com/gopher-lab/gopher-client/client" - "github.com/masa-finance/tee-worker/v2/api/args/twitter" + "github.com/masa-finance/tee-worker/v2/api/args/twitter" openai "github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai/jsonschema" ) @@ -18,32 +19,15 @@ love OR hate containing either "love" or "hate" (or both). beer -root containing "beer" but not "root". #haiku containing the hashtag "haiku". from:interior sent from Twitter account "interior". -list:NASA/astronauts-in-space-now sent from a Twitter account in the NASA list astronauts-in-space-now to:NASA a Tweet authored in reply to Twitter account "NASA". @NASA mentioning Twitter account "NASA". -puppy filter:media containing "puppy" and an image or video. -puppy -filter:retweets containing "puppy", filtering out retweets -puppy filter:native_video containing "puppy" and an uploaded video, Amplify video, Periscope, or Vine. -puppy filter:periscope containing "puppy" and a Periscope video URL. -puppy filter:vine containing "puppy" and a Vine. -puppy filter:images containing "puppy" and links identified as photos, including third parties such as Instagram. -puppy filter:twimg containing "puppy" and a pic.twitter.com link representing one or more photos. -hilarious filter:links containing "hilarious" and linking to URL. -puppy url:amazon containing "puppy" and a URL with the word "amazon" anywhere within it. superhero since:2015-12-21 containing "superhero" and sent since date "2015-12-21" (year-month-day). puppy until:2015-12-21 containing "puppy" and sent before the date "2015-12-21". -movie -scary :) containing "movie", but not "scary", and with a positive attitude. -flight :( containing "flight" and with a negative attitude. -traffic ? containing "traffic" and asking a question. - -Example: - -altcoin or bitcoin :) To search for the same day, you must subtract a day between since and until: altcoin or bitcoin :) since:2025-03-23 until:2025-03-24 -If no date range is specified, default to the last 7 days. +If no date range is specified, default to the last 1 day. ` // TwitterSearch is a Cogito tool that bridges to the client's SearchTwitterWithArgs @@ -56,7 +40,7 @@ func (t *TwitterSearch) Name() string { } func (t *TwitterSearch) Description() string { - return "Search Twitter using the provided query. Include operators, since/until. Defaults to last 7 days if none provided." + return "Search Twitter using the provided query. Include operators, since/until. Defaults to last 1 day if none provided." } // Tool describes the tool for the underlying LLM provider (OpenAI-compatible) @@ -78,13 +62,11 @@ func (t *TwitterSearch) Tool() openai.Tool { } // Execute executes the tool. Signature follows Cogito's ToolDefinitionInterface expectations. -// Expects params to include either {"query": "..."} or raw query under a heuristic. +// Expects params to include {"query": "..."} per the schema definition. func (t *TwitterSearch) Execute(params map[string]any) (string, error) { var query string if q, ok := params["query"].(string); ok { query = q - } else if q, ok := params["input"].(string); ok { - query = q } else { b, _ := json.Marshal(params) query = string(b) @@ -95,15 +77,16 @@ func (t *TwitterSearch) Execute(params map[string]any) (string, error) { docs, err := t.Client.SearchTwitterWithArgs(args) if err != nil { + // If this is a timeout error, return a user-friendly message that the framework can use + // The framework will convert this to a result string and continue execution + if isTimeoutError(err) { + return "", fmt.Errorf("twitter search timed out for query %s: %v", query, err) + } return "", err } - // Extract only the content field from documents - contents := make([]string, 0, len(docs)) - for _, d := range docs { - contents = append(contents, d.Content) - } - - b, _ := json.Marshal(contents) + // Return full documents - lean structure with useful metadata (username, created_at, likes, etc.) + // Embedding and Score are omitempty so won't be serialized + b, _ := json.Marshal(docs) return string(b), nil } diff --git a/agent/web.go b/agent/web.go index f5c4d70..ad59f65 100644 --- a/agent/web.go +++ b/agent/web.go @@ -2,6 +2,7 @@ package agent import ( "encoding/json" + "fmt" "github.com/gopher-lab/gopher-client/client" "github.com/masa-finance/tee-worker/v2/api/args/web" @@ -41,13 +42,11 @@ func (t *WebSearch) Tool() openai.Tool { } // Execute executes the tool. Signature follows Cogito's ToolDefinitionInterface expectations. -// Expects params to include either {"url": "..."} or raw url under a heuristic. +// Expects params to include {"url": "..."} per the schema definition. func (t *WebSearch) Execute(params map[string]any) (string, error) { var url string if q, ok := params["url"].(string); ok { url = q - } else if q, ok := params["url"].(string); ok { - url = q } else { b, _ := json.Marshal(params) url = string(b) @@ -58,29 +57,16 @@ func (t *WebSearch) Execute(params map[string]any) (string, error) { docs, err := t.Client.ScrapeWebWithArgs(args) if err != nil { - return "", err - } - - // Extract both content and metadata.markdown from documents - type docResult struct { - Content string `json:"content"` - Markdown string `json:"markdown,omitempty"` - } - - results := make([]docResult, 0, len(docs)) - for _, d := range docs { - markdown := "" - if d.Metadata != nil { - if md, ok := d.Metadata["markdown"].(string); ok { - markdown = md - } + // If this is a timeout error, return a user-friendly message that the framework can use + // The framework will convert this to a result string and continue execution + if isTimeoutError(err) { + return "", fmt.Errorf("web search timed out for URL %s: %v", url, err) } - results = append(results, docResult{ - Content: d.Content, // llm summary - Markdown: markdown, - }) + return "", err } - b, _ := json.Marshal(results) + // Return full documents - lean structure with useful metadata (title, canonicalUrl, markdown, etc.) + // Embedding and Score are omitempty so won't be serialized + b, _ := json.Marshal(docs) return string(b), nil } diff --git a/agent/websites.go b/agent/websites.go new file mode 100644 index 0000000..c76b2bc --- /dev/null +++ b/agent/websites.go @@ -0,0 +1,11 @@ +package agent + +var Websites = []string{ + "farside.co.uk/btc/", + "farside.co.uk/eth/", + "https://coinmarketcap.com/gainers-losers/", + "https://coin360.com/", + "https://www.cmegroup.com/markets/interest-rates/cme-fedwatch-tool.html", + "https://www.marketwatch.com/economy-politics/calendar", + "https://www.coinglass.com/LiquidationData", +} diff --git a/go.mod b/go.mod index fa6ca8f..ed215f7 100644 --- a/go.mod +++ b/go.mod @@ -44,4 +44,5 @@ require ( golang.org/x/tools v0.37.0 // indirect ) -replace github.com/mudler/cogito => github.com/grantdfoster/cogito v0.5.1-0.20251103175146-d2e5d548359f +// FIXME: update to latest release +replace github.com/mudler/cogito => ../cogito diff --git a/go.sum b/go.sum index 203f77b..e3d588e 100644 --- a/go.sum +++ b/go.sum @@ -65,8 +65,6 @@ github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grantdfoster/cogito v0.5.1-0.20251103175146-d2e5d548359f h1:Nh6j7RbbKM+FNP8zHePBS1CH0iGb6Zf2BGRseZJ3mfA= -github.com/grantdfoster/cogito v0.5.1-0.20251103175146-d2e5d548359f/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= diff --git a/types/agent.go b/types/agent.go index 55a047e..7f7e686 100644 --- a/types/agent.go +++ b/types/agent.go @@ -4,25 +4,19 @@ import ( "github.com/masa-finance/tee-worker/v2/api/types" ) -type Sentiment string -type TopInfluencers []string -type Topic string - -const ( - SentimentBullish Sentiment = "bullish" - SentimentBearish Sentiment = "bearish" - SentimentNeutral Sentiment = "neutral" -) +type Sentiment uint +type Reasoning string +type Asset string // TopicSummary represents a topic with sentiment and influencers discovered by the agent -type TopicSummary struct { - Topic Topic `json:"topic"` - Sentiment Sentiment `json:"sentiment"` - TopInfluencers TopInfluencers `json:"top_influencers"` +type AssetSummary struct { + Asset Asset `json:"asset"` + Reasoning Reasoning `json:"reasoning"` + Sentiment Sentiment `json:"sentiment"` } // Output is the agent's structured output type Output struct { - Topics []TopicSummary `json:"topics"` + Assets []AssetSummary `json:"assets"` Documents []types.Document `json:"documents,omitempty"` } From 083f60e81fa31c64a1262745405cd23c47a3e38a Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Tue, 4 Nov 2025 12:28:25 -0800 Subject: [PATCH 06/21] feat: improved fetching with 3 assets --- agent/agent.go | 43 ++++++++++++++++++++++--------------------- agent/agent_test.go | 6 ++++-- agent/errors.go | 20 -------------------- agent/twitter.go | 30 +++++++++++++++++++++--------- agent/web.go | 17 ++++++++++------- agent/websites.go | 15 ++++++++------- 6 files changed, 65 insertions(+), 66 deletions(-) delete mode 100644 agent/errors.go diff --git a/agent/agent.go b/agent/agent.go index 0847d8a..38af75e 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -3,7 +3,6 @@ package agent import ( "context" "errors" - "fmt" "time" "github.com/mudler/cogito" @@ -12,6 +11,7 @@ import ( "github.com/gopher-lab/gopher-client/client" "github.com/gopher-lab/gopher-client/config" + "github.com/gopher-lab/gopher-client/log" "github.com/gopher-lab/gopher-client/types" ) @@ -27,7 +27,21 @@ var ( const ( DefaultModel = "gpt-5-nano" DefaultOpenAIApiUrl = "https://api.openai.com/v1" - DefaultPromptSuffix = "If no date range is specified, search the last 7 days." + DefaultPromptSuffix = "If no date range is specified, search the last 1 day" + // DefaultDataCollectionInstructions provides explicit guidance about trying all sources + DefaultDataCollectionInstructions = ` +IMPORTANT: You must attempt to gather data from ALL available sources, even if some fail. +- Try ALL websites provided, even if some URLs timeout or return errors +- Execute Twitter searches for sentiment analysis, but IMPORTANT: Randomly sample Twitter accounts and query ONLY 1 account per search + - Randomly select 1 account from the provided list for each search + - DO NOT exhaustively query all accounts - a random sample is sufficient + - Query format: 'from:username (BTC OR Bitcoin OR ETH OR Ethereum OR SOL OR Solana)' - NO SPACE after 'from:' (e.g., 'from:JamesWynnReal', NOT 'from: JamesWynnReal') + - Use hashtags and keywords: '#BTC OR #ETH OR bitcoin OR ethereum' + - Example correct query: 'from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03 until:2025-11-04' +- Continue with remaining sources even if earlier sources fail +- Partial data is acceptable - gather what you can from each source +- Use multiple iterations to systematically collect data from all sources before synthesizing results +` ) // QueryOptions configures the behavior of the Query method @@ -84,7 +98,7 @@ func DefaultSchema() jsonschema.Definition { Properties: map[string]jsonschema.Definition{ "asset": {Type: jsonschema.String, Description: "Asset name"}, "reasoning": {Type: jsonschema.String, Description: "Brief reasoning about the sentiment of the asset"}, - "sentiment": {Type: jsonschema.Integer, Description: "Numeric sentiment score from 1-100, where 100 is the most bullish and 1 is the most bearish"}, + "sentiment": {Type: jsonschema.Integer, Description: "Numeric sentiment score from 0-100. Scale: 0 = most bearish, 50 = neutral, 100 = most bullish. Each asset should have a distinct score based on its unique data and sentiment signals."}, }, Required: []string{"asset", "reasoning", "sentiment"}, }, @@ -96,7 +110,7 @@ func DefaultSchema() jsonschema.Definition { // DefaultFinalPrompt returns the default final prompt instruction func DefaultFinalPrompt() string { - return "Return now only a JSON object with fields that match the supplied schema." + return "Return now only a JSON object with fields that match the supplied schema. IMPORTANT: Each asset must have a distinct sentiment score based on its unique data and signals - do not use the same sentiment value for different assets. Analyze each asset independently." } // New creates a new Agent with the provided OpenAI token and model. Model defaults to gpt-5-nano. @@ -154,6 +168,7 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* // Build full prompt with query, instructions, and suffix fullPrompt := query + fullPrompt += "\n\n" + DefaultDataCollectionInstructions if options.Instructions != "" { fullPrompt += "\n\n" + options.Instructions } @@ -169,8 +184,8 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* a.llm, fragment, cogito.WithContext(ctx), - cogito.WithIterations(2), // Allow multiple tool calls in sequence - cogito.WithMaxAttempts(2), // Allow multiple attempts for tool selection + cogito.WithIterations(3), // Reduced for faster testing (was 10) + cogito.WithMaxAttempts(1), // Allow multiple attempts for tool selection cogito.WithForceReasoning(), // Force LLM to reason about tool usage cogito.WithTools(&WebSearch{Client: a.Client}, &TwitterSearch{Client: a.Client}), ) @@ -184,11 +199,6 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* return nil, err } - // Log the raw LLM response for debugging - if lastMsg := result.LastMessage(); lastMsg != nil { - fmt.Printf("DEBUG: LLM response before extraction: %s\n", lastMsg.Content) - } - out := &types.Output{} s := structures.Structure{ @@ -201,16 +211,7 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* defer cancel() if err := result.ExtractStructure(ctxExtract, a.llm, s); err != nil { - // If extraction fails, still return what we have (might be partial data) - // Log the error but don't fail completely - we might have some data - fmt.Printf("DEBUG: Extraction error (but returning partial results): %v\n", err) - // Continue to return out even if extraction failed - it might have partial data - } - - // Check if we got any data - if len(out.Assets) == 0 { - // If no topics extracted, log this for debugging - fmt.Printf("DEBUG: No topics extracted. Tools called: %d\n", len(improved.Status.ToolsCalled)) + log.Error("Extraction error", err) } return out, nil diff --git a/agent/agent_test.go b/agent/agent_test.go index 023bb70..33c5736 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -29,7 +29,7 @@ var _ = Describe("Agent integration", func() { ag, err := agent.NewAgentFromConfig() Expect(err).ToNot(HaveOccurred()) - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) defer cancel() query := fmt.Sprintf("use all available tools to determine the market sentiment of the following assets: %s, using the following websites: %s, and the following twitter accounts: %s", strings.Join(agent.Assets, ", "), strings.Join(agent.Websites, ", "), strings.Join(agent.Kols, ", ")) @@ -40,6 +40,8 @@ var _ = Describe("Agent integration", func() { // Verify we got multiple topics (at least Bitcoin and Ethereum) Expect(out.Assets).ToNot(BeEmpty(), "Expected at least one asset in the output") - fmt.Println(prettyPrint(out)) + fmt.Println("--------------------------------") + fmt.Println(prettyPrint(out.Assets)) + }) }) diff --git a/agent/errors.go b/agent/errors.go deleted file mode 100644 index f8f8ad3..0000000 --- a/agent/errors.go +++ /dev/null @@ -1,20 +0,0 @@ -package agent - -import ( - "strings" -) - -// isTimeoutError checks if an error is a timeout error by examining the error message -// This helps identify timeout errors from WaitForJobCompletion and other timeout scenarios -func isTimeoutError(err error) bool { - if err == nil { - return false - } - errStr := err.Error() - // Check for common timeout error patterns - return strings.Contains(errStr, "timed out") || - strings.Contains(errStr, "timeout") || - strings.Contains(errStr, "context deadline exceeded") || - strings.Contains(errStr, "deadline exceeded") -} - diff --git a/agent/twitter.go b/agent/twitter.go index 740e0ec..2c098ee 100644 --- a/agent/twitter.go +++ b/agent/twitter.go @@ -2,7 +2,6 @@ package agent import ( "encoding/json" - "fmt" "github.com/gopher-lab/gopher-client/client" "github.com/masa-finance/tee-worker/v2/api/args/twitter" @@ -18,7 +17,7 @@ watching now containing both "watching" and "now". This is the default operator love OR hate containing either "love" or "hate" (or both). beer -root containing "beer" but not "root". #haiku containing the hashtag "haiku". -from:interior sent from Twitter account "interior". +from:interior sent from Twitter account "interior". CRITICAL: NO SPACE after 'from:' (e.g., 'from:username', NOT 'from: username'). to:NASA a Tweet authored in reply to Twitter account "NASA". @NASA mentioning Twitter account "NASA". superhero since:2015-12-21 containing "superhero" and sent since date "2015-12-21" (year-month-day). @@ -28,6 +27,15 @@ To search for the same day, you must subtract a day between since and until: altcoin or bitcoin :) since:2025-03-23 until:2025-03-24 If no date range is specified, default to the last 1 day. + +CORRECT EXAMPLES: +- from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03 until:2025-11-04 +- from:CryptoWendyO #BTC OR #ETH since:2025-11-03 until:2025-11-04 +- from:VitalikButerin (ethereum OR ETH) since:2025-11-03 until:2025-11-04 + +INCORRECT (DO NOT USE SPACES AFTER from:): +- from: JamesWynnReal (WRONG - has space after from:) +- from: CryptoWendyO (WRONG - has space after from:) ` // TwitterSearch is a Cogito tool that bridges to the client's SearchTwitterWithArgs @@ -40,7 +48,7 @@ func (t *TwitterSearch) Name() string { } func (t *TwitterSearch) Description() string { - return "Search Twitter using the provided query. Include operators, since/until. Defaults to last 1 day if none provided." + return "Search Twitter using the provided query. Include operators, since/until. Defaults to last 1 day if none provided. CRITICAL: Query ONLY 1 account per search using format 'from:username' with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal', NOT 'from: JamesWynnReal'). Randomly sample accounts - do not exhaustively query all accounts. Use hashtags and keywords like '#BTC OR #ETH OR bitcoin OR ethereum' to find relevant tweets." } // Tool describes the tool for the underlying LLM provider (OpenAI-compatible) @@ -53,7 +61,7 @@ func (t *TwitterSearch) Tool() openai.Tool { Parameters: jsonschema.Definition{ Type: jsonschema.Object, Properties: map[string]jsonschema.Definition{ - "query": {Type: jsonschema.String, Description: "Twitter advanced search query (with operators, since/until)"}, + "query": {Type: jsonschema.String, Description: "Twitter advanced search query. CRITICAL: Use 'from:username' format with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal (BTC OR Bitcoin)', NOT 'from: JamesWynnReal'). Include operators like since/until, hashtags (#BTC), and keywords."}, }, Required: []string{"query"}, }, @@ -77,12 +85,16 @@ func (t *TwitterSearch) Execute(params map[string]any) (string, error) { docs, err := t.Client.SearchTwitterWithArgs(args) if err != nil { - // If this is a timeout error, return a user-friendly message that the framework can use - // The framework will convert this to a result string and continue execution - if isTimeoutError(err) { - return "", fmt.Errorf("twitter search timed out for query %s: %v", query, err) + // Return error as a structured result string so the LLM can see what happened + // This allows the agent to continue with partial data + errorResult := map[string]any{ + "error": true, + "query": query, + "error_msg": err.Error(), + "documents": []any{}, } - return "", err + b, _ := json.Marshal(errorResult) + return string(b), nil } // Return full documents - lean structure with useful metadata (username, created_at, likes, etc.) diff --git a/agent/web.go b/agent/web.go index ad59f65..1b4f290 100644 --- a/agent/web.go +++ b/agent/web.go @@ -2,7 +2,6 @@ package agent import ( "encoding/json" - "fmt" "github.com/gopher-lab/gopher-client/client" "github.com/masa-finance/tee-worker/v2/api/args/web" @@ -20,7 +19,7 @@ func (t *WebSearch) Name() string { } func (t *WebSearch) Description() string { - return "Web search using the provided url" + return "Web search using the provided url. Call this tool once per URL - execute multiple calls sequentially to fetch multiple URLs." } // Tool describes the tool for the underlying LLM provider (OpenAI-compatible) @@ -57,12 +56,16 @@ func (t *WebSearch) Execute(params map[string]any) (string, error) { docs, err := t.Client.ScrapeWebWithArgs(args) if err != nil { - // If this is a timeout error, return a user-friendly message that the framework can use - // The framework will convert this to a result string and continue execution - if isTimeoutError(err) { - return "", fmt.Errorf("web search timed out for URL %s: %v", url, err) + // Return error as a structured result string so the LLM can see what happened + // This allows the agent to continue with partial data + errorResult := map[string]any{ + "error": true, + "url": url, + "error_msg": err.Error(), + "documents": []any{}, } - return "", err + b, _ := json.Marshal(errorResult) + return string(b), nil } // Return full documents - lean structure with useful metadata (title, canonicalUrl, markdown, etc.) diff --git a/agent/websites.go b/agent/websites.go index c76b2bc..d045d73 100644 --- a/agent/websites.go +++ b/agent/websites.go @@ -1,11 +1,12 @@ package agent var Websites = []string{ - "farside.co.uk/btc/", - "farside.co.uk/eth/", - "https://coinmarketcap.com/gainers-losers/", - "https://coin360.com/", - "https://www.cmegroup.com/markets/interest-rates/cme-fedwatch-tool.html", - "https://www.marketwatch.com/economy-politics/calendar", - "https://www.coinglass.com/LiquidationData", + "https://coinmarketcap.com/gainers-losers", + // "https://farside.co.uk/btc/", + // "https://farside.co.uk/eth/", + // "https://coinmarketcap.com/gainers-losers/", + // "https://coin360.com/", + // "https://www.cmegroup.com/markets/interest-rates/cme-fedwatch-tool.html", + // "https://www.marketwatch.com/economy-politics/calendar", + // "https://www.coinglass.com/LiquidationData", } From a4882be286ea1e208ef7e37fc836c7fb73f29b7a Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Tue, 4 Nov 2025 12:38:34 -0800 Subject: [PATCH 07/21] chore: use grant fork --- agent/agent.go | 2 +- agent/twitter.go | 18 +++++++----------- go.mod | 4 ++-- go.sum | 2 ++ 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 38af75e..af9e276 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -37,7 +37,7 @@ IMPORTANT: You must attempt to gather data from ALL available sources, even if s - DO NOT exhaustively query all accounts - a random sample is sufficient - Query format: 'from:username (BTC OR Bitcoin OR ETH OR Ethereum OR SOL OR Solana)' - NO SPACE after 'from:' (e.g., 'from:JamesWynnReal', NOT 'from: JamesWynnReal') - Use hashtags and keywords: '#BTC OR #ETH OR bitcoin OR ethereum' - - Example correct query: 'from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03 until:2025-11-04' + - Example correct query: 'from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03' - Continue with remaining sources even if earlier sources fail - Partial data is acceptable - gather what you can from each source - Use multiple iterations to systematically collect data from all sources before synthesizing results diff --git a/agent/twitter.go b/agent/twitter.go index 2c098ee..3f8f60c 100644 --- a/agent/twitter.go +++ b/agent/twitter.go @@ -10,7 +10,7 @@ import ( ) // TwitterQueryInstructions contains long-form guidance and examples for constructing Twitter queries. -// If no date range is specified by the user, searches should consider the last 7 days. +// If no date range is specified by the user, searches should consider the last 1 day. const TwitterQueryInstructions = ` watching now containing both "watching" and "now". This is the default operator. "happy hour" containing the exact phrase "happy hour". @@ -21,17 +21,13 @@ from:interior sent from Twitter account "interior". CRITICAL: NO SPACE after 'f to:NASA a Tweet authored in reply to Twitter account "NASA". @NASA mentioning Twitter account "NASA". superhero since:2015-12-21 containing "superhero" and sent since date "2015-12-21" (year-month-day). -puppy until:2015-12-21 containing "puppy" and sent before the date "2015-12-21". -To search for the same day, you must subtract a day between since and until: -altcoin or bitcoin :) since:2025-03-23 until:2025-03-24 - -If no date range is specified, default to the last 1 day. +If no date range is specified, default to the last 1 day (use since:YYYY-MM-DD format, typically one day before today). CORRECT EXAMPLES: -- from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03 until:2025-11-04 -- from:CryptoWendyO #BTC OR #ETH since:2025-11-03 until:2025-11-04 -- from:VitalikButerin (ethereum OR ETH) since:2025-11-03 until:2025-11-04 +- from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03 +- from:CryptoWendyO #BTC OR #ETH since:2025-11-03 +- from:VitalikButerin (ethereum OR ETH) since:2025-11-03 INCORRECT (DO NOT USE SPACES AFTER from:): - from: JamesWynnReal (WRONG - has space after from:) @@ -48,7 +44,7 @@ func (t *TwitterSearch) Name() string { } func (t *TwitterSearch) Description() string { - return "Search Twitter using the provided query. Include operators, since/until. Defaults to last 1 day if none provided. CRITICAL: Query ONLY 1 account per search using format 'from:username' with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal', NOT 'from: JamesWynnReal'). Randomly sample accounts - do not exhaustively query all accounts. Use hashtags and keywords like '#BTC OR #ETH OR bitcoin OR ethereum' to find relevant tweets." + return "Search Twitter using the provided query. Include operators like 'since:YYYY-MM-DD' (typically one day before today). Defaults to last 1 day if none provided. CRITICAL: Query ONLY 1 account per search using format 'from:username' with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal', NOT 'from: JamesWynnReal'). Randomly sample accounts - do not exhaustively query all accounts. Use hashtags and keywords like '#BTC OR #ETH OR bitcoin OR ethereum' to find relevant tweets." } // Tool describes the tool for the underlying LLM provider (OpenAI-compatible) @@ -61,7 +57,7 @@ func (t *TwitterSearch) Tool() openai.Tool { Parameters: jsonschema.Definition{ Type: jsonschema.Object, Properties: map[string]jsonschema.Definition{ - "query": {Type: jsonschema.String, Description: "Twitter advanced search query. CRITICAL: Use 'from:username' format with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal (BTC OR Bitcoin)', NOT 'from: JamesWynnReal'). Include operators like since/until, hashtags (#BTC), and keywords."}, + "query": {Type: jsonschema.String, Description: "Twitter advanced search query. CRITICAL: Use 'from:username' format with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal (BTC OR Bitcoin)', NOT 'from: JamesWynnReal'). Include operators like 'since:YYYY-MM-DD' (typically one day before today), hashtags (#BTC), and keywords."}, }, Required: []string{"query"}, }, diff --git a/go.mod b/go.mod index ed215f7..2f2533a 100644 --- a/go.mod +++ b/go.mod @@ -44,5 +44,5 @@ require ( golang.org/x/tools v0.37.0 // indirect ) -// FIXME: update to latest release -replace github.com/mudler/cogito => ../cogito +// Using specific commit from grantdfoster/cogito fork +replace github.com/mudler/cogito => github.com/grantdfoster/cogito v0.0.0-20251104184711-4b65f4d0640a diff --git a/go.sum b/go.sum index e3d588e..694871f 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grantdfoster/cogito v0.0.0-20251104184711-4b65f4d0640a h1:mVf4CH83q/CoPjOQPvS6PyHsTroOb4v6gVWHNfop+XU= +github.com/grantdfoster/cogito v0.0.0-20251104184711-4b65f4d0640a/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= From abfec110aea989b997b65d07771d35fa395f655c Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Tue, 4 Nov 2025 12:43:17 -0800 Subject: [PATCH 08/21] chore: add secrets --- .github/workflows/tests.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f6d82df..ba4f43d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' - name: Restore cached cookies id: cache-cookies-restore uses: actions/cache/restore@v4 @@ -21,6 +25,11 @@ jobs: cookies key: ${{ runner.os }}-cookies - name: Run tests + env: + OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} + GOPHER_CLIENT_TOKEN: ${{ secrets.GOPHER_CLIENT_TOKEN }} + GOPHER_CLIENT_URL: ${{ secrets.GOPHER_CLIENT_URL }} + GOPHER_CLIENT_TIMEOUT: ${{ secrets.GOPHER_CLIENT_TIMEOUT }} run: make test ready-to-merge: From c42639f8b7e9a94deb1fc03eb47309eb1aa5e7b5 Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Tue, 4 Nov 2025 12:53:29 -0800 Subject: [PATCH 09/21] chore: update tests --- .github/workflows/tests.yml | 12 ++++ agent/agent.go | 26 ++++++-- agent/twitter.go | 124 ++++++++++++++++++++++++++++++++++-- 3 files changed, 152 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ba4f43d..8942227 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,6 +24,18 @@ jobs: path: | cookies key: ${{ runner.os }}-cookies + - name: Verify environment variables + env: + OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} + GOPHER_CLIENT_TOKEN: ${{ secrets.GOPHER_CLIENT_TOKEN }} + GOPHER_CLIENT_URL: ${{ secrets.GOPHER_CLIENT_URL }} + GOPHER_CLIENT_TIMEOUT: ${{ secrets.GOPHER_CLIENT_TIMEOUT }} + run: | + echo "Checking environment variables..." + if [ -z "$OPENAI_TOKEN" ]; then echo "ERROR: OPENAI_TOKEN is not set"; exit 1; else echo "✓ OPENAI_TOKEN is set"; fi + if [ -z "$GOPHER_CLIENT_TOKEN" ]; then echo "ERROR: GOPHER_CLIENT_TOKEN is not set"; exit 1; else echo "✓ GOPHER_CLIENT_TOKEN is set (length: ${#GOPHER_CLIENT_TOKEN})"; fi + if [ -z "$GOPHER_CLIENT_URL" ]; then echo "ERROR: GOPHER_CLIENT_URL is not set"; exit 1; else echo "✓ GOPHER_CLIENT_URL is set: $GOPHER_CLIENT_URL"; fi + if [ -z "$GOPHER_CLIENT_TIMEOUT" ]; then echo "WARN: GOPHER_CLIENT_TIMEOUT is not set, will use default"; else echo "✓ GOPHER_CLIENT_TIMEOUT is set: $GOPHER_CLIENT_TIMEOUT"; fi - name: Run tests env: OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} diff --git a/agent/agent.go b/agent/agent.go index af9e276..849aea6 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -3,6 +3,7 @@ package agent import ( "context" "errors" + "fmt" "time" "github.com/mudler/cogito" @@ -30,14 +31,27 @@ const ( DefaultPromptSuffix = "If no date range is specified, search the last 1 day" // DefaultDataCollectionInstructions provides explicit guidance about trying all sources DefaultDataCollectionInstructions = ` +CRITICAL: You MUST use the available tools to gather data. Do NOT attempt to answer without using tools. + +You have access to these tools: +1. search_web - Search and scrape web pages +2. search_twitter - Search Twitter for tweets from specific accounts + +REQUIRED ACTIONS: +1. You MUST use search_web to fetch data from the provided websites +2. You MUST use search_twitter to gather sentiment from Twitter accounts +3. Do NOT skip using tools - they are required to complete this task + IMPORTANT: You must attempt to gather data from ALL available sources, even if some fail. - Try ALL websites provided, even if some URLs timeout or return errors -- Execute Twitter searches for sentiment analysis, but IMPORTANT: Randomly sample Twitter accounts and query ONLY 1 account per search - - Randomly select 1 account from the provided list for each search +- Execute Twitter searches for sentiment analysis, but IMPORTANT: Randomly sample Twitter accounts + - Randomly select accounts from the provided list (typically 3-6 accounts for good coverage) - DO NOT exhaustively query all accounts - a random sample is sufficient - Query format: 'from:username (BTC OR Bitcoin OR ETH OR Ethereum OR SOL OR Solana)' - NO SPACE after 'from:' (e.g., 'from:JamesWynnReal', NOT 'from: JamesWynnReal') - Use hashtags and keywords: '#BTC OR #ETH OR bitcoin OR ethereum' - - Example correct query: 'from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03' + - For faster execution, you can batch multiple queries using the 'queries' array parameter - this will execute them concurrently + - Example single query: 'from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03' + - Example batch queries: ["from:JamesWynnReal (BTC OR Bitcoin) since:2025-11-03", "from:CryptoWendyO (ETH OR Ethereum) since:2025-11-03", "from:PeterLBrandt (SOL OR Solana) since:2025-11-03"] - Continue with remaining sources even if earlier sources fail - Partial data is acceptable - gather what you can from each source - Use multiple iterations to systematically collect data from all sources before synthesizing results @@ -184,12 +198,16 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* a.llm, fragment, cogito.WithContext(ctx), - cogito.WithIterations(3), // Reduced for faster testing (was 10) + cogito.WithIterations(6), // Increased to allow more Twitter account sampling cogito.WithMaxAttempts(1), // Allow multiple attempts for tool selection cogito.WithForceReasoning(), // Force LLM to reason about tool usage cogito.WithTools(&WebSearch{Client: a.Client}, &TwitterSearch{Client: a.Client}), ) if err != nil { + // Check if it's ErrNoToolSelected and provide a more helpful error + if errors.Is(err, cogito.ErrNoToolSelected) { + return nil, fmt.Errorf("LLM did not select any tools. This task requires using search_web and search_twitter tools to gather data: %w", err) + } return nil, err } diff --git a/agent/twitter.go b/agent/twitter.go index 3f8f60c..5ce2f49 100644 --- a/agent/twitter.go +++ b/agent/twitter.go @@ -2,9 +2,12 @@ package agent import ( "encoding/json" + "fmt" + "sync" "github.com/gopher-lab/gopher-client/client" "github.com/masa-finance/tee-worker/v2/api/args/twitter" + "github.com/masa-finance/tee-worker/v2/api/types" openai "github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai/jsonschema" ) @@ -25,10 +28,15 @@ superhero since:2015-12-21 containing "superhero" and sent since date "2015-12- If no date range is specified, default to the last 1 day (use since:YYYY-MM-DD format, typically one day before today). CORRECT EXAMPLES: +Single query: - from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03 - from:CryptoWendyO #BTC OR #ETH since:2025-11-03 - from:VitalikButerin (ethereum OR ETH) since:2025-11-03 +Batch queries (executed concurrently): +- Use the "queries" parameter with an array of queries for faster parallel execution +- Example: ["from:JamesWynnReal (BTC OR Bitcoin) since:2025-11-03", "from:CryptoWendyO (ETH OR Ethereum) since:2025-11-03", "from:PeterLBrandt (SOL OR Solana) since:2025-11-03"] + INCORRECT (DO NOT USE SPACES AFTER from:): - from: JamesWynnReal (WRONG - has space after from:) - from: CryptoWendyO (WRONG - has space after from:) @@ -44,7 +52,7 @@ func (t *TwitterSearch) Name() string { } func (t *TwitterSearch) Description() string { - return "Search Twitter using the provided query. Include operators like 'since:YYYY-MM-DD' (typically one day before today). Defaults to last 1 day if none provided. CRITICAL: Query ONLY 1 account per search using format 'from:username' with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal', NOT 'from: JamesWynnReal'). Randomly sample accounts - do not exhaustively query all accounts. Use hashtags and keywords like '#BTC OR #ETH OR bitcoin OR ethereum' to find relevant tweets." + return "Search Twitter using the provided query or queries. Include operators like 'since:YYYY-MM-DD' (typically one day before today). Defaults to last 1 day if none provided. You can provide a single 'query' or multiple 'queries' as an array. For multiple queries, they will be executed concurrently. CRITICAL: Use 'from:username' format with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal', NOT 'from: JamesWynnReal'). Randomly sample accounts - do not exhaustively query all accounts. Use hashtags and keywords like '#BTC OR #ETH OR bitcoin OR ethereum' to find relevant tweets." } // Tool describes the tool for the underlying LLM provider (OpenAI-compatible) @@ -57,17 +65,33 @@ func (t *TwitterSearch) Tool() openai.Tool { Parameters: jsonschema.Definition{ Type: jsonschema.Object, Properties: map[string]jsonschema.Definition{ - "query": {Type: jsonschema.String, Description: "Twitter advanced search query. CRITICAL: Use 'from:username' format with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal (BTC OR Bitcoin)', NOT 'from: JamesWynnReal'). Include operators like 'since:YYYY-MM-DD' (typically one day before today), hashtags (#BTC), and keywords."}, + "query": { + Type: jsonschema.String, + Description: "Twitter advanced search query. CRITICAL: Use 'from:username' format with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal (BTC OR Bitcoin)', NOT 'from: JamesWynnReal'). Include operators like 'since:YYYY-MM-DD' (typically one day before today), hashtags (#BTC), and keywords.", + }, + "queries": { + Type: jsonschema.Array, + Description: "Array of Twitter advanced search queries to execute concurrently. Each query should follow the same format as 'query'. When provided, all queries will be executed in parallel for faster results.", + Items: &jsonschema.Definition{ + Type: jsonschema.String, + }, + }, }, - Required: []string{"query"}, }, }, } } // Execute executes the tool. Signature follows Cogito's ToolDefinitionInterface expectations. -// Expects params to include {"query": "..."} per the schema definition. +// Supports both single query {"query": "..."} and multiple queries {"queries": ["...", "..."]} +// Multiple queries are executed concurrently using goroutines. func (t *TwitterSearch) Execute(params map[string]any) (string, error) { + // Check if multiple queries are provided + if queries, ok := params["queries"].([]any); ok && len(queries) > 0 { + return t.executeConcurrent(queries) + } + + // Single query execution (backward compatible) var query string if q, ok := params["query"].(string); ok { query = q @@ -76,13 +100,17 @@ func (t *TwitterSearch) Execute(params map[string]any) (string, error) { query = string(b) } + return t.executeSingle(query) +} + +// executeSingle executes a single Twitter search query +func (t *TwitterSearch) executeSingle(query string) (string, error) { args := twitter.NewSearchArguments() args.Query = query docs, err := t.Client.SearchTwitterWithArgs(args) if err != nil { // Return error as a structured result string so the LLM can see what happened - // This allows the agent to continue with partial data errorResult := map[string]any{ "error": true, "query": query, @@ -94,7 +122,91 @@ func (t *TwitterSearch) Execute(params map[string]any) (string, error) { } // Return full documents - lean structure with useful metadata (username, created_at, likes, etc.) - // Embedding and Score are omitempty so won't be serialized b, _ := json.Marshal(docs) return string(b), nil } + +// executeConcurrent executes multiple Twitter search queries concurrently using goroutines +func (t *TwitterSearch) executeConcurrent(queries []any) (string, error) { + type queryResult struct { + query string + documents []types.Document + err error + } + + // Convert queries to strings + queryStrings := make([]string, 0, len(queries)) + for _, q := range queries { + if qStr, ok := q.(string); ok { + queryStrings = append(queryStrings, qStr) + } + } + + if len(queryStrings) == 0 { + return "[]", nil + } + + // Channel to collect results + results := make(chan queryResult, len(queryStrings)) + var wg sync.WaitGroup + + // Execute all queries concurrently + for _, query := range queryStrings { + wg.Add(1) + go func(q string) { + defer wg.Done() + args := twitter.NewSearchArguments() + args.Query = q + + docs, err := t.Client.SearchTwitterWithArgs(args) + result := queryResult{ + query: q, + documents: docs, + err: err, + } + results <- result + }(query) + } + + // Wait for all goroutines to complete + go func() { + wg.Wait() + close(results) + }() + + // Collect all results + allDocs := []types.Document{} + errors := []map[string]any{} + + for result := range results { + if result.err != nil { + // Store error info but continue processing other results + errors = append(errors, map[string]any{ + "error": true, + "query": result.query, + "error_msg": result.err.Error(), + }) + } else { + allDocs = append(allDocs, result.documents...) + } + } + + // Build response with aggregated results and any errors + response := map[string]any{ + "documents": allDocs, + "total_queries": len(queryStrings), + "successful_queries": len(queryStrings) - len(errors), + "failed_queries": len(errors), + } + + if len(errors) > 0 { + response["errors"] = errors + } + + b, err := json.Marshal(response) + if err != nil { + return "", fmt.Errorf("failed to marshal results: %w", err) + } + + return string(b), nil +} From a034fcd21a69b4a98fd8e2f9d3b302b2786a7488 Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Tue, 4 Nov 2025 13:02:15 -0800 Subject: [PATCH 10/21] chore: run only client tests --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 388aa5c..cc91e43 100644 --- a/Makefile +++ b/Makefile @@ -59,10 +59,10 @@ run: build ## Build and run the application @./$(BUILD_DIR)/$(BINARY_NAME) .PHONY: test -test: deps ## Run tests with coverage (used by GitHub Actions) - @echo "Running tests with coverage..." +test: deps ## Run client tests with coverage (used by GitHub Actions) + @echo "Running client tests with coverage..." @mkdir -p $(COVERAGE_DIR) - @$(GOTEST) -v -count=1 -p=1 -coverprofile=$(COVERAGE_DIR)/coverage.txt -covermode=atomic $(TEST_ARGS) + @$(GOTEST) -v -count=1 -p=1 -coverprofile=$(COVERAGE_DIR)/coverage.txt -covermode=atomic ./client .PHONY: test-ginkgo test-ginkgo: deps ## Run tests using Ginkgo From dfa0b3f56102cb3e10ff879e578e84e5928406b6 Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Tue, 4 Nov 2025 13:05:38 -0800 Subject: [PATCH 11/21] chore: fix tests --- .github/workflows/tests.yml | 41 +++++++++++++++---------------------- agent/agent_test.go | 2 -- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8942227..edc1507 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,35 +24,26 @@ jobs: path: | cookies key: ${{ runner.os }}-cookies - - name: Verify environment variables - env: - OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} - GOPHER_CLIENT_TOKEN: ${{ secrets.GOPHER_CLIENT_TOKEN }} - GOPHER_CLIENT_URL: ${{ secrets.GOPHER_CLIENT_URL }} - GOPHER_CLIENT_TIMEOUT: ${{ secrets.GOPHER_CLIENT_TIMEOUT }} + - name: Create .env file from secrets run: | - echo "Checking environment variables..." - if [ -z "$OPENAI_TOKEN" ]; then echo "ERROR: OPENAI_TOKEN is not set"; exit 1; else echo "✓ OPENAI_TOKEN is set"; fi - if [ -z "$GOPHER_CLIENT_TOKEN" ]; then echo "ERROR: GOPHER_CLIENT_TOKEN is not set"; exit 1; else echo "✓ GOPHER_CLIENT_TOKEN is set (length: ${#GOPHER_CLIENT_TOKEN})"; fi - if [ -z "$GOPHER_CLIENT_URL" ]; then echo "ERROR: GOPHER_CLIENT_URL is not set"; exit 1; else echo "✓ GOPHER_CLIENT_URL is set: $GOPHER_CLIENT_URL"; fi - if [ -z "$GOPHER_CLIENT_TIMEOUT" ]; then echo "WARN: GOPHER_CLIENT_TIMEOUT is not set, will use default"; else echo "✓ GOPHER_CLIENT_TIMEOUT is set: $GOPHER_CLIENT_TIMEOUT"; fi + cat > .env << EOF + OPENAI_TOKEN=${{ secrets.OPENAI_TOKEN }} + GOPHER_CLIENT_TOKEN=${{ secrets.GOPHER_CLIENT_TOKEN }} + GOPHER_CLIENT_URL=${{ secrets.GOPHER_CLIENT_URL }} + GOPHER_CLIENT_TIMEOUT=${{ secrets.GOPHER_CLIENT_TIMEOUT }} + EOF - name: Run tests - env: - OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} - GOPHER_CLIENT_TOKEN: ${{ secrets.GOPHER_CLIENT_TOKEN }} - GOPHER_CLIENT_URL: ${{ secrets.GOPHER_CLIENT_URL }} - GOPHER_CLIENT_TIMEOUT: ${{ secrets.GOPHER_CLIENT_TIMEOUT }} run: make test ready-to-merge: - runs-on: ubuntu-latest + runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Verify that merging is OK - run: | - if grep -rE 'DO[ ]NOT[ ]MERGE|[ ]FIXME' .; then - exit 1 - fi + - name: Verify that merging is OK + run: | + if grep -rE 'DO[ ]NOT[ ]MERGE|[ ]FIXME' .; then + exit 1 + fi diff --git a/agent/agent_test.go b/agent/agent_test.go index 33c5736..326a58a 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -40,8 +40,6 @@ var _ = Describe("Agent integration", func() { // Verify we got multiple topics (at least Bitcoin and Ethereum) Expect(out.Assets).ToNot(BeEmpty(), "Expected at least one asset in the output") - fmt.Println("--------------------------------") fmt.Println(prettyPrint(out.Assets)) - }) }) From 6a377d76217fb9d41a5dbde1664faf90639adaea Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Tue, 4 Nov 2025 13:06:32 -0800 Subject: [PATCH 12/21] chore: less agent iterations --- agent/agent.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/agent.go b/agent/agent.go index 849aea6..84d291a 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -198,7 +198,7 @@ func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (* a.llm, fragment, cogito.WithContext(ctx), - cogito.WithIterations(6), // Increased to allow more Twitter account sampling + cogito.WithIterations(3), // Increased to allow more Twitter account sampling cogito.WithMaxAttempts(1), // Allow multiple attempts for tool selection cogito.WithForceReasoning(), // Force LLM to reason about tool usage cogito.WithTools(&WebSearch{Client: a.Client}, &TwitterSearch{Client: a.Client}), From 815b4852b1a7af59c4c29f210876d34a43d535a1 Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Tue, 4 Nov 2025 20:49:11 -0800 Subject: [PATCH 13/21] chore: cleanup test --- agent/agent_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/agent/agent_test.go b/agent/agent_test.go index 326a58a..4aa3aba 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -8,7 +8,6 @@ import ( "time" "github.com/gopher-lab/gopher-client/agent" - "github.com/gopher-lab/gopher-client/config" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -21,11 +20,6 @@ func prettyPrint(v any) string { var _ = Describe("Agent integration", func() { It("creates Agent from config and performs sentiment analysis on assets", func() { - cfg := config.MustLoadConfig() - if cfg.OpenAIToken == "" { - Skip("OPENAI_TOKEN not set; skipping agent integration test") - } - ag, err := agent.NewAgentFromConfig() Expect(err).ToNot(HaveOccurred()) From 262bd5aa109c1d7225e1e0753886eb7e6f5f3885 Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Tue, 4 Nov 2025 20:52:28 -0800 Subject: [PATCH 14/21] chore: cleanup test --- .github/workflows/tests.yml | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index edc1507..1a14fc0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,22 +17,12 @@ jobs: uses: actions/setup-go@v5 with: go-version: '1.24' - - name: Restore cached cookies - id: cache-cookies-restore - uses: actions/cache/restore@v4 - with: - path: | - cookies - key: ${{ runner.os }}-cookies - - name: Create .env file from secrets - run: | - cat > .env << EOF - OPENAI_TOKEN=${{ secrets.OPENAI_TOKEN }} - GOPHER_CLIENT_TOKEN=${{ secrets.GOPHER_CLIENT_TOKEN }} - GOPHER_CLIENT_URL=${{ secrets.GOPHER_CLIENT_URL }} - GOPHER_CLIENT_TIMEOUT=${{ secrets.GOPHER_CLIENT_TIMEOUT }} - EOF - name: Run tests + env: + OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} + GOPHER_CLIENT_TOKEN: ${{ secrets.GOPHER_CLIENT_TOKEN }} + GOPHER_CLIENT_URL: ${{ secrets.GOPHER_CLIENT_URL }} + GOPHER_CLIENT_TIMEOUT: ${{ secrets.GOPHER_CLIENT_TIMEOUT }} run: make test ready-to-merge: @@ -41,7 +31,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - name: Verify that merging is OK run: | if grep -rE 'DO[ ]NOT[ ]MERGE|[ ]FIXME' .; then From 05a8b946756279a653d48bca759ec930140ac1ad Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Tue, 4 Nov 2025 20:57:46 -0800 Subject: [PATCH 15/21] chore: clean tests --- .github/workflows/tests.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1a14fc0..6b49966 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,12 +17,15 @@ jobs: uses: actions/setup-go@v5 with: go-version: '1.24' + - name: Create .env file from secrets + run: | + cat > .env << EOF + OPENAI_TOKEN=${{ secrets.OPENAI_TOKEN }} + GOPHER_CLIENT_TOKEN=${{ secrets.GOPHER_CLIENT_TOKEN }} + GOPHER_CLIENT_URL=${{ secrets.GOPHER_CLIENT_URL }} + GOPHER_CLIENT_TIMEOUT=${{ secrets.GOPHER_CLIENT_TIMEOUT }} + EOF - name: Run tests - env: - OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} - GOPHER_CLIENT_TOKEN: ${{ secrets.GOPHER_CLIENT_TOKEN }} - GOPHER_CLIENT_URL: ${{ secrets.GOPHER_CLIENT_URL }} - GOPHER_CLIENT_TIMEOUT: ${{ secrets.GOPHER_CLIENT_TIMEOUT }} run: make test ready-to-merge: From 93f22e71399c9c8d418d1adcda4c1c8644aa2beb Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Wed, 5 Nov 2025 09:50:15 -0800 Subject: [PATCH 16/21] chore: removes agent --- agent/agent.go | 236 -------------------------------------- agent/agent_suite_test.go | 13 --- agent/agent_test.go | 39 ------- agent/assets.go | 7 -- agent/kols.go | 104 ----------------- agent/twitter.go | 212 ---------------------------------- agent/web.go | 75 ------------ agent/websites.go | 12 -- 8 files changed, 698 deletions(-) delete mode 100644 agent/agent.go delete mode 100644 agent/agent_suite_test.go delete mode 100644 agent/agent_test.go delete mode 100644 agent/assets.go delete mode 100644 agent/kols.go delete mode 100644 agent/twitter.go delete mode 100644 agent/web.go delete mode 100644 agent/websites.go diff --git a/agent/agent.go b/agent/agent.go deleted file mode 100644 index 84d291a..0000000 --- a/agent/agent.go +++ /dev/null @@ -1,236 +0,0 @@ -package agent - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/mudler/cogito" - "github.com/mudler/cogito/structures" - "github.com/sashabaranov/go-openai/jsonschema" - - "github.com/gopher-lab/gopher-client/client" - "github.com/gopher-lab/gopher-client/config" - "github.com/gopher-lab/gopher-client/log" - "github.com/gopher-lab/gopher-client/types" -) - -type Agent struct { - llm cogito.LLM - Client *client.Client -} - -var ( - ErrOpenAITokenRequired = errors.New("must supply an OPENAI_TOKEN") -) - -const ( - DefaultModel = "gpt-5-nano" - DefaultOpenAIApiUrl = "https://api.openai.com/v1" - DefaultPromptSuffix = "If no date range is specified, search the last 1 day" - // DefaultDataCollectionInstructions provides explicit guidance about trying all sources - DefaultDataCollectionInstructions = ` -CRITICAL: You MUST use the available tools to gather data. Do NOT attempt to answer without using tools. - -You have access to these tools: -1. search_web - Search and scrape web pages -2. search_twitter - Search Twitter for tweets from specific accounts - -REQUIRED ACTIONS: -1. You MUST use search_web to fetch data from the provided websites -2. You MUST use search_twitter to gather sentiment from Twitter accounts -3. Do NOT skip using tools - they are required to complete this task - -IMPORTANT: You must attempt to gather data from ALL available sources, even if some fail. -- Try ALL websites provided, even if some URLs timeout or return errors -- Execute Twitter searches for sentiment analysis, but IMPORTANT: Randomly sample Twitter accounts - - Randomly select accounts from the provided list (typically 3-6 accounts for good coverage) - - DO NOT exhaustively query all accounts - a random sample is sufficient - - Query format: 'from:username (BTC OR Bitcoin OR ETH OR Ethereum OR SOL OR Solana)' - NO SPACE after 'from:' (e.g., 'from:JamesWynnReal', NOT 'from: JamesWynnReal') - - Use hashtags and keywords: '#BTC OR #ETH OR bitcoin OR ethereum' - - For faster execution, you can batch multiple queries using the 'queries' array parameter - this will execute them concurrently - - Example single query: 'from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03' - - Example batch queries: ["from:JamesWynnReal (BTC OR Bitcoin) since:2025-11-03", "from:CryptoWendyO (ETH OR Ethereum) since:2025-11-03", "from:PeterLBrandt (SOL OR Solana) since:2025-11-03"] -- Continue with remaining sources even if earlier sources fail -- Partial data is acceptable - gather what you can from each source -- Use multiple iterations to systematically collect data from all sources before synthesizing results -` -) - -// QueryOptions configures the behavior of the Query method -type QueryOptions struct { - Schema jsonschema.Definition - Instructions string - PromptSuffix string - FinalPrompt string -} - -// QueryOption modifies QueryOptions -type QueryOption func(*QueryOptions) - -// WithSchema sets a custom schema for structured extraction -func WithSchema(schema jsonschema.Definition) QueryOption { - return func(opts *QueryOptions) { - opts.Schema = schema - } -} - -// WithInstructions sets custom query instructions -func WithInstructions(instructions string) QueryOption { - return func(opts *QueryOptions) { - opts.Instructions = instructions - } -} - -// WithPromptSuffix sets a custom prompt suffix -func WithPromptSuffix(suffix string) QueryOption { - return func(opts *QueryOptions) { - opts.PromptSuffix = suffix - } -} - -// WithFinalPrompt sets a custom final prompt instruction -func WithFinalPrompt(prompt string) QueryOption { - return func(opts *QueryOptions) { - opts.FinalPrompt = prompt - } -} - -// DefaultSchema returns the default schema for topic sentiment analysis -func DefaultSchema() jsonschema.Definition { - return jsonschema.Definition{ - Type: jsonschema.Object, - AdditionalProperties: false, - Properties: map[string]jsonschema.Definition{ - "assets": { - Type: jsonschema.Array, - Description: "Track the market sentiment of assets, such as Bitcoin, Ethereum, and other cryptocurrencies.", - Items: &jsonschema.Definition{ - Type: jsonschema.Object, - AdditionalProperties: false, - Properties: map[string]jsonschema.Definition{ - "asset": {Type: jsonschema.String, Description: "Asset name"}, - "reasoning": {Type: jsonschema.String, Description: "Brief reasoning about the sentiment of the asset"}, - "sentiment": {Type: jsonschema.Integer, Description: "Numeric sentiment score from 0-100. Scale: 0 = most bearish, 50 = neutral, 100 = most bullish. Each asset should have a distinct score based on its unique data and sentiment signals."}, - }, - Required: []string{"asset", "reasoning", "sentiment"}, - }, - }, - }, - Required: []string{"assets"}, - } -} - -// DefaultFinalPrompt returns the default final prompt instruction -func DefaultFinalPrompt() string { - return "Return now only a JSON object with fields that match the supplied schema. IMPORTANT: Each asset must have a distinct sentiment score based on its unique data and signals - do not use the same sentiment value for different assets. Analyze each asset independently." -} - -// New creates a new Agent with the provided OpenAI token and model. Model defaults to gpt-5-nano. -func New(c *client.Client, openAIToken string) (*Agent, error) { - if openAIToken == "" { - return nil, ErrOpenAITokenRequired - } - llm := cogito.NewOpenAILLM(DefaultModel, openAIToken, DefaultOpenAIApiUrl) - return &Agent{llm: llm, Client: c}, nil -} - -// NewFromConfig creates a new Agent from config, defaulting the model to gpt-5-nano. -func NewFromConfig(c *client.Client) (*Agent, error) { - cfg, err := config.LoadConfig() - if err != nil { - return nil, err - } - return New(c, cfg.OpenAIToken) -} - -// NewAgentFromConfig creates an Agent from config in a single call. -// This convenience function creates both the underlying Client and Agent automatically, -// eliminating the need to manually create a client first. -func NewAgentFromConfig() (*Agent, error) { - cfg, err := config.LoadConfig() - if err != nil { - return nil, err - } - c, err := client.NewClientFromConfig() - if err != nil { - return nil, err - } - ag, err := New(c, cfg.OpenAIToken) - if err != nil { - return nil, err - } - return ag, nil -} - -// Query runs the agent with the provided natural language instruction. -// It uses Cogito with the TwitterSearch and WebSearch tools and extracts a structured Output. -// Query options can be provided to customize schema, instructions, and prompts. -func (a *Agent) Query(ctx context.Context, query string, opts ...QueryOption) (*types.Output, error) { - // Apply options with defaults - options := &QueryOptions{ - Schema: DefaultSchema(), - Instructions: TwitterQueryInstructions, - PromptSuffix: DefaultPromptSuffix, - FinalPrompt: DefaultFinalPrompt(), - } - - for _, opt := range opts { - opt(options) - } - - // Build full prompt with query, instructions, and suffix - fullPrompt := query - fullPrompt += "\n\n" + DefaultDataCollectionInstructions - if options.Instructions != "" { - fullPrompt += "\n\n" + options.Instructions - } - if options.PromptSuffix != "" { - fullPrompt += "\n\n" + options.PromptSuffix - } - - fragment := cogito.NewEmptyFragment(). - AddMessage("user", fullPrompt) - - // Execute tools with the LLM - improved, err := cogito.ExecuteTools( - a.llm, - fragment, - cogito.WithContext(ctx), - cogito.WithIterations(3), // Increased to allow more Twitter account sampling - cogito.WithMaxAttempts(1), // Allow multiple attempts for tool selection - cogito.WithForceReasoning(), // Force LLM to reason about tool usage - cogito.WithTools(&WebSearch{Client: a.Client}, &TwitterSearch{Client: a.Client}), - ) - if err != nil { - // Check if it's ErrNoToolSelected and provide a more helpful error - if errors.Is(err, cogito.ErrNoToolSelected) { - return nil, fmt.Errorf("LLM did not select any tools. This task requires using search_web and search_twitter tools to gather data: %w", err) - } - return nil, err - } - - // Ask the model to return structured JSON according to the final prompt - result, err := a.llm.Ask(ctx, improved.AddMessage("user", options.FinalPrompt)) - if err != nil { - return nil, err - } - - out := &types.Output{} - - s := structures.Structure{ - Schema: options.Schema, - Object: out, - } - - // Provide a timeout context for extraction - ctxExtract, cancel := context.WithTimeout(ctx, 2*time.Minute) - defer cancel() - - if err := result.ExtractStructure(ctxExtract, a.llm, s); err != nil { - log.Error("Extraction error", err) - } - - return out, nil -} diff --git a/agent/agent_suite_test.go b/agent/agent_suite_test.go deleted file mode 100644 index 736071d..0000000 --- a/agent/agent_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package agent_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestAgent(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Agent Suite") -} diff --git a/agent/agent_test.go b/agent/agent_test.go deleted file mode 100644 index 4aa3aba..0000000 --- a/agent/agent_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package agent_test - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/gopher-lab/gopher-client/agent" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func prettyPrint(v any) string { - b, _ := json.MarshalIndent(v, "", " ") - return string(b) -} - -var _ = Describe("Agent integration", func() { - It("creates Agent from config and performs sentiment analysis on assets", func() { - ag, err := agent.NewAgentFromConfig() - Expect(err).ToNot(HaveOccurred()) - - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) - defer cancel() - - query := fmt.Sprintf("use all available tools to determine the market sentiment of the following assets: %s, using the following websites: %s, and the following twitter accounts: %s", strings.Join(agent.Assets, ", "), strings.Join(agent.Websites, ", "), strings.Join(agent.Kols, ", ")) - out, err := ag.Query(ctx, query) - Expect(err).ToNot(HaveOccurred()) - Expect(out).ToNot(BeNil()) - - // Verify we got multiple topics (at least Bitcoin and Ethereum) - Expect(out.Assets).ToNot(BeEmpty(), "Expected at least one asset in the output") - - fmt.Println(prettyPrint(out.Assets)) - }) -}) diff --git a/agent/assets.go b/agent/assets.go deleted file mode 100644 index 67395c1..0000000 --- a/agent/assets.go +++ /dev/null @@ -1,7 +0,0 @@ -package agent - -var Assets = []string{ - "bitcoin", - "ethereum", - "solana", -} diff --git a/agent/kols.go b/agent/kols.go deleted file mode 100644 index bc774ed..0000000 --- a/agent/kols.go +++ /dev/null @@ -1,104 +0,0 @@ -package agent - -var Kols = []string{ - "JamesWynnReal", - "buzzdefi0x", - "crypto_birb", - "cryptojack", - "cryptomanran", - "IvanOnTech", - "TeddyCleps", - "BigCheds", - "Nebraskangooner", - "Ashcryptoreal", - "CredibleCrypto", - "_ColesTrading", - "Livercoin", - "CastilloTrading", - "legen_eth", - "DaanCrypto", - "kyle_chasse", - "MikybullCrypto", - "CryptoTony__", - "Crypto_Chase", - "martypartymusic", - "eliz883", - "Trader_XO", - "SatoshiFlipper", - "CryptoTea_", - "CryptoMichNL", - "scottmelker", - "PeterLBrandt", - "CryptoDonAlt", - "TheCryptoDog", - "CryptoKaleo", - "woonomic", - "100trillionUSD", - "RaoulGMI", - "APompliano", - "CryptoWendyO", - "iamjosephyoung", - "intocryptoverse", - "elliotrades", - "WClementeIII", - "CryptoHayes", - "PaikCapital", - "Bitdealer_", - "_Checkmatey_", - "AltcoinGordon", - "MadelonVos__", - "TheCryptoLark", - "AltcoinDaily", - "lookonchain", - "cobie", - "GoingParabolic", - "LynAldenContact", - "CathieDWood", - "TuurDemeester", - "ercwl", - "AriDavidPaul", - "cburniske", - "hasufl", - "scupytrooples", - "Arthur_0x", - "RyanWatkins_", - "PeterMcCormack", - "danheld", - "tatianakoffman", - "notsofast", - "saylor", - "VitalikButerin", - "ethereumJoseph", - "cz_binance", - "adam3us", - "CryptoJack", - "maxkeiser", - "novogratz", - "jackmallers", - "Excellion", - "brian_armstrong", - "justinsuntron", - "CarlRunefelt", - "LukeBelmar", - "viveksen", - "MustStopMurad", - "JackTheRippler", - "naiive", - "Gordon", - "lynk", - "mattwalker", - "SergeyNazarov", - "paoloardoino", - "fundstrat", - "anatolyakovenko", - "jerallaire", - "CaitlinLong_", - "starkness", - "robin_linus", - "_RichardTeng", - "ASvanevik", - "Melt_Dem", - "nic__carter", - "naval", - "laurashin", -} diff --git a/agent/twitter.go b/agent/twitter.go deleted file mode 100644 index 5ce2f49..0000000 --- a/agent/twitter.go +++ /dev/null @@ -1,212 +0,0 @@ -package agent - -import ( - "encoding/json" - "fmt" - "sync" - - "github.com/gopher-lab/gopher-client/client" - "github.com/masa-finance/tee-worker/v2/api/args/twitter" - "github.com/masa-finance/tee-worker/v2/api/types" - openai "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/jsonschema" -) - -// TwitterQueryInstructions contains long-form guidance and examples for constructing Twitter queries. -// If no date range is specified by the user, searches should consider the last 1 day. -const TwitterQueryInstructions = ` -watching now containing both "watching" and "now". This is the default operator. -"happy hour" containing the exact phrase "happy hour". -love OR hate containing either "love" or "hate" (or both). -beer -root containing "beer" but not "root". -#haiku containing the hashtag "haiku". -from:interior sent from Twitter account "interior". CRITICAL: NO SPACE after 'from:' (e.g., 'from:username', NOT 'from: username'). -to:NASA a Tweet authored in reply to Twitter account "NASA". -@NASA mentioning Twitter account "NASA". -superhero since:2015-12-21 containing "superhero" and sent since date "2015-12-21" (year-month-day). - -If no date range is specified, default to the last 1 day (use since:YYYY-MM-DD format, typically one day before today). - -CORRECT EXAMPLES: -Single query: -- from:JamesWynnReal (BTC OR Bitcoin OR ETH OR Ethereum) since:2025-11-03 -- from:CryptoWendyO #BTC OR #ETH since:2025-11-03 -- from:VitalikButerin (ethereum OR ETH) since:2025-11-03 - -Batch queries (executed concurrently): -- Use the "queries" parameter with an array of queries for faster parallel execution -- Example: ["from:JamesWynnReal (BTC OR Bitcoin) since:2025-11-03", "from:CryptoWendyO (ETH OR Ethereum) since:2025-11-03", "from:PeterLBrandt (SOL OR Solana) since:2025-11-03"] - -INCORRECT (DO NOT USE SPACES AFTER from:): -- from: JamesWynnReal (WRONG - has space after from:) -- from: CryptoWendyO (WRONG - has space after from:) -` - -// TwitterSearch is a Cogito tool that bridges to the client's SearchTwitterWithArgs -type TwitterSearch struct { - Client *client.Client -} - -func (t *TwitterSearch) Name() string { - return "search_twitter" -} - -func (t *TwitterSearch) Description() string { - return "Search Twitter using the provided query or queries. Include operators like 'since:YYYY-MM-DD' (typically one day before today). Defaults to last 1 day if none provided. You can provide a single 'query' or multiple 'queries' as an array. For multiple queries, they will be executed concurrently. CRITICAL: Use 'from:username' format with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal', NOT 'from: JamesWynnReal'). Randomly sample accounts - do not exhaustively query all accounts. Use hashtags and keywords like '#BTC OR #ETH OR bitcoin OR ethereum' to find relevant tweets." -} - -// Tool describes the tool for the underlying LLM provider (OpenAI-compatible) -func (t *TwitterSearch) Tool() openai.Tool { - return openai.Tool{ - Type: openai.ToolTypeFunction, - Function: &openai.FunctionDefinition{ - Name: t.Name(), - Description: t.Description(), - Parameters: jsonschema.Definition{ - Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "query": { - Type: jsonschema.String, - Description: "Twitter advanced search query. CRITICAL: Use 'from:username' format with NO SPACE after 'from:' (e.g., 'from:JamesWynnReal (BTC OR Bitcoin)', NOT 'from: JamesWynnReal'). Include operators like 'since:YYYY-MM-DD' (typically one day before today), hashtags (#BTC), and keywords.", - }, - "queries": { - Type: jsonschema.Array, - Description: "Array of Twitter advanced search queries to execute concurrently. Each query should follow the same format as 'query'. When provided, all queries will be executed in parallel for faster results.", - Items: &jsonschema.Definition{ - Type: jsonschema.String, - }, - }, - }, - }, - }, - } -} - -// Execute executes the tool. Signature follows Cogito's ToolDefinitionInterface expectations. -// Supports both single query {"query": "..."} and multiple queries {"queries": ["...", "..."]} -// Multiple queries are executed concurrently using goroutines. -func (t *TwitterSearch) Execute(params map[string]any) (string, error) { - // Check if multiple queries are provided - if queries, ok := params["queries"].([]any); ok && len(queries) > 0 { - return t.executeConcurrent(queries) - } - - // Single query execution (backward compatible) - var query string - if q, ok := params["query"].(string); ok { - query = q - } else { - b, _ := json.Marshal(params) - query = string(b) - } - - return t.executeSingle(query) -} - -// executeSingle executes a single Twitter search query -func (t *TwitterSearch) executeSingle(query string) (string, error) { - args := twitter.NewSearchArguments() - args.Query = query - - docs, err := t.Client.SearchTwitterWithArgs(args) - if err != nil { - // Return error as a structured result string so the LLM can see what happened - errorResult := map[string]any{ - "error": true, - "query": query, - "error_msg": err.Error(), - "documents": []any{}, - } - b, _ := json.Marshal(errorResult) - return string(b), nil - } - - // Return full documents - lean structure with useful metadata (username, created_at, likes, etc.) - b, _ := json.Marshal(docs) - return string(b), nil -} - -// executeConcurrent executes multiple Twitter search queries concurrently using goroutines -func (t *TwitterSearch) executeConcurrent(queries []any) (string, error) { - type queryResult struct { - query string - documents []types.Document - err error - } - - // Convert queries to strings - queryStrings := make([]string, 0, len(queries)) - for _, q := range queries { - if qStr, ok := q.(string); ok { - queryStrings = append(queryStrings, qStr) - } - } - - if len(queryStrings) == 0 { - return "[]", nil - } - - // Channel to collect results - results := make(chan queryResult, len(queryStrings)) - var wg sync.WaitGroup - - // Execute all queries concurrently - for _, query := range queryStrings { - wg.Add(1) - go func(q string) { - defer wg.Done() - args := twitter.NewSearchArguments() - args.Query = q - - docs, err := t.Client.SearchTwitterWithArgs(args) - result := queryResult{ - query: q, - documents: docs, - err: err, - } - results <- result - }(query) - } - - // Wait for all goroutines to complete - go func() { - wg.Wait() - close(results) - }() - - // Collect all results - allDocs := []types.Document{} - errors := []map[string]any{} - - for result := range results { - if result.err != nil { - // Store error info but continue processing other results - errors = append(errors, map[string]any{ - "error": true, - "query": result.query, - "error_msg": result.err.Error(), - }) - } else { - allDocs = append(allDocs, result.documents...) - } - } - - // Build response with aggregated results and any errors - response := map[string]any{ - "documents": allDocs, - "total_queries": len(queryStrings), - "successful_queries": len(queryStrings) - len(errors), - "failed_queries": len(errors), - } - - if len(errors) > 0 { - response["errors"] = errors - } - - b, err := json.Marshal(response) - if err != nil { - return "", fmt.Errorf("failed to marshal results: %w", err) - } - - return string(b), nil -} diff --git a/agent/web.go b/agent/web.go deleted file mode 100644 index 1b4f290..0000000 --- a/agent/web.go +++ /dev/null @@ -1,75 +0,0 @@ -package agent - -import ( - "encoding/json" - - "github.com/gopher-lab/gopher-client/client" - "github.com/masa-finance/tee-worker/v2/api/args/web" - openai "github.com/sashabaranov/go-openai" - "github.com/sashabaranov/go-openai/jsonschema" -) - -// WebSearch is a Cogito tool that bridges to the client's SearchTwitterWithArgs -type WebSearch struct { - Client *client.Client -} - -func (t *WebSearch) Name() string { - return "search_web" -} - -func (t *WebSearch) Description() string { - return "Web search using the provided url. Call this tool once per URL - execute multiple calls sequentially to fetch multiple URLs." -} - -// Tool describes the tool for the underlying LLM provider (OpenAI-compatible) -func (t *WebSearch) Tool() openai.Tool { - return openai.Tool{ - Type: openai.ToolTypeFunction, - Function: &openai.FunctionDefinition{ - Name: t.Name(), - Description: t.Description(), - Parameters: jsonschema.Definition{ - Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "url": {Type: jsonschema.String, Description: "Web scrape url"}, - }, - Required: []string{"url"}, - }, - }, - } -} - -// Execute executes the tool. Signature follows Cogito's ToolDefinitionInterface expectations. -// Expects params to include {"url": "..."} per the schema definition. -func (t *WebSearch) Execute(params map[string]any) (string, error) { - var url string - if q, ok := params["url"].(string); ok { - url = q - } else { - b, _ := json.Marshal(params) - url = string(b) - } - - args := web.NewScraperArguments() - args.URL = url - - docs, err := t.Client.ScrapeWebWithArgs(args) - if err != nil { - // Return error as a structured result string so the LLM can see what happened - // This allows the agent to continue with partial data - errorResult := map[string]any{ - "error": true, - "url": url, - "error_msg": err.Error(), - "documents": []any{}, - } - b, _ := json.Marshal(errorResult) - return string(b), nil - } - - // Return full documents - lean structure with useful metadata (title, canonicalUrl, markdown, etc.) - // Embedding and Score are omitempty so won't be serialized - b, _ := json.Marshal(docs) - return string(b), nil -} diff --git a/agent/websites.go b/agent/websites.go deleted file mode 100644 index d045d73..0000000 --- a/agent/websites.go +++ /dev/null @@ -1,12 +0,0 @@ -package agent - -var Websites = []string{ - "https://coinmarketcap.com/gainers-losers", - // "https://farside.co.uk/btc/", - // "https://farside.co.uk/eth/", - // "https://coinmarketcap.com/gainers-losers/", - // "https://coin360.com/", - // "https://www.cmegroup.com/markets/interest-rates/cme-fedwatch-tool.html", - // "https://www.marketwatch.com/economy-politics/calendar", - // "https://www.coinglass.com/LiquidationData", -} From 604ef11555c10223db37ee39ea162d68eafee54c Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Wed, 5 Nov 2025 09:50:44 -0800 Subject: [PATCH 17/21] chore: remove agent test in makefile --- Makefile | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Makefile b/Makefile index cc91e43..f43f506 100644 --- a/Makefile +++ b/Makefile @@ -82,12 +82,6 @@ test-verbose: deps ## Run tests with verbose output @mkdir -p $(COVERAGE_DIR) @$(GOTEST) $(TEST_FLAGS) $(TEST_ARGS) -.PHONY: test-agent -test-agent: deps ## Run only agent package tests (lightweight) - @echo "Running agent package tests..." - @mkdir -p $(COVERAGE_DIR) - @$(GOTEST) -v -count=1 -p=1 -coverprofile=$(COVERAGE_DIR)/coverage.txt -covermode=atomic ./agent - .PHONY: coverage coverage: test ## Generate and display coverage report @echo "Generating coverage report..." From 850ff5d998373ca70ffcd1f2b949d9b6da98e2ed Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Wed, 5 Nov 2025 09:52:50 -0800 Subject: [PATCH 18/21] chore: go mod tidy and config cleanup --- config/config.go | 7 ++- go.mod | 20 ++------ go.sum | 123 ----------------------------------------------- types/agent.go | 22 --------- 4 files changed, 6 insertions(+), 166 deletions(-) delete mode 100644 types/agent.go diff --git a/config/config.go b/config/config.go index 5a486a6..fd2da62 100644 --- a/config/config.go +++ b/config/config.go @@ -13,10 +13,9 @@ import ( ) type Config struct { - BaseUrl string `envconfig:"GOPHER_CLIENT_URL" default:"https://data.gopher-ai.com/api"` - Timeout time.Duration `envconfig:"GOPHER_CLIENT_TIMEOUT" default:"60s"` - Token string `envconfig:"GOPHER_CLIENT_TOKEN"` - OpenAIToken string `envconfig:"OPENAI_TOKEN"` + BaseUrl string `envconfig:"GOPHER_CLIENT_URL" default:"https://data.gopher-ai.com/api"` + Timeout time.Duration `envconfig:"GOPHER_CLIENT_TIMEOUT" default:"60s"` + Token string `envconfig:"GOPHER_CLIENT_TOKEN"` } // LoadConfig loads the Config from environment variables. diff --git a/go.mod b/go.mod index 2f2533a..662eb75 100644 --- a/go.mod +++ b/go.mod @@ -6,38 +6,24 @@ require ( github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/masa-finance/tee-worker/v2 v2.0.1 - github.com/mudler/cogito v0.5.0 github.com/onsi/ginkgo/v2 v2.26.0 github.com/onsi/gomega v1.38.2 - github.com/sashabaranov/go-openai v1.41.2 ) require ( - dario.cat/mergo v1.0.2 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/jsonschema-go v0.3.0 // indirect github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/huandu/xstrings v1.5.0 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/modelcontextprotocol/go-sdk v1.1.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/tmc/langchaingo v0.1.14 // indirect - github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/stretchr/testify v1.11.1 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.28.0 // indirect golang.org/x/net v0.46.0 // indirect - golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect diff --git a/go.sum b/go.sum index 694871f..d228434 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,7 @@ -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= -github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= -github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= -github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -47,104 +10,42 @@ github.com/gkampitakis/go-snaps v0.5.14 h1:3fAqdB6BCPKHDMHAKRwtPUwYexKtGrNuw8HX/ github.com/gkampitakis/go-snaps v0.5.14/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= -github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0= github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grantdfoster/cogito v0.0.0-20251104184711-4b65f4d0640a h1:mVf4CH83q/CoPjOQPvS6PyHsTroOb4v6gVWHNfop+XU= -github.com/grantdfoster/cogito v0.0.0-20251104184711-4b65f4d0640a/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw= -github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= -github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= -github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/masa-finance/tee-worker/v2 v2.0.1 h1:slBs/++SaNldV0vFL5IpAZSyKKd0HYPsyrr7FWFjPgg= github.com/masa-finance/tee-worker/v2 v2.0.1/go.mod h1:+xlwtrj+bQDSf4jsPlJAMPvqvYrGS3ItHG0J1WRXweY= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA= -github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE= github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM= -github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= -github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= -github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= -github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -153,40 +54,16 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= -github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc= -github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378= -github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= -github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= diff --git a/types/agent.go b/types/agent.go deleted file mode 100644 index 7f7e686..0000000 --- a/types/agent.go +++ /dev/null @@ -1,22 +0,0 @@ -package types - -import ( - "github.com/masa-finance/tee-worker/v2/api/types" -) - -type Sentiment uint -type Reasoning string -type Asset string - -// TopicSummary represents a topic with sentiment and influencers discovered by the agent -type AssetSummary struct { - Asset Asset `json:"asset"` - Reasoning Reasoning `json:"reasoning"` - Sentiment Sentiment `json:"sentiment"` -} - -// Output is the agent's structured output -type Output struct { - Assets []AssetSummary `json:"assets"` - Documents []types.Document `json:"documents,omitempty"` -} From 748251975f71b168da57bf244e281caca6b3cca3 Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Wed, 5 Nov 2025 09:54:19 -0800 Subject: [PATCH 19/21] fix: test --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index f43f506..5fb4cde 100644 --- a/Makefile +++ b/Makefile @@ -59,10 +59,10 @@ run: build ## Build and run the application @./$(BUILD_DIR)/$(BINARY_NAME) .PHONY: test -test: deps ## Run client tests with coverage (used by GitHub Actions) - @echo "Running client tests with coverage..." +test: deps ## Run tests with coverage (used by GitHub Actions) + @echo "Running tests with coverage..." @mkdir -p $(COVERAGE_DIR) - @$(GOTEST) -v -count=1 -p=1 -coverprofile=$(COVERAGE_DIR)/coverage.txt -covermode=atomic ./client + @$(GOTEST) -v -count=1 -p=1 -coverprofile=$(COVERAGE_DIR)/coverage.txt -covermode=atomic $(TEST_ARGS) .PHONY: test-ginkgo test-ginkgo: deps ## Run tests using Ginkgo From fd3d5711e79b2022c0310298aba0809ea664dc32 Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Wed, 5 Nov 2025 09:55:25 -0800 Subject: [PATCH 20/21] chore: clean go mod --- go.mod | 3 --- 1 file changed, 3 deletions(-) diff --git a/go.mod b/go.mod index 662eb75..0a4d065 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,3 @@ require ( golang.org/x/text v0.30.0 // indirect golang.org/x/tools v0.37.0 // indirect ) - -// Using specific commit from grantdfoster/cogito fork -replace github.com/mudler/cogito => github.com/grantdfoster/cogito v0.0.0-20251104184711-4b65f4d0640a From 19fd2974ddfcf13ad72c7bb49d27027e8e7aa4db Mon Sep 17 00:00:00 2001 From: Grant Foster Date: Wed, 5 Nov 2025 10:07:50 -0800 Subject: [PATCH 21/21] fix: tests --- config/config_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index 1a5b42f..270dc98 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -27,10 +27,6 @@ var _ = Describe("Config", func() { Equal("https://data.gopher-ai.com/api"), // default Equal("https://gopher-data.dev.masalabs.ai/api"), // dev from .env )) - Expect(cfg.Token).To(SatisfyAny( - Equal(""), // default - Equal("rv53jbsoZieW4jOUdArHPmYGxlOiz4UvTyZv2h5bkbSjTU7J"), // dev from .env - )) }) })