Compare commits

..

6 Commits

Author SHA1 Message Date
1c3ea7d1f1 Refactor Knowledge struct into shared package
Moved the Knowledge struct and related types to the shared package, updating all references across the codebase. This improves modularity and enables better reuse of the Knowledge type across different components.
2025-05-03 22:09:02 -04:00
d2b9eb350e Refactor console agents for improved modularity and configuration
Consolidated and refactored console agent logic into a streamlined structure with better configuration capabilities via `ConsoleConfig`. Improved code reusability and readability by converting nested structures and functions, and introduced more modular execution types for handling commands and history tracking.
2025-05-03 22:06:32 -04:00
4c5922d571 Merge remote-tracking branch 'origin/main' 2025-05-03 21:52:04 -04:00
7834943cb6 Update go-llm module version in go.mod and go.sum 2025-05-03 21:51:54 -04:00
8ae16cb633 Update go-llm module version in go.mod and go.sum 2025-05-03 05:55:09 -04:00
24f248d900 Update go-llm module version in go.mod and go.sum 2025-05-03 05:36:37 -04:00
14 changed files with 322 additions and 267 deletions

View File

@@ -9,6 +9,7 @@ import (
"time"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/console"
@@ -102,7 +103,7 @@ func main() {
c := console.Agent{
Model: m,
OnDone: func(ctx context.Context, knowledge agents.Knowledge) error { return nil },
OnDone: func(ctx context.Context, knowledge shared.Knowledge) error { return nil },
OnCommandStart: func(ctx context.Context, command string) error {
slog.Info("command", "command", command)
return nil

27
go.mod
View File

@@ -8,14 +8,14 @@ replace github.com/rocketlaunchr/google-search => github.com/chrisjoyce911/googl
require (
gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250318064250-39453288ce2a
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412190744-39ffb8223775
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250502021123-e9baf7910e00
github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d
github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275
github.com/asticode/go-astisub v0.34.0
github.com/davecgh/go-spew v1.1.1
github.com/docker/docker v28.0.2+incompatible
github.com/joho/godotenv v1.5.1
github.com/lrstanley/go-ytdlp v0.0.0-20250401014907-da1707e4fb85
github.com/lrstanley/go-ytdlp v0.0.0-20250501010938-80d02fe36936
github.com/opencontainers/image-spec v1.1.1
github.com/playwright-community/playwright-go v0.5101.0
github.com/rocketlaunchr/google-search v1.1.6
@@ -24,12 +24,12 @@ require (
)
require (
cloud.google.com/go v0.120.0 // indirect
cloud.google.com/go/ai v0.10.1 // indirect
cloud.google.com/go/auth v0.15.0 // indirect
cloud.google.com/go v0.121.0 // indirect
cloud.google.com/go/ai v0.11.0 // indirect
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/longrunning v0.6.6 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.2.0 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
@@ -42,7 +42,7 @@ require (
github.com/asticode/go-astits v1.13.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
@@ -63,7 +63,7 @@ require (
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/generative-ai-go v0.19.0 // indirect
github.com/google/generative-ai-go v0.20.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
@@ -76,13 +76,12 @@ require (
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/openai/openai-go v0.1.0-beta.9 // indirect
github.com/openai/openai-go v0.1.0-beta.10 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/sashabaranov/go-openai v1.38.1 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/temoto/robotstxt v1.1.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
@@ -103,11 +102,11 @@ require (
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/api v0.228.0 // indirect
google.golang.org/api v0.231.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a // indirect
google.golang.org/grpc v1.71.1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
google.golang.org/grpc v1.72.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gotest.tools/v3 v3.5.2 // indirect
)

86
go.sum
View File

@@ -1,28 +1,20 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
cloud.google.com/go/ai v0.10.1 h1:EU93KqYmMeOKgaBXAz2DshH2C/BzAT1P+iJORksLIic=
cloud.google.com/go/ai v0.10.1/go.mod h1:sWWHZvmJ83BjuxAQtYEiA0SFTpijtbH+SXWFO14ri5A=
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg=
cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q=
cloud.google.com/go/ai v0.11.0 h1:e0uK//+NCv70DBz8qJ0zRW90OP5kQjt27CjbHl3SG2A=
cloud.google.com/go/ai v0.11.0/go.mod h1:bmTa0Ir0VJzDEv841PbAlQlJ/Jxv8Ab5L6LR7Z1Q/QE=
cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250318064250-39453288ce2a h1:LZriHuPVjdus7Haz+qEFYgr+g/eOdmeAvlbgk67DDHA=
gitea.stevedudenhoeffer.com/steve/go-extractor v0.0.0-20250318064250-39453288ce2a/go.mod h1:fzvvUfN8ej2u1ruCsABG+D+2dAPfOklInS4b1pvog1M=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250407055702-5ba0d5df7e96 h1:kT0pwH+q9i4TcFSRems8UFgaKCO94bCzLCf0IgAj6qw=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250407055702-5ba0d5df7e96/go.mod h1:Puz2eDyIwyQLKFt20BU9eRrfkUpBFo+ZX+PtTI64XSo=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250408003321-2ae583e9f360 h1:eZ8CZ1o4ZaciaDL0B/6tYwIERFZ94tNQtG7NKdb7cEQ=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250408003321-2ae583e9f360/go.mod h1:Puz2eDyIwyQLKFt20BU9eRrfkUpBFo+ZX+PtTI64XSo=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412062040-3093b988f80a h1:SH2fWzDtv2KeWiwW+wtPr4NLI3CwdCSoYKn3FCRDdZc=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412062040-3093b988f80a/go.mod h1:RPbuI2VSwQJArwr4tdqmu+fEKlhpro5Cqtq6aC4Cp1w=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412074148-916f07be1840 h1:Yf33CXaCYwBG6AQxQAiK5MrdCQrRrf+Y0tzSfuXPb30=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412074148-916f07be1840/go.mod h1:RPbuI2VSwQJArwr4tdqmu+fEKlhpro5Cqtq6aC4Cp1w=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412190744-39ffb8223775 h1:KF6HdT7A5fqDnEWRjoYCm2mm5booxd+YD6j0wJkh+GU=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250412190744-39ffb8223775/go.mod h1:RPbuI2VSwQJArwr4tdqmu+fEKlhpro5Cqtq6aC4Cp1w=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250502021123-e9baf7910e00 h1:yPVQZG4xdENkWZi/9OLQcFSQb603ftWUTRct51Q64xc=
gitea.stevedudenhoeffer.com/steve/go-llm v0.0.0-20250502021123-e9baf7910e00/go.mod h1:RPbuI2VSwQJArwr4tdqmu+fEKlhpro5Cqtq6aC4Cp1w=
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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -31,15 +23,11 @@ github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d h1:dxGZ0drmrUfNO
github.com/Edw590/go-wolfram v0.0.0-20241010091529-fb9031908c5d/go.mod h1:ubjYqrt3dF4G+YVEDQr+qa2aveeMzt27o/GOH2hswPo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/PuerkitoBio/goquery v1.4.1/go.mod h1:T9ezsOHcCrDCgA8aF1Cqr3sSYbO/xgdy8/R/XiIMAhA=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275 h1:Kuhf+w+ilOGoXaR4O4nZ6Dp+ZS83LdANUjwyMXsPGX4=
@@ -83,15 +71,13 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
github.com/chrisjoyce911/google-search v0.0.0-20230910003754-e501aedf805a h1:OZQiBySVd55npXVsIKnJT6q+9A1tPiXhGnFlc+q0YqQ=
github.com/chrisjoyce911/google-search v0.0.0-20230910003754-e501aedf805a/go.mod h1:fk5J/qPpaRDjLWdFxT+dmuiqG7kxXArC7K8A+gj88Nk=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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=
@@ -159,8 +145,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ=
github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -192,8 +178,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/liushuangls/go-anthropic/v2 v2.15.0 h1:zpplg7BRV/9FlMmeMPI0eDwhViB0l9SkNrF8ErYlRoQ=
github.com/liushuangls/go-anthropic/v2 v2.15.0/go.mod h1:kq2yW3JVy1/rph8u5KzX7F3q95CEpCT2RXp/2nfCmb4=
github.com/lrstanley/go-ytdlp v0.0.0-20250401014907-da1707e4fb85 h1:fgU9HcQ95uG9vqkYP/YW/H6DhwsmkXHriuMM27bwpYU=
github.com/lrstanley/go-ytdlp v0.0.0-20250401014907-da1707e4fb85/go.mod h1:HpxGaeaOpXVUPxUUmj8Izr3helrDGN90haPtmpY5xzA=
github.com/lrstanley/go-ytdlp v0.0.0-20250501010938-80d02fe36936 h1:hYa4l1wvSl9OHHgfNemq8I/L1iyWl2KGVp43GMwTGzQ=
github.com/lrstanley/go-ytdlp v0.0.0-20250501010938-80d02fe36936/go.mod h1:HpxGaeaOpXVUPxUUmj8Izr3helrDGN90haPtmpY5xzA=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
@@ -210,12 +196,8 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/openai/openai-go v0.1.0-beta.6 h1:JquYDpprfrGnlKvQQg+apy9dQ8R9mIrm+wNvAPp6jCQ=
github.com/openai/openai-go v0.1.0-beta.6/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/openai/openai-go v0.1.0-beta.7 h1:ykC09BCIgdXL69wE/8NUjL2rCdAbo9kL3AjnGR6H91o=
github.com/openai/openai-go v0.1.0-beta.7/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/openai/openai-go v0.1.0-beta.9 h1:ABpubc5yU/3ejee2GgRrbFta81SG/d7bQbB8mIdP0Xo=
github.com/openai/openai-go v0.1.0-beta.9/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/openai/openai-go v0.1.0-beta.10 h1:CknhGXe8aXQMRuqg255PFnWzgRY9nEryMxoNIBBM9tU=
github.com/openai/openai-go v0.1.0-beta.10/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
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=
@@ -224,8 +206,6 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/playwright-community/playwright-go v0.5001.0 h1:EY3oB+rU9cUp6CLHguWE8VMZTwAg+83Yyb7dQqEmGLg=
github.com/playwright-community/playwright-go v0.5001.0/go.mod h1:kBNWs/w2aJ2ZUp1wEOOFLXgOqvppFngM5OS+qyhl+ZM=
github.com/playwright-community/playwright-go v0.5101.0 h1:gVCMZThDO76LJ/aCI27lpB8hEAWhZszeS0YB+oTxJp0=
github.com/playwright-community/playwright-go v0.5101.0/go.mod h1:kBNWs/w2aJ2ZUp1wEOOFLXgOqvppFngM5OS+qyhl+ZM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -235,15 +215,11 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=
github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
@@ -359,8 +335,6 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -447,8 +421,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q=
google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
@@ -458,23 +432,15 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0 h1:Qbb5RVn5xzI4naMJSpJ7lhvmos6UwZkbekd5Uz7rt9E=
google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:6T35kB3IPpdw7Wul09by0G/JuOuIFkXV6OOvt8IZeT8=
google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 h1:AMLTAunltONNuzWgVPZXrjLWtXpsG6A3yLLPEoJ/IjU=
google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac=
google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a h1:OQ7sHVzkx6L57dQpzUS4ckfWJ51KDH74XHTDe23xWAs=
google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 h1:0K7wTWyzxZ7J+L47+LbFogJW1nn/gnnMCN0vGXNYtTI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755 h1:TwXJCGVREgQ/cl18iY0Z4wJCTL/GmW+Um2oSwZiZPnc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a h1:GIqLhp/cYUkuGuiT+vJk8vhOP86L4+SP5j8yXgeVpvI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View File

@@ -12,21 +12,23 @@ import (
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type Agent struct {
agents.Agent
// Model is the chat completion model to use
Model gollm.ChatCompletion
OnLoopComplete func(ctx context.Context, knowledge agents.Knowledge) error
OnLoopComplete func(ctx context.Context, knowledge shared.Knowledge) error
OnCommandStart func(ctx context.Context, command string) error
OnCommandDone func(ctx context.Context, command string, output string, err error) error
OnDone func(ctx context.Context, knowledge agents.Knowledge) error
OnDone func(ctx context.Context, knowledge shared.Knowledge) error
ContextualInformation []string
@@ -34,8 +36,9 @@ type Agent struct {
}
type Response struct {
Knowledge agents.Knowledge
Directory string
Knowledge shared.Knowledge
DataDir string
OutputDir string
}
// Answer will give the model access to an ubuntu console with python and pip installed, and then ask the model to
@@ -43,25 +46,36 @@ type Response struct {
func (a Agent) Answer(ctx context.Context, questions []string) (Response, error) {
var res Response
a.Agent = agents.NewAgent(a.Model, gollm.NewToolBox()).WithMaxCalls(200)
if a.MaxCommands <= 0 {
a.MaxCommands = 10000
a.MaxCommands = 20 // Default to 20 commands as per requirements
}
res.Knowledge = agents.Knowledge{
res.Knowledge = shared.Knowledge{
OriginalQuestions: questions,
RemainingQuestions: questions,
}
// create a temporary scratch directory
dir, err := os.MkdirTemp("", "console-")
// create temporary directories for data and output
dataDir, err := os.MkdirTemp("", "console-data-")
if err != nil {
return res, err
}
res.Directory = dir
outputDir, err := os.MkdirTemp("", "console-output-")
if err != nil {
os.RemoveAll(dataDir)
return res, err
}
res.DataDir = dataDir
res.OutputDir = outputDir
cl, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
os.RemoveAll(dataDir)
os.RemoveAll(outputDir)
return res, err
}
defer cl.Close()
@@ -69,8 +83,13 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
mounts := []mount.Mount{
{
Type: mount.TypeBind,
Source: dir,
Target: "/home/user",
Source: dataDir,
Target: "/data",
},
{
Type: mount.TypeBind,
Source: outputDir,
Target: "/output",
},
}
@@ -79,25 +98,29 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
Image: "ubuntu:latest",
Cmd: []string{"tail", "-f", "/dev/null"},
Tty: true,
WorkingDir: "/home/user",
WorkingDir: "/data",
},
HostConfig: &container.HostConfig{
AutoRemove: true,
Mounts: mounts,
},
Name: filepath.Base(dir),
Name: filepath.Base(dataDir),
})
if err != nil {
os.RemoveAll(dataDir)
os.RemoveAll(outputDir)
return res, fmt.Errorf("failed to create container: %w", err)
}
defer func() {
_ = c.Close(ctx)
}()
slog.Info("starting container", "dir", dir, "container", fmt.Sprintf("%+v", c))
slog.Info("starting container", "dataDir", dataDir, "outputDir", outputDir, "container", fmt.Sprintf("%+v", c))
err = c.Start(ctx)
if err != nil {
os.RemoveAll(dataDir)
os.RemoveAll(outputDir)
return res, fmt.Errorf("failed to start container: %w", err)
}
@@ -106,12 +129,17 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
var history executions
var keepGoing = true
opwd, epwd := c.Execute(ctx, "ls -al /home")
// Initial setup - install basic tools
setupCmd, setupErr := c.Sudo(ctx, "apt-get update && apt-get install -y curl wget git python3 python3-pip")
if setupErr == nil {
history = append(history, execution{
Command: "sudo apt-get update && apt-get install -y curl wget git python3 python3-pip",
Output: setupCmd,
WhatILearned: []string{"Basic tools installed: curl, wget, git, python3, pip"},
})
}
fmt.Println(opwd)
slog.Info("pwd", "pwd", opwd, "epwd", epwd)
tools := map[string]*gollm.Function{
tools := map[string]gollm.Function{
"exit": gollm.NewFunction(
"exit",
"exit the container",
@@ -122,18 +150,24 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
return "exiting", nil
}),
"write": gollm.NewFunction(
"write",
"write a file in the /root directory",
"write_data": gollm.NewFunction(
"write_data",
"write a file in the /data directory",
func(ctx *gollm.Context, args struct {
Filename string `description:"The name of the file to write"`
Content string `description:"The content of the file to write"`
}) (any, error) {
target, err := SafeJoinPath(dir, args.Filename)
target, err := SafeJoinPath(dataDir, args.Filename)
if err != nil {
return "", err
}
// Ensure directory exists
dir := filepath.Dir(target)
if err := os.MkdirAll(dir, 0755); err != nil {
return "", err
}
f, err := os.Create(target)
if err != nil {
return "", err
@@ -145,16 +179,67 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
return "", err
}
return "wrote file", nil
return "wrote file to /data/" + args.Filename, nil
}),
"read": gollm.NewFunction(
"read",
"read a file in the /root directory",
"write_output": gollm.NewFunction(
"write_output",
"write a file in the /output directory (files here will be available after the agent completes)",
func(ctx *gollm.Context, args struct {
Filename string `description:"The name of the file to write"`
Content string `description:"The content of the file to write"`
}) (any, error) {
target, err := SafeJoinPath(outputDir, args.Filename)
if err != nil {
return "", err
}
// Ensure directory exists
dir := filepath.Dir(target)
if err := os.MkdirAll(dir, 0755); err != nil {
return "", err
}
f, err := os.Create(target)
if err != nil {
return "", err
}
defer f.Close()
_, err = f.WriteString(args.Content)
if err != nil {
return "", err
}
return "wrote file to /output/" + args.Filename, nil
}),
"read_data": gollm.NewFunction(
"read_data",
"read a file from the /data directory",
func(ctx *gollm.Context, args struct {
Filename string `description:"The name of the file to read"`
}) (any, error) {
target, err := SafeJoinPath(dir, args.Filename)
target, err := SafeJoinPath(dataDir, args.Filename)
if err != nil {
return "", err
}
b, err := os.ReadFile(target)
if err != nil {
return "", err
}
return string(b), nil
}),
"read_output": gollm.NewFunction(
"read_output",
"read a file from the /output directory",
func(ctx *gollm.Context, args struct {
Filename string `description:"The name of the file to read"`
}) (any, error) {
target, err := SafeJoinPath(outputDir, args.Filename)
if err != nil {
return "", err
}
@@ -172,9 +257,11 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
"execute a command in the container",
func(ctx *gollm.Context, args struct {
Command string `description:"The command to execute"`
Learn []string `description:"What you learned from this command (optional)"`
ToLearn []string `description:"What you still need to learn (optional)"`
}) (any, error) {
if len(history) >= a.MaxCommands {
return "too many commands", nil
return "Command limit reached. You've used all " + fmt.Sprintf("%d", a.MaxCommands) + " allowed commands.", nil
}
if a.OnCommandStart != nil {
@@ -202,69 +289,77 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
history = append(history, execution{
Command: args.Command,
Output: res,
WhatILearned: args.Learn,
WhatIStillNeedToLearn: args.ToLearn,
})
return res, nil
}),
"sudo": gollm.NewFunction(
"sudo",
"execute a command in the container",
func(ctx *gollm.Context, args struct {
Command string `description:"The command to execute"`
}) (any, error) {
if len(history) >= a.MaxCommands {
return "too many commands", nil
"summarize_knowledge": gollm.NewFunction(
"summarize_knowledge",
"summarize what you've learned so far",
func(ctx *gollm.Context, args struct{}) (any, error) {
var learned []string
var toLearn []string
for _, exec := range history {
learned = append(learned, exec.WhatILearned...)
toLearn = append(toLearn, exec.WhatIStillNeedToLearn...)
}
if a.OnCommandStart != nil {
err := a.OnCommandStart(ctx, args.Command)
if err != nil {
return "", err
}
summary := "Knowledge Summary:\n"
if len(learned) > 0 {
summary += "What I've learned:\n- " + strings.Join(learned, "\n- ") + "\n\n"
} else {
summary += "I haven't learned anything specific yet.\n\n"
}
res, err := c.Sudo(ctx, args.Command)
if a.OnCommandDone != nil {
err = a.OnCommandDone(ctx, args.Command, res, nil)
if err != nil {
return "", err
}
if len(toLearn) > 0 {
summary += "What I still need to learn:\n- " + strings.Join(toLearn, "\n- ")
} else {
summary += "I don't have any specific learning goals at the moment."
}
if err != nil {
res = "error executing: " + err.Error()
}
history = append(history, execution{
Command: "sudo " + args.Command,
Output: res,
})
return res, nil
return summary, nil
}),
}
for i := 0; i < a.MaxCommands && len(history) < a.MaxCommands && keepGoing; i++ {
systemPrompt := `You are now in a shell in a container of the ubuntu:latest image to answer a question asked by the user, it is very basic install of ubuntu, simple things (like python) are not preinstalled but can be installed via apt. You will be run multiple times and gain knowledge throughout the process.`
systemPrompt := `You are now in a shell in a container of the ubuntu:latest image to answer a question asked by the user.
You have full control over a bash shell inside this Docker container.
Important directories:
- /data: A temporary directory with the lifespan of your processing. Use this for working files.
- /output: Files placed here will be returned to the caller after you're done. Use this for final results.
You can execute up to ` + fmt.Sprintf("%d", a.MaxCommands) + ` commands total. You're currently on command ` + fmt.Sprintf("%d", len(history)+1) + ` of ` + fmt.Sprintf("%d", a.MaxCommands) + `.
For each command, you should:
1. Think about what you need to learn
2. Execute the command using the "execute" function
3. Analyze the output and record what you learned
4. Plan your next command based on this knowledge
You can write files directly to /data or /output using the write_data and write_output functions.
When you are done, use "exit" to exit the container.`
var toolbox []gollm.Function
// Add all tools
toolbox = append(toolbox,
tools["exit"],
tools["write_data"],
tools["write_output"],
tools["read_data"],
tools["read_output"],
tools["summarize_knowledge"],
)
// Only add execute if we haven't reached the command limit
if len(history) < a.MaxCommands {
systemPrompt += `You can run any command you like to get to the needed results.`
}
systemPrompt += `Alternatively, you can use the tool "write" to write a file in the home directory, and also the tool "read" to read a file in the home directory.
When you are done, please use "exit" to exit the container.
Respond with any number of commands to answer the question, they will be executed in order.`
var toolbox []*gollm.Function
// add unrestricted tools
toolbox = append(toolbox, tools["exit"], tools["write"], tools["read"])
if len(history) < a.MaxCommands {
toolbox = append(toolbox, tools["execute"], tools["sudo"])
toolbox = append(toolbox, tools["execute"])
}
kw := shared.KnowledgeWorker{
@@ -291,7 +386,7 @@ Respond with any number of commands to answer the question, they will be execute
slog.Info("answered question and learned nothing")
}
res.Knowledge, err = agents.KnowledgeIntegrate(ctx, a.Model, res.Knowledge, r)
res.Knowledge, err = a.KnowledgeIntegrate(ctx, res.Knowledge, r)
if err != nil {
return res, fmt.Errorf("error integrating knowledge: %w", err)
}

View File

@@ -1,4 +1,4 @@
package console
package agents
import (
"context"
@@ -8,46 +8,29 @@ import (
"path/filepath"
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/console"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type Agent struct {
// Model is the chat completion model to use
Model gollm.ChatCompletion
OnLoopComplete func(ctx context.Context, knowledge agents.Knowledge) error
type ConsoleConfig struct {
// MaxCommands is how many total commands this console can run.
MaxCommands int `json:"max_commands"`
OnCommandStart func(ctx context.Context, command string) error
OnCommandDone func(ctx context.Context, command string, output string, err error) error
OnDone func(ctx context.Context, knowledge agents.Knowledge) error
ContextualInformation []string
MaxCommands int
}
type Response struct {
Knowledge agents.Knowledge
Directory string
func (a Agent) Console(ctx context.Context, questions []string, cfg ConsoleConfig) (shared.Knowledge, string, error) {
if cfg.MaxCommands <= 0 {
cfg.MaxCommands = 10000
}
// Answer will give the model access to an ubuntu console with python and pip installed, and then ask the model to
// do what is necessary to answer the question.
func (a Agent) Answer(ctx context.Context, questions []string) (Response, error) {
var res Response
if a.MaxCommands <= 0 {
a.MaxCommands = 10000
}
res.Knowledge = agents.Knowledge{
var resKnowledge = shared.Knowledge{
OriginalQuestions: questions,
RemainingQuestions: questions,
}
@@ -55,14 +38,14 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
// create a temporary scratch directory
dir, err := os.MkdirTemp("", "console-")
if err != nil {
return res, err
return resKnowledge, "", err
}
res.Directory = dir
var resDirectory = dir
cl, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return res, err
return resKnowledge, resDirectory, err
}
defer cl.Close()
@@ -74,7 +57,7 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
},
}
c, err := CreateContainer(ctx, cl, ContainerConfig{
c, err := console.CreateContainer(ctx, cl, console.ContainerConfig{
Config: &container.Config{
Image: "ubuntu:latest",
Cmd: []string{"tail", "-f", "/dev/null"},
@@ -89,7 +72,7 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
})
if err != nil {
return res, fmt.Errorf("failed to create container: %w", err)
return resKnowledge, resDirectory, fmt.Errorf("failed to create container: %w", err)
}
defer func() {
_ = c.Close(ctx)
@@ -98,12 +81,12 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
slog.Info("starting container", "dir", dir, "container", fmt.Sprintf("%+v", c))
err = c.Start(ctx)
if err != nil {
return res, fmt.Errorf("failed to start container: %w", err)
return resKnowledge, resDirectory, fmt.Errorf("failed to start container: %w", err)
}
// Run the model
var history executions
var history console.Executions
var keepGoing = true
opwd, epwd := c.Execute(ctx, "ls -al /home")
@@ -111,7 +94,7 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
fmt.Println(opwd)
slog.Info("pwd", "pwd", opwd, "epwd", epwd)
tools := map[string]*gollm.Function{
tools := map[string]gollm.Function{
"exit": gollm.NewFunction(
"exit",
"exit the container",
@@ -129,7 +112,7 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
Filename string `description:"The name of the file to write"`
Content string `description:"The content of the file to write"`
}) (any, error) {
target, err := SafeJoinPath(dir, args.Filename)
target, err := console.SafeJoinPath(dir, args.Filename)
if err != nil {
return "", err
}
@@ -154,7 +137,7 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
func(ctx *gollm.Context, args struct {
Filename string `description:"The name of the file to read"`
}) (any, error) {
target, err := SafeJoinPath(dir, args.Filename)
target, err := console.SafeJoinPath(dir, args.Filename)
if err != nil {
return "", err
}
@@ -173,12 +156,12 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
func(ctx *gollm.Context, args struct {
Command string `description:"The command to execute"`
}) (any, error) {
if len(history) >= a.MaxCommands {
if len(history) >= cfg.MaxCommands {
return "too many commands", nil
}
if a.OnCommandStart != nil {
err := a.OnCommandStart(ctx, args.Command)
if cfg.OnCommandStart != nil {
err := cfg.OnCommandStart(ctx, args.Command)
if err != nil {
return "", err
}
@@ -192,14 +175,14 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
res, err = c.Execute(ctx, args.Command)
}
if a.OnCommandDone != nil {
err = a.OnCommandDone(ctx, args.Command, res, nil)
if cfg.OnCommandDone != nil {
err = cfg.OnCommandDone(ctx, args.Command, res, nil)
if err != nil {
return "", err
}
}
history = append(history, execution{
history = append(history, console.Execution{
Command: args.Command,
Output: res,
})
@@ -213,12 +196,12 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
func(ctx *gollm.Context, args struct {
Command string `description:"The command to execute"`
}) (any, error) {
if len(history) >= a.MaxCommands {
if len(history) >= cfg.MaxCommands {
return "too many commands", nil
}
if a.OnCommandStart != nil {
err := a.OnCommandStart(ctx, args.Command)
if cfg.OnCommandStart != nil {
err := cfg.OnCommandStart(ctx, args.Command)
if err != nil {
return "", err
}
@@ -226,8 +209,8 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
res, err := c.Sudo(ctx, args.Command)
if a.OnCommandDone != nil {
err = a.OnCommandDone(ctx, args.Command, res, nil)
if cfg.OnCommandDone != nil {
err = cfg.OnCommandDone(ctx, args.Command, res, nil)
if err != nil {
return "", err
}
@@ -237,7 +220,7 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
res = "error executing: " + err.Error()
}
history = append(history, execution{
history = append(history, console.Execution{
Command: "sudo " + args.Command,
Output: res,
})
@@ -246,11 +229,11 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
}),
}
for i := 0; i < a.MaxCommands && len(history) < a.MaxCommands && keepGoing; i++ {
for i := 0; i < cfg.MaxCommands && len(history) < cfg.MaxCommands && keepGoing; i++ {
systemPrompt := `You are now in a shell in a container of the ubuntu:latest image to answer a question asked by the user, it is very basic install of ubuntu, simple things (like python) are not preinstalled but can be installed via apt. You will be run multiple times and gain knowledge throughout the process.`
if len(history) < a.MaxCommands {
if len(history) < cfg.MaxCommands {
systemPrompt += `You can run any command you like to get to the needed results.`
}
@@ -258,19 +241,19 @@ func (a Agent) Answer(ctx context.Context, questions []string) (Response, error)
When you are done, please use "exit" to exit the container.
Respond with any number of commands to answer the question, they will be executed in order.`
var toolbox []*gollm.Function
var toolbox []gollm.Function
// add unrestricted tools
toolbox = append(toolbox, tools["exit"], tools["write"], tools["read"])
if len(history) < a.MaxCommands {
if len(history) < cfg.MaxCommands {
toolbox = append(toolbox, tools["execute"], tools["sudo"])
}
kw := shared.KnowledgeWorker{
Model: a.Model,
Model: a.model,
ToolBox: gollm.NewToolBox(toolbox...),
ContextualInformation: a.ContextualInformation,
ContextualInformation: a.contextualInformation,
OnNewFunction: func(ctx context.Context, funcName string, args string) (any, error) {
slog.Info("new function called", "function name", funcName, "args", args)
return nil, nil
@@ -278,11 +261,11 @@ Respond with any number of commands to answer the question, they will be execute
}
slog.Info("answering question", "question", questions[0])
r, err := kw.Answer(ctx, &res.Knowledge, systemPrompt, "", "", history.ToGeneralButLastMessageHistory(), func(res gollm.ToolCallResponse) {
r, err := kw.Answer(ctx, &resKnowledge, systemPrompt, "", "", history.ToGeneralButLastMessageHistory(), func(res gollm.ToolCallResponse) {
})
if err != nil {
return res, fmt.Errorf("error answering question: %w", err)
return resKnowledge, resDirectory, fmt.Errorf("error answering question: %w", err)
}
if len(r.Knowledge) > 0 {
@@ -291,12 +274,12 @@ Respond with any number of commands to answer the question, they will be execute
slog.Info("answered question and learned nothing")
}
res.Knowledge, err = agents.KnowledgeIntegrate(ctx, a.Model, res.Knowledge, r)
resKnowledge, err = a.KnowledgeIntegrate(ctx, resKnowledge, r)
if err != nil {
return res, fmt.Errorf("error integrating knowledge: %w", err)
return resKnowledge, resDirectory, fmt.Errorf("error integrating knowledge: %w", err)
}
slog.Info("knowledge integrated", "question", questions[0], "knowledge", res.Knowledge)
slog.Info("knowledge integrated", "question", questions[0], "knowledge", resKnowledge)
}
return res, nil
return resKnowledge, resDirectory, nil
}

View File

@@ -6,7 +6,7 @@ import (
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type execution struct {
type Execution struct {
Command string
Output string
WhatILearned []string
@@ -16,7 +16,7 @@ type execution struct {
const kMaxLenCommandSummary = 200
const kMaxLenCommandOutputSummary = 200
func (e execution) ToGeneralMessageHistory() gollm.Message {
func (e Execution) ToGeneralMessageHistory() gollm.Message {
if len(e.Command) > kMaxLenCommandSummary {
e.Command = e.Command[:kMaxLenCommandSummary] + "... (truncated)"
}
@@ -33,7 +33,7 @@ func (e execution) ToGeneralMessageHistory() gollm.Message {
}
}
func (e execution) ToDetailedMessageHistory() gollm.Message {
func (e Execution) ToDetailedMessageHistory() gollm.Message {
prompt := "$ "
if strings.HasPrefix(e.Command, "sudo ") {
prompt = "# "
@@ -60,9 +60,9 @@ func (e execution) ToDetailedMessageHistory() gollm.Message {
}
}
type executions []execution
type Executions []Execution
func (e executions) ToGeneralMessageHistory() []gollm.Message {
func (e Executions) ToGeneralMessageHistory() []gollm.Message {
var messages []gollm.Message
for _, v := range e {
@@ -72,7 +72,7 @@ func (e executions) ToGeneralMessageHistory() []gollm.Message {
return messages
}
func (e executions) ToGeneralButLastMessageHistory() []gollm.Message {
func (e Executions) ToGeneralButLastMessageHistory() []gollm.Message {
var messages []gollm.Message
for i, v := range e {
@@ -86,7 +86,7 @@ func (e executions) ToGeneralButLastMessageHistory() []gollm.Message {
return messages
}
func (e executions) ToDetailedMessageHistory() []gollm.Message {
func (e Executions) ToDetailedMessageHistory() []gollm.Message {
var messages []gollm.Message
for _, v := range e {

View File

@@ -5,6 +5,8 @@ import (
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
)
// ExtractKnowledge will take a knowledge object and use the gained knowledge to extract the knowledge relevant to the
@@ -16,16 +18,16 @@ import (
// contextualInformation is any contextual information that should be provided to the model.
// It will return the knowledge extracted from the sourceData along with any remaining questions.
// This agent call will not use the Agent's system prompts, but will instead form its own. The contextual information will be used.
func (a Agent) ExtractKnowledge(ctx context.Context, sourceData string, source string, questions []string) (Knowledge, error) {
func (a Agent) ExtractKnowledge(ctx context.Context, sourceData string, source string, questions []string) (shared.Knowledge, error) {
var knowledge Knowledge
var knowledge shared.Knowledge
fnAnswer := gollm.NewFunction(
"learn",
`Use learn to pass some relevant information to the model. The model will use this information to answer the question. Use it to learn relevant information from the text. Keep these concise and relevant to the question.`,
func(ctx *gollm.Context, args struct {
Info string `description:"The information to learn from the text."`
}) (any, error) {
knowledge.Knowledge = append(knowledge.Knowledge, TidBit{Info: args.Info, Source: source})
knowledge.Knowledge = append(knowledge.Knowledge, shared.TidBit{Info: args.Info, Source: source})
return "", nil
})

View File

@@ -4,12 +4,14 @@ import (
"context"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
)
// KnowledgeIntegrate will ask the LLM to combine the gained knowledge with the current knowledge, and return the new representation of overall.
// If source is not empty, then any new Knowledge will an empty source will be given the source.
// This will override objectives, notes, and remaining questions.
func (a Agent) KnowledgeIntegrate(ctx context.Context, base Knowledge, in ...Knowledge) (Knowledge, error) {
func (a Agent) KnowledgeIntegrate(ctx context.Context, base shared.Knowledge, in ...shared.Knowledge) (shared.Knowledge, error) {
// if there are no changes we can just return the knowledge
if len(in) == 0 {
return base, nil
@@ -29,7 +31,7 @@ func (a Agent) KnowledgeIntegrate(ctx context.Context, base Knowledge, in ...Kno
}
}
var incoming Knowledge
var incoming shared.Knowledge
for _, k := range in {
incoming.NotesToSelf = append(incoming.NotesToSelf, k.NotesToSelf...)
@@ -43,7 +45,7 @@ func (a Agent) KnowledgeIntegrate(ctx context.Context, base Knowledge, in ...Kno
baseMsg.Text = "The original knowledge is as follows: " + baseMsg.Text
incomingMsg.Text = "The new knowledge is as follows: " + incomingMsg.Text
var result = Knowledge{
var result = shared.Knowledge{
OriginalQuestions: base.OriginalQuestions,
Knowledge: append(base.Knowledge, incoming.Knowledge...),
}

View File

@@ -8,10 +8,12 @@ import (
"strings"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
)
// AnswerQuestionWithKnowledge will take a knowledge object and use the gained knowledge to answer a question.
func (a Agent) AnswerQuestionWithKnowledge(ctx context.Context, knowledge Knowledge) (string, error) {
func (a Agent) AnswerQuestionWithKnowledge(ctx context.Context, knowledge shared.Knowledge) (string, error) {
originalQuestions := strings.Join(knowledge.OriginalQuestions, "\n")
infoGained := ""

View File

@@ -3,19 +3,21 @@ package agents
import (
"context"
"fmt"
"net/url"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/cache"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/extractor"
"net/url"
)
func (a Agent) ReadPage(ctx context.Context, u *url.URL, questions []string) (Knowledge, error) {
func (a Agent) ReadPage(ctx context.Context, u *url.URL, questions []string) (shared.Knowledge, error) {
ar, err := extractArticle(ctx, u)
if err != nil {
return Knowledge{}, err
return shared.Knowledge{}, err
}
if ar.Body == "" {
return Knowledge{}, fmt.Errorf("could not extract body from page")
return shared.Knowledge{}, fmt.Errorf("could not extract body from page")
}
return a.ExtractKnowledge(ctx, ar.Body, u.String(), questions)

View File

@@ -14,6 +14,8 @@ import (
"gitea.stevedudenhoeffer.com/steve/go-extractor"
"gitea.stevedudenhoeffer.com/steve/go-extractor/sites/duckduckgo"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
)
func deferClose(c io.Closer) {
@@ -26,7 +28,7 @@ func deferClose(c io.Closer) {
type SearchTool struct {
Name string
Description string
Function func(ctx context.Context, src *url.URL, questions []string) (Knowledge, error)
Function func(ctx context.Context, src *url.URL, questions []string) (shared.Knowledge, error)
}
// SearchAndUseTools will search duckduckgo for the given question, and then ask the LLM to select a search result to
@@ -41,8 +43,8 @@ type SearchTool struct {
// will be combined and returned.
// messages will be appended to all search results. The types of messages that can be appended are both string and
// gollm.Message.
func (a Agent) SearchAndUseTools(ctx context.Context, searchQuery string, questions []string, loops int, allowConcurrent bool, maxReads int, tools []SearchTool, messages ...any) (Knowledge, error) {
var knowledge = Knowledge{
func (a Agent) SearchAndUseTools(ctx context.Context, searchQuery string, questions []string, loops int, allowConcurrent bool, maxReads int, tools []SearchTool, messages ...any) (shared.Knowledge, error) {
var knowledge = shared.Knowledge{
OriginalQuestions: questions,
RemainingQuestions: questions,
}
@@ -184,13 +186,13 @@ Use appropriate tools to analyze the search results and determine if they answer
}
slog.Info("search results called and executed", "error", err, "results text", results.Text, "results", results.CallResults)
var learned []Knowledge
var learned []shared.Knowledge
for _, r := range results.CallResults {
if r.Error != nil {
continue
}
if k, ok := r.Result.(Knowledge); ok {
if k, ok := r.Result.(shared.Knowledge); ok {
learned = append(learned, k)
} else {
slog.Error("result is not knowledge", "result", r.Result)
@@ -210,7 +212,7 @@ Use appropriate tools to analyze the search results and determine if they answer
return knowledge, nil
}
func (a Agent) SearchAndRead(ctx context.Context, searchQuery string, questions []string, allowConcurrent bool, maxReads int) (Knowledge, error) {
func (a Agent) SearchAndRead(ctx context.Context, searchQuery string, questions []string, allowConcurrent bool, maxReads int) (shared.Knowledge, error) {
return a.SearchAndUseTools(ctx, searchQuery, questions, 2, allowConcurrent, maxReads, []SearchTool{
{
Name: "readpage",

View File

@@ -1,4 +1,4 @@
package agents
package shared
import (
"strings"

View File

@@ -5,14 +5,12 @@ import (
"fmt"
"strings"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents"
gollm "gitea.stevedudenhoeffer.com/steve/go-llm"
)
type KnowledgeWorker struct {
Model gollm.ChatCompletion
ToolBox *gollm.ToolBox
ToolBox gollm.ToolBox
ContextualInformation []string
OnNewFunction func(ctx context.Context, funcName string, args string) (any, error)
OnFunctionFinished func(ctx context.Context, funcName string, args string, result any, err error, newFunctionResult any) error
@@ -27,7 +25,7 @@ const DefaultPrompt = `Use the provided tools to answer the questions in your cu
// source is the source of the knowledge, for example a URL.
// Any tool call that returns a Knowledge object will be handled by this function in crafting the final Knowledge object.
// Any other return type will be passed to the resultWorker function, if provided.
func (w KnowledgeWorker) Answer(context context.Context, knowledge *agents.Knowledge, systemPrompt string, userInput string, source string, history []gollm.Message, resultWorker func(res gollm.ToolCallResponse)) (agents.Knowledge, error) {
func (w KnowledgeWorker) Answer(context context.Context, knowledge *Knowledge, systemPrompt string, userInput string, source string, history []gollm.Message, resultWorker func(res gollm.ToolCallResponse)) (Knowledge, error) {
var req gollm.Request
if systemPrompt != "" {
@@ -80,7 +78,7 @@ func (w KnowledgeWorker) Answer(context context.Context, knowledge *agents.Knowl
func(ctx *gollm.Context, args struct {
NotesToSelf []string `description:"Notes to leave for yourself for later."`
}) (any, error) {
return agents.Knowledge{
return Knowledge{
NotesToSelf: args.NotesToSelf,
}, nil
}),
@@ -90,7 +88,7 @@ func (w KnowledgeWorker) Answer(context context.Context, knowledge *agents.Knowl
func(ctx *gollm.Context, args struct {
Objectives []string `description:"The objectives to set for executions going forward."`
}) (any, error) {
return agents.Knowledge{
return Knowledge{
CurrentObjectives: args.Objectives,
}, nil
}),
@@ -100,13 +98,13 @@ func (w KnowledgeWorker) Answer(context context.Context, knowledge *agents.Knowl
func(ctx *gollm.Context, args struct {
Info []string `description:"The information to learn from the input."`
}) (any, error) {
var k []agents.TidBit
var k []TidBit
for _, i := range args.Info {
k = append(k, agents.TidBit{Info: i, Source: source})
k = append(k, TidBit{Info: i, Source: source})
}
return agents.Knowledge{
return Knowledge{
Knowledge: k,
}, nil
})).
@@ -120,17 +118,17 @@ func (w KnowledgeWorker) Answer(context context.Context, knowledge *agents.Knowl
resp, err := w.Model.ChatComplete(context, req)
if err != nil {
return agents.Knowledge{}, fmt.Errorf("error calling model: %w", err)
return Knowledge{}, fmt.Errorf("error calling model: %w", err)
}
if len(resp.Choices) == 0 {
return agents.Knowledge{}, fmt.Errorf("no choices found")
return Knowledge{}, fmt.Errorf("no choices found")
}
choice := resp.Choices[0]
if len(choice.Calls) == 0 {
return agents.Knowledge{}, fmt.Errorf("no calls found")
return Knowledge{}, fmt.Errorf("no calls found")
}
var callNames []string
@@ -141,14 +139,14 @@ func (w KnowledgeWorker) Answer(context context.Context, knowledge *agents.Knowl
results, err := w.ToolBox.ExecuteCallbacks(gollm.NewContext(context, req, &choice, nil), choice.Calls, w.OnNewFunction, w.OnFunctionFinished)
if err != nil {
return agents.Knowledge{}, fmt.Errorf("error executing callbacks: %w", err)
return Knowledge{}, fmt.Errorf("error executing callbacks: %w", err)
}
var res = agents.Knowledge{}
var res = Knowledge{}
for _, r := range results {
switch v := r.Result.(type) {
case agents.Knowledge:
case Knowledge:
res = res.Absorb(v)
default:

View File

@@ -3,25 +3,28 @@ package agents
import (
"context"
"fmt"
"github.com/asticode/go-astisub"
"github.com/lrstanley/go-ytdlp"
"io"
"log/slog"
"net/url"
"os"
"path/filepath"
"github.com/asticode/go-astisub"
"github.com/lrstanley/go-ytdlp"
"gitea.stevedudenhoeffer.com/steve/answer/pkg/agents/shared"
)
func init() {
ytdlp.MustInstall(context.Background(), nil)
}
func (a Agent) ReadYouTubeTranscript(ctx context.Context, u *url.URL, questions []string) (Knowledge, error) {
func (a Agent) ReadYouTubeTranscript(ctx context.Context, u *url.URL, questions []string) (shared.Knowledge, error) {
dlp := ytdlp.New()
tmpDir, err := os.MkdirTemp("", "mort-ytdlp-")
if err != nil {
return Knowledge{}, fmt.Errorf("error creating temp dir: %w", err)
return shared.Knowledge{}, fmt.Errorf("error creating temp dir: %w", err)
}
slog.Info("created temp dir", "path", tmpDir)
@@ -40,15 +43,15 @@ func (a Agent) ReadYouTubeTranscript(ctx context.Context, u *url.URL, questions
res, err := dlp.Run(ctx, u.String())
if err != nil {
return Knowledge{}, fmt.Errorf("error running yt-dlp: %w", err)
return shared.Knowledge{}, fmt.Errorf("error running yt-dlp: %w", err)
}
if res == nil {
return Knowledge{}, fmt.Errorf("yt-dlp returned nil")
return shared.Knowledge{}, fmt.Errorf("yt-dlp returned nil")
}
if res.ExitCode != 0 {
return Knowledge{}, fmt.Errorf("yt-dlp exited with code %d", res.ExitCode)
return shared.Knowledge{}, fmt.Errorf("yt-dlp exited with code %d", res.ExitCode)
}
// the transcript for this video now _should_ be at tmpDir/subs.en.vtt, however if it's not then just fine any
@@ -60,7 +63,7 @@ func (a Agent) ReadYouTubeTranscript(ctx context.Context, u *url.URL, questions
vttFile = ""
files, err := os.ReadDir(tmpDir)
if err != nil {
return Knowledge{}, fmt.Errorf("error reading directory: %w", err)
return shared.Knowledge{}, fmt.Errorf("error reading directory: %w", err)
}
for _, file := range files {
@@ -72,7 +75,7 @@ func (a Agent) ReadYouTubeTranscript(ctx context.Context, u *url.URL, questions
}
if vttFile == "" {
return Knowledge{}, fmt.Errorf("no vtt file found")
return shared.Knowledge{}, fmt.Errorf("no vtt file found")
}
fp, err := os.Open(vttFile)
@@ -83,16 +86,16 @@ func (a Agent) ReadYouTubeTranscript(ctx context.Context, u *url.URL, questions
}
}(fp)
if err != nil {
return Knowledge{}, fmt.Errorf("error opening vtt file: %w", err)
return shared.Knowledge{}, fmt.Errorf("error opening vtt file: %w", err)
}
subs, err := astisub.ReadFromWebVTT(fp)
if err != nil {
return Knowledge{}, fmt.Errorf("error reading vtt file: %w", err)
return shared.Knowledge{}, fmt.Errorf("error reading vtt file: %w", err)
}
if len(subs.Items) == 0 {
return Knowledge{}, fmt.Errorf("no subtitles found")
return shared.Knowledge{}, fmt.Errorf("no subtitles found")
}
var ts string