mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-11 08:28:11 -05:00
Compare commits
12 Commits
averikitsc
...
llm-testin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be31376212 | ||
|
|
db8473263e | ||
|
|
2680864dca | ||
|
|
010037af19 | ||
|
|
f69ec70eaf | ||
|
|
187fe69a8b | ||
|
|
45de436118 | ||
|
|
1598e32e34 | ||
|
|
ae68aa58bd | ||
|
|
7fa8633a20 | ||
|
|
615e6e76d9 | ||
|
|
9624d845f2 |
@@ -185,6 +185,7 @@ func NewCommand(opts ...Option) *Command {
|
||||
flags.StringVar(&cmd.prebuiltConfig, "prebuilt", "", "Use a prebuilt tool configuration by source type. Cannot be used with --tools-file. Allowed: 'alloydb-postgres', 'bigquery', 'cloud-sql-mysql', 'cloud-sql-postgres', 'cloud-sql-mssql', 'postgres', 'spanner', 'spanner-postgres'.")
|
||||
flags.BoolVar(&cmd.cfg.Stdio, "stdio", false, "Listens via MCP STDIO instead of acting as a remote HTTP server.")
|
||||
flags.BoolVar(&cmd.cfg.DisableReload, "disable-reload", false, "Disables dynamic reloading of tools file.")
|
||||
flags.BoolVar(&cmd.cfg.UI, "ui", false, "Launches the Toolbox UI web server.")
|
||||
|
||||
// wrap RunE command so that we have access to original Command object
|
||||
cmd.RunE = func(*cobra.Command, []string) error { return run(cmd) }
|
||||
@@ -765,6 +766,9 @@ func run(cmd *Command) error {
|
||||
return errMsg
|
||||
}
|
||||
cmd.logger.InfoContext(ctx, "Server ready to serve!")
|
||||
if cmd.cfg.UI {
|
||||
cmd.logger.InfoContext(ctx, "Toolbox UI is up and running at: http://localhost:5000/ui")
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(srvErr)
|
||||
|
||||
19
go.mod
19
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/googleapis/genai-toolbox
|
||||
|
||||
go 1.23.8
|
||||
go 1.24.2
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
@@ -18,17 +18,20 @@ require (
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/go-chi/httplog/v2 v2.1.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-goquery/goquery v1.0.1
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/googleapis/mcp-toolbox-sdk-go v0.2.0
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/microsoft/go-mssqldb v1.9.2
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.1
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/tmc/langchaingo v0.1.13
|
||||
github.com/valkey-io/valkey-go v1.0.62
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.61.0
|
||||
go.opentelemetry.io/otel v1.36.0
|
||||
@@ -39,7 +42,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0
|
||||
go.opentelemetry.io/otel/trace v1.36.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
google.golang.org/api v0.240.0
|
||||
google.golang.org/api v0.242.0
|
||||
modernc.org/sqlite v1.38.0
|
||||
)
|
||||
|
||||
@@ -48,6 +51,8 @@ require golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
require (
|
||||
cel.dev/expr v0.23.0 // indirect
|
||||
cloud.google.com/go v0.121.2 // indirect
|
||||
cloud.google.com/go/ai v0.7.0 // indirect
|
||||
cloud.google.com/go/aiplatform v1.85.0 // indirect
|
||||
cloud.google.com/go/alloydb v1.16.1 // indirect
|
||||
cloud.google.com/go/auth v0.16.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
@@ -56,11 +61,14 @@ require (
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cloud.google.com/go/trace v1.11.6 // indirect
|
||||
cloud.google.com/go/vertexai v0.12.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/apache/arrow/go/v15 v15.0.2 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
@@ -71,12 +79,13 @@ require (
|
||||
github.com/couchbase/tools-common/errors v1.0.0 // indirect
|
||||
github.com/couchbaselabs/gocbconnstr/v2 v2.0.0-20240607131231-fb385523de28 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
@@ -87,6 +96,7 @@ require (
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/flatbuffers v23.5.26+incompatible // indirect
|
||||
github.com/google/generative-ai-go v0.15.1 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||
@@ -97,7 +107,7 @@ require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/compress v1.17.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -105,6 +115,7 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
|
||||
78
go.sum
78
go.sum
@@ -47,12 +47,16 @@ cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp
|
||||
cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE=
|
||||
cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM=
|
||||
cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ=
|
||||
cloud.google.com/go/ai v0.7.0 h1:P6+b5p4gXlza5E+u7uvcgYlzZ7103ACg70YdZeC6oGE=
|
||||
cloud.google.com/go/ai v0.7.0/go.mod h1:7ozuEcraovh4ABsPbrec3o4LmFl9HigNI3D5haxYeQo=
|
||||
cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
|
||||
cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=
|
||||
cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg=
|
||||
cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ=
|
||||
cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k=
|
||||
cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw=
|
||||
cloud.google.com/go/aiplatform v1.85.0 h1:80/GqdP8Tovaaw9Qr6fYZNDvwJeA9rLk8mYkqBJNIJQ=
|
||||
cloud.google.com/go/aiplatform v1.85.0/go.mod h1:S4DIKz3TFLSt7ooF2aCRdAqsUR4v/YDXUoHqn5P0EFc=
|
||||
cloud.google.com/go/alloydb v1.16.1 h1:pW4D0O2jAfAjoOEI1bgChPwMHWE8X8BjwSO0tfWkWvk=
|
||||
cloud.google.com/go/alloydb v1.16.1/go.mod h1:zeZuGJ5mEaQE70FMXEvZIp5hQLR9yrGnHo1YUOncWRY=
|
||||
cloud.google.com/go/alloydbconn v1.15.3 h1:j0Y0+LpVjdyUguX0uwsaeTtq4tQUZiFvsO52AH+yusY=
|
||||
@@ -502,6 +506,8 @@ cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISI
|
||||
cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4=
|
||||
cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4=
|
||||
cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU=
|
||||
cloud.google.com/go/secretmanager v1.15.0 h1:RtkCMgTpaBMbzozcRUGfZe46jb9a3qh5EdEtVRUATF8=
|
||||
cloud.google.com/go/secretmanager v1.15.0/go.mod h1:1hQSAhKK7FldiYw//wbR/XPfPc08eQ81oBsnRUHEvUc=
|
||||
cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=
|
||||
cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=
|
||||
cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=
|
||||
@@ -559,8 +565,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL
|
||||
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
|
||||
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
|
||||
cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
|
||||
cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw=
|
||||
cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
|
||||
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
|
||||
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
|
||||
cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w=
|
||||
cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I=
|
||||
cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4=
|
||||
@@ -587,6 +593,8 @@ cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fp
|
||||
cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0=
|
||||
cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos=
|
||||
cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos=
|
||||
cloud.google.com/go/vertexai v0.12.0 h1:zTadEo/CtsoyRXNx3uGCncoWAP1H2HakGqwznt+iMo8=
|
||||
cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8=
|
||||
cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk=
|
||||
cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw=
|
||||
cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg=
|
||||
@@ -661,6 +669,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
|
||||
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
|
||||
@@ -668,6 +678,8 @@ github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
|
||||
github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
|
||||
@@ -734,6 +746,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -784,8 +798,10 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-goquery/goquery v1.0.1 h1:kpchVA1LdOFWdRpkDPESVdlb1JQI6ixsJ5MiNUITO7U=
|
||||
github.com/go-goquery/goquery v1.0.1/go.mod h1:W5s8OWbqWf6lG0LkXWBeh7U1Y/X5XTI0Br65MHF8uJk=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
|
||||
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
|
||||
@@ -870,6 +886,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
|
||||
github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
|
||||
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/generative-ai-go v0.15.1 h1:n8aQUpvhPOlGVuM2DRkJ2jvx04zpp42B778AROJa+pQ=
|
||||
github.com/google/generative-ai-go v0.15.1/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw=
|
||||
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=
|
||||
@@ -885,6 +903,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -942,6 +961,8 @@ github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3
|
||||
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
|
||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/googleapis/mcp-toolbox-sdk-go v0.2.0 h1:y242XXymvSDJ84FhDvSqpyjq4bOtRDy6yOxs7QR8etY=
|
||||
github.com/googleapis/mcp-toolbox-sdk-go v0.2.0/go.mod h1:Zd5cooy5sH5ThiTwzhKtZZxTkLGbPlqDZ9c8er969Ug=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
@@ -991,8 +1012,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/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
||||
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
@@ -1045,6 +1066,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -1094,6 +1117,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
|
||||
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
|
||||
github.com/valkey-io/valkey-go v1.0.62 h1:oQdPlQGRyxcQWL8fnu6J3SCaQwayc/hRZifjJIaJqu0=
|
||||
github.com/valkey-io/valkey-go v1.0.62/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
@@ -1178,6 +1203,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -1239,6 +1268,9 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1298,6 +1330,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -1347,6 +1384,10 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1428,8 +1469,14 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
@@ -1438,6 +1485,11 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1454,6 +1506,10 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1528,6 +1584,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1607,8 +1665,8 @@ google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/
|
||||
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
|
||||
google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
|
||||
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
|
||||
google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU=
|
||||
google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
|
||||
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -1825,6 +1883,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
@@ -1900,3 +1960,5 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
|
||||
216
internal/server/agent/engine.go
Normal file
216
internal/server/agent/engine.go
Normal file
@@ -0,0 +1,216 @@
|
||||
// agent/engine.go
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/googleapis/mcp-toolbox-sdk-go/core"
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"github.com/tmc/langchaingo/llms/googleai"
|
||||
)
|
||||
// ChatEvent is streamed to the UI via SSE.
|
||||
type ChatEvent struct {
|
||||
Type string `json:"type"` // user | assistant | tool_call | tool_resp | agent_error | done
|
||||
Content interface{} `json:"content,omitempty"` // text or raw JSON
|
||||
ToolName string `json:"toolName,omitempty"` // for tool_* events
|
||||
Arguments interface{} `json:"arguments,omitempty"` // for tool_call
|
||||
}
|
||||
|
||||
// Engine can be reused safely by many goroutines.
|
||||
type Engine struct {
|
||||
llm llms.LLM
|
||||
langchainTools []llms.Tool // tools passed to the LLM
|
||||
toolsMap map[string]*core.ToolboxTool // lookup by both hyphen and snake names
|
||||
validNames []string // cached list for error messages
|
||||
sysPrompt string
|
||||
maxToolRuns int
|
||||
}
|
||||
|
||||
// New builds a single Engine instance that you can share.
|
||||
func New(ctx context.Context, genaiKey, toolboxURL, toolsetID string) (*Engine, error) {
|
||||
llm, err := googleai.New(ctx,
|
||||
googleai.WithAPIKey(genaiKey),
|
||||
googleai.WithDefaultModel("gemini-2.5-pro"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("googleai: %w", err)
|
||||
}
|
||||
|
||||
tb, err := core.NewToolboxClient(toolboxURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("toolbox client: %w", err)
|
||||
}
|
||||
tools, err := tb.LoadToolset(toolsetID, ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load toolset: %w", err)
|
||||
}
|
||||
|
||||
toolsMap := make(map[string]*core.ToolboxTool, len(tools)*2)
|
||||
var langTools []llms.Tool
|
||||
var valid []string
|
||||
|
||||
for _, t := range tools {
|
||||
orig := t.Name()
|
||||
alias := toSnake(orig)
|
||||
|
||||
toolsMap[orig] = t
|
||||
valid = append(valid, orig)
|
||||
|
||||
if alias != orig {
|
||||
toolsMap[alias] = t
|
||||
valid = append(valid, alias)
|
||||
}
|
||||
|
||||
langTools = append(langTools, makeLangTool(t, alias))
|
||||
}
|
||||
|
||||
fullPrompt := fmt.Sprintf("%s\n\nValid tools:\n- %s",
|
||||
basePrompt, strings.Join(valid, "\n- "))
|
||||
|
||||
return &Engine{
|
||||
llm: llm,
|
||||
langchainTools: langTools,
|
||||
toolsMap: toolsMap,
|
||||
validNames: valid,
|
||||
sysPrompt: fullPrompt,
|
||||
maxToolRuns: 5,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *Engine) Run(ctx context.Context, userMsg string, sink chan<- ChatEvent) {
|
||||
defer close(sink)
|
||||
|
||||
// seed history
|
||||
history := []llms.MessageContent{
|
||||
llms.TextParts(llms.ChatMessageTypeSystem, e.sysPrompt),
|
||||
llms.TextParts(llms.ChatMessageTypeHuman, userMsg),
|
||||
}
|
||||
sink <- ChatEvent{Type: "user", Content: userMsg}
|
||||
|
||||
toolRuns := 0
|
||||
|
||||
for {
|
||||
// ask the model
|
||||
resp, err := e.llm.GenerateContent(ctx, history, llms.WithTools(e.langchainTools))
|
||||
if err != nil {
|
||||
sink <- ChatEvent{Type: "agent_error", Content: err.Error()}
|
||||
return
|
||||
}
|
||||
choice := resp.Choices[0]
|
||||
|
||||
// stream assistant thought
|
||||
sink <- ChatEvent{Type: "assistant", Content: choice.Content}
|
||||
|
||||
// if no tool calls, we're done
|
||||
if len(choice.ToolCalls) == 0 {
|
||||
sink <- ChatEvent{Type: "done"}
|
||||
return
|
||||
}
|
||||
|
||||
// handle every tool call synchronously
|
||||
retry := false
|
||||
for _, tc := range choice.ToolCalls {
|
||||
if toolRuns >= e.maxToolRuns {
|
||||
sink <- ChatEvent{Type: "agent_error",
|
||||
Content: fmt.Sprintf("aborted: exceeded max tool runs (%d)", e.maxToolRuns)}
|
||||
sink <- ChatEvent{Type: "done"}
|
||||
return
|
||||
}
|
||||
toolRuns++
|
||||
|
||||
tool, ok := e.toolsMap[tc.FunctionCall.Name]
|
||||
if !ok {
|
||||
// hallucinated tool kept happening add correction, retry loop
|
||||
msg := fmt.Sprintf("Tool %q does not exist. Valid tools: %s",
|
||||
tc.FunctionCall.Name, strings.Join(e.validNames, ", "))
|
||||
sink <- ChatEvent{Type: "agent_error", Content: msg}
|
||||
history = append(history,
|
||||
llms.TextParts(llms.ChatMessageTypeSystem, msg))
|
||||
retry = true
|
||||
break // leave inner loop, go back to LLM
|
||||
}
|
||||
|
||||
// parse arguments
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(tc.FunctionCall.Arguments), &args); err != nil {
|
||||
sink <- ChatEvent{Type: "agent_error",
|
||||
Content: fmt.Sprintf("arg unmarshal: %v", err)}
|
||||
sink <- ChatEvent{Type: "done"}
|
||||
return
|
||||
}
|
||||
|
||||
// announce call
|
||||
sink <- ChatEvent{Type: "tool_call", ToolName: tc.FunctionCall.Name, Arguments: args}
|
||||
|
||||
// invoke tool
|
||||
result, err := tool.Invoke(ctx, args)
|
||||
if err != nil {
|
||||
sink <- ChatEvent{Type: "agent_error",
|
||||
Content: fmt.Sprintf("tool error: %v", err)}
|
||||
sink <- ChatEvent{Type: "done"}
|
||||
return
|
||||
}
|
||||
if result == "" || result == nil {
|
||||
result = "Operation completed successfully."
|
||||
}
|
||||
|
||||
// stream response
|
||||
sink <- ChatEvent{Type: "tool_resp", ToolName: tc.FunctionCall.Name, Content: result}
|
||||
|
||||
// add to memory
|
||||
history = append(history,
|
||||
llms.MessageContent{
|
||||
Role: llms.ChatMessageTypeTool,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.ToolCallResponse{
|
||||
Name: tc.FunctionCall.Name,
|
||||
Content: fmt.Sprintf("%v", result),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if retry {
|
||||
continue // model will be asked again with correction in history
|
||||
}
|
||||
|
||||
// append assistant message (already streamed)
|
||||
history = append(history,
|
||||
llms.TextParts(llms.ChatMessageTypeAI, choice.Content))
|
||||
}
|
||||
}
|
||||
|
||||
// makeLangTool converts a Toolbox tool into a LangChain function tool.
|
||||
func makeLangTool(t *core.ToolboxTool, exposedName string) llms.Tool {
|
||||
schemaBytes, _ := t.InputSchema()
|
||||
var paramsSchema map[string]any
|
||||
_ = json.Unmarshal(schemaBytes, ¶msSchema)
|
||||
|
||||
return llms.Tool{
|
||||
Type: "function",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: exposedName,
|
||||
Description: t.Description(),
|
||||
Parameters: paramsSchema,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// toSnake replaces hyphens with underscores.
|
||||
func toSnake(s string) string {
|
||||
return strings.ReplaceAll(s, "-", "_")
|
||||
}
|
||||
|
||||
const basePrompt = `
|
||||
You are a helpful hotel assistant that uses tools to handle hotel searching, booking, updating, and cancellations.
|
||||
|
||||
Rules:
|
||||
1. When the user searches for a hotel (by name, location, or price tier), call the appropriate tool.
|
||||
2. Always return the hotel name, id, location, and price tier in search results.
|
||||
3. When the user asks to book, update, or cancel a hotel, extract the hotel ID and use it in the tool call.
|
||||
4. You may chain multiple tools in sequence, passing outputs as inputs.
|
||||
5. Do NOT ask the user for confirmation; just act.
|
||||
6. Call ONLY tools from list of valid tools; every other name is invalid.
|
||||
`
|
||||
@@ -55,6 +55,8 @@ type ServerConfig struct {
|
||||
Stdio bool
|
||||
// DisableReload indicates if the user has disabled dynamic reloading for Toolbox.
|
||||
DisableReload bool
|
||||
// UI indicates if Toolbox UI endpoints (/ui) are available
|
||||
UI bool
|
||||
}
|
||||
|
||||
type logFormat string
|
||||
|
||||
@@ -330,6 +330,13 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
r.Mount("/mcp", mcpR)
|
||||
if cfg.UI {
|
||||
webR, err := webRouter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Mount("/ui", webR)
|
||||
}
|
||||
// default endpoint for validating server is running
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("🧰 Hello, World! 🧰"))
|
||||
|
||||
34
internal/server/static/agent.html
Normal file
34
internal/server/static/agent.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Toolbox Chat</title>
|
||||
<link rel="stylesheet" href="/ui/css/style.css" />
|
||||
<style>
|
||||
#chat-log { padding: 1rem; max-height: 80vh; overflow-y: auto; }
|
||||
.user { text-align: right; color: #007bff; margin: .5rem 0; }
|
||||
.assistant { text-align: left; color: #333; margin: .5rem 0; }
|
||||
.tool_call { font-style: italic; color: #555; }
|
||||
.tool_resp { font-family: monospace; white-space: pre; background:#fafafa; padding:.25rem }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container" data-active-nav=""></div>
|
||||
|
||||
<div id="chat-log"></div>
|
||||
|
||||
<form id="chat-form" style="padding:1rem;display:flex;gap:.5rem">
|
||||
<input id="msg" style="flex:1" placeholder="Ask me anything…" autocomplete="off" />
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
|
||||
<script src="/ui/js/navbar.js"></script>
|
||||
<script type="module" src="/ui/js/agent.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () =>
|
||||
renderNavbar('navbar-container', '')
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
internal/server/static/assets/mcptoolboxlogo.png
Normal file
BIN
internal/server/static/assets/mcptoolboxlogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
24
internal/server/static/auth.html
Normal file
24
internal/server/static/auth.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OAuth ID Token Generator</title>
|
||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Get Google ID Token</h1>
|
||||
<label for="clientIdInput">OAuth Client ID:</label>
|
||||
<input type="text" id="clientIdInput" placeholder="Enter Client ID (e.g., ....apps.googleusercontent.com)">
|
||||
<button onclick="startSignIn()">Get ID Token</button>
|
||||
|
||||
<div id="gisContainer" style="margin-top: 20px;">
|
||||
</div>
|
||||
|
||||
<h3>ID Token:</h3>
|
||||
<textarea id="idTokenResult" rows="15" cols="80" readonly></textarea>
|
||||
|
||||
<h3>ID Token Claims (Decoded):</h3>
|
||||
<pre id="idTokenClaims"></pre>
|
||||
|
||||
<script src="/ui/js/auth.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
691
internal/server/static/css/style.css
Normal file
691
internal/server/static/css/style.css
Normal file
@@ -0,0 +1,691 @@
|
||||
:root {
|
||||
--toolbox-blue: #4285f4;
|
||||
--text-primary-gray: #444444;
|
||||
--text-secondary-gray: #6e6e6e;
|
||||
--button-primary: var(--toolbox-blue);
|
||||
--button-secondary: #616161;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: 'Trebuchet MS';
|
||||
background-color: #f8f9fa;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
#navbar-container {
|
||||
flex: 0 0 250px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#main-content-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.left-nav {
|
||||
background-color: #fff;
|
||||
box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 3;
|
||||
|
||||
ul {
|
||||
font-family: 'Verdana';
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
|
||||
li {
|
||||
margin-bottom: 5px;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: #e9e9e9;
|
||||
border-radius: 35px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #d0d0d0;
|
||||
font-weight: bold;
|
||||
border-radius: 35px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.second-nav {
|
||||
flex: 0 0 250px;
|
||||
background-color: #fff;
|
||||
box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
width: 90%;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
background-color: #fff;
|
||||
padding: 30px 30px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 20px;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
font: inherit;
|
||||
font-size: 1em;
|
||||
font-weight: bolder;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.btn--run {
|
||||
background-color: var(--button-primary);
|
||||
}
|
||||
|
||||
.btn--editHeaders {
|
||||
background-color: var(--button-secondary)
|
||||
}
|
||||
|
||||
.btn--saveHeaders {
|
||||
background-color: var(--button-primary)
|
||||
}
|
||||
|
||||
.btn--closeHeaders {
|
||||
background-color: var(--button-secondary)
|
||||
}
|
||||
|
||||
.btn--setup-gis {
|
||||
background-color: var(--button-secondary)
|
||||
}
|
||||
|
||||
.tool-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
|
||||
transition: background-color 0.1s ease-in-out, border-radius 0.1s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: #e9e9e9;
|
||||
border-radius: 35px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(208, 208, 208, 0.5);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #d0d0d0;
|
||||
font-weight: bold;
|
||||
border-radius: 35px;
|
||||
|
||||
&:hover {
|
||||
background-color: #d0d0d0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#secondary-panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 20px;
|
||||
margin: 0 0 20px 0;
|
||||
align-items: start;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.tool-execution-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tool-params {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
|
||||
h5 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-box {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #eee;
|
||||
|
||||
h5 {
|
||||
color: var(--toolbox-blue);
|
||||
margin-top: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.params-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8px;
|
||||
padding-right: 6px;
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-secondary-gray);
|
||||
}
|
||||
|
||||
.params-disclaimer {
|
||||
font-style: italic;
|
||||
color: var(--text-secondary-gray);
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.param-item {
|
||||
margin-bottom: 12px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
&.disabled-param {
|
||||
> label {
|
||||
color: #888;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.param-input-element {
|
||||
background-color: #f5f5f5;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select,
|
||||
textarea {
|
||||
width: calc(100% - 12px);
|
||||
padding: 6px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input[type="checkbox"].param-input-element {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
border: initial;
|
||||
border-radius: initial;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
accent-color: var(--toolbox-blue);
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.input-checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.param-input-element-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.param-input-element {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.include-param-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
border: initial;
|
||||
border-radius: initial;
|
||||
vertical-align: middle;
|
||||
margin-right: 0;
|
||||
accent-color: var(--toolbox-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.include-param-container input[type="checkbox"] {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
border: initial;
|
||||
border-radius: initial;
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
accent-color: var(--toolbox-blue);
|
||||
}
|
||||
|
||||
.checkbox-bool-label {
|
||||
margin-left: 5px;
|
||||
font-style: italic;
|
||||
color: var(--text-primary-gray);
|
||||
}
|
||||
|
||||
.checkbox-bool-label.disabled {
|
||||
color: #aaa;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.param-label-extras {
|
||||
font-style: italic;
|
||||
font-weight: lighter;
|
||||
color: var(--text-secondary-gray);
|
||||
}
|
||||
|
||||
.auth-param-input {
|
||||
background-color: #e0e0e0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.run-button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.header-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header-modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 10% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
max-width: 50%;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
|
||||
h5 {
|
||||
margin-top: 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.headers-textarea {
|
||||
width: calc(100% - 16px);
|
||||
padding: 8px;
|
||||
font-family: monospace;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.header-modal-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-token-details {
|
||||
width: 100%;
|
||||
max-width: calc(100% - 16px);
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.auth-token-content {
|
||||
padding: 10px;
|
||||
border: 1px solid #eee;
|
||||
margin-top: 5px;
|
||||
background-color: #f9f9f9;
|
||||
text-align: left;
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
.auth-tab-group {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.auth-tab-picker {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
background-color: #f0f0f0;
|
||||
|
||||
&.active {
|
||||
background-color: #fff;
|
||||
border-color: #ccc;
|
||||
border-bottom-color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-tab-content {
|
||||
display: none;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-x: auto;
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
max-width: 100%;
|
||||
|
||||
code {
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-method-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8f9fa; /* Light grey background */
|
||||
border: 1px solid #e0e0e0; /* Light border */
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.auth-method-label {
|
||||
font-weight: 500;
|
||||
color: #3c4043; /* Dark grey text */
|
||||
}
|
||||
|
||||
.auth-helper-section {
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
background-color: #f8f9fa;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.auth-helper-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-primary-gray);
|
||||
font-size: 1.15em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.auth-method-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.auth-method-details {
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* Wrapper for input rows and action buttons */
|
||||
.auth-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Row containing a label and an input field */
|
||||
.auth-input-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.auth-input-row label {
|
||||
font-size: 14px;
|
||||
color: #3c4043;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Text input field style */
|
||||
.auth-input {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #bdc1c6; /* Grey border */
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.auth-input:focus {
|
||||
outline: none;
|
||||
border-color: #1a73e8; /* Blue border on focus */
|
||||
box-shadow: 0 0 0 1px #1a73e8;
|
||||
}
|
||||
|
||||
/* Container for action buttons within the details section */
|
||||
.auth-method-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tool-response {
|
||||
margin: 20px 0 0 0;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
|
||||
#toolset-search-input {
|
||||
flex-grow: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 20px 0 0 20px;
|
||||
border-right: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-primary-gray);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--toolbox-blue);
|
||||
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-secondary-gray);
|
||||
}
|
||||
}
|
||||
|
||||
#toolset-search-button {
|
||||
padding: 10px 15px;
|
||||
border: 1px solid var(--button-primary);
|
||||
background-color: var(--button-primary);
|
||||
color: white;
|
||||
border-radius: 0 20px 20px 0;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-details-tab {
|
||||
background-color: transparent;
|
||||
color: var(--toolbox-blue);
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
24
internal/server/static/index.html
Normal file
24
internal/server/static/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Toolbox UI</title>
|
||||
<link rel="stylesheet" href="/ui/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container" data-active-nav=""></div>
|
||||
<div id="main-content-container"></div>
|
||||
|
||||
<script src="/ui/js/navbar.js"></script>
|
||||
<script src="/ui/js/mainContent.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', '')
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
62
internal/server/static/js/agent.js
Normal file
62
internal/server/static/js/agent.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const log = document.getElementById("chat-log");
|
||||
const form = document.getElementById("chat-form");
|
||||
const input = document.getElementById("msg");
|
||||
let es; // EventSource
|
||||
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const txt = input.value.trim();
|
||||
if (!txt) return;
|
||||
append("user", txt);
|
||||
|
||||
// start conversation
|
||||
const res = await fetch("/ui/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: txt })
|
||||
});
|
||||
const { id } = await res.json();
|
||||
|
||||
// subscribe to SSE
|
||||
es?.close();
|
||||
es = new EventSource(`/ui/chat/${id}/events`);
|
||||
|
||||
// ---------- connection‑level errors ----------
|
||||
es.addEventListener("error", (ev) => {
|
||||
if (es.readyState === EventSource.CLOSED) return;
|
||||
console.error("EventSource connection error", ev);
|
||||
append("error", { content: "lost connection to server" });
|
||||
});
|
||||
|
||||
// ---------- server‑sent events we expect ----------
|
||||
["assistant", "tool_call", "tool_resp", "agent_error", "done"].forEach(type => {
|
||||
es.addEventListener(type, (ev) => {
|
||||
if (type === "done") {
|
||||
append("assistant", { content: "✓ conversation finished" });
|
||||
es.close();
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(ev.data);
|
||||
append(type, data);
|
||||
});
|
||||
});
|
||||
|
||||
input.value = "";
|
||||
};
|
||||
|
||||
function append(type, payload) {
|
||||
const div = document.createElement("div");
|
||||
div.className = type;
|
||||
switch (type) {
|
||||
case "tool_call":
|
||||
div.textContent = `${payload.toolName}(${JSON.stringify(payload.arguments)})`;
|
||||
break;
|
||||
case "tool_resp":
|
||||
div.textContent = `${payload.toolName} → ${JSON.stringify(payload.content)}`;
|
||||
break;
|
||||
default:
|
||||
div.textContent = (payload.content ?? payload);
|
||||
}
|
||||
log.appendChild(div);
|
||||
log.scrollTop = log.scrollHeight; // auto‑scroll
|
||||
}
|
||||
160
internal/server/static/js/auth.js
Normal file
160
internal/server/static/js/auth.js
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Handles the credential response from the Google Sign-In library.
|
||||
* @param {!CredentialResponse} response The credential response object from GIS.
|
||||
* @param {string} toolId The ID of the tool.
|
||||
* @param {string} authProfileName The name of the authentication profile.
|
||||
*/
|
||||
function handleCredentialResponse(response, toolId, authProfileName) {
|
||||
console.log("handleCredentialResponse called with:", { response, toolId, authProfileName });
|
||||
const headersTextarea = document.getElementById(`headers-textarea-${toolId}`);
|
||||
if (!headersTextarea) {
|
||||
console.error('Headers textarea not found for toolId:', toolId);
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueIdBase = `${toolId}-${authProfileName}`;
|
||||
const setupGisBtn = document.querySelector(`#google-auth-details-${uniqueIdBase} .setup-gis-btn`);
|
||||
const gisContainer = document.getElementById(`gisContainer-${uniqueIdBase}`);
|
||||
|
||||
if (response.credential) {
|
||||
const idToken = response.credential;
|
||||
console.log("ID Token:", idToken);
|
||||
|
||||
try {
|
||||
let currentHeaders = {};
|
||||
if (headersTextarea.value) {
|
||||
currentHeaders = JSON.parse(headersTextarea.value);
|
||||
}
|
||||
const headerKey = `${authProfileName}_token`;
|
||||
currentHeaders[headerKey] = `${idToken}`;
|
||||
headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
|
||||
// alert(`Header '${headerKey}' updated.`);
|
||||
|
||||
if (gisContainer) gisContainer.style.display = 'none';
|
||||
if (setupGisBtn) setupGisBtn.style.display = '';
|
||||
|
||||
} catch (e) {
|
||||
alert('Headers are not valid JSON. Please correct and try again.');
|
||||
console.error("Header JSON parse error:", e);
|
||||
}
|
||||
} else {
|
||||
console.error("Error: No credential in response", response);
|
||||
alert('Error: No ID Token received. Check console for details.');
|
||||
|
||||
if (gisContainer) gisContainer.style.display = 'none';
|
||||
if (setupGisBtn) setupGisBtn.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the Google Sign-In button using the GIS library.
|
||||
* @param {string} toolId The ID of the tool.
|
||||
* @param {string} clientId The Google OAuth Client ID.
|
||||
* @param {string} authProfileName The name of the authentication profile.
|
||||
*/
|
||||
function renderGoogleSignInButton(toolId, clientId, authProfileName) {
|
||||
const uniqueIdBase = `${toolId}-${authProfileName}`;
|
||||
const gisContainerId = `gisContainer-${uniqueIdBase}`;
|
||||
const gisContainer = document.getElementById(gisContainerId);
|
||||
const setupGisBtn = document.querySelector(`#google-auth-details-${uniqueIdBase} .setup-gis-btn`);
|
||||
|
||||
if (!gisContainer) {
|
||||
console.error('GIS container not found:', gisContainerId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clientId) {
|
||||
alert('Please enter an OAuth Client ID first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide the setup button and show the container for the GIS button
|
||||
if (setupGisBtn) setupGisBtn.style.display = 'none';
|
||||
gisContainer.innerHTML = ''; // Clear previous button
|
||||
gisContainer.style.display = 'flex'; // Make it visible
|
||||
console.log(window.google, window.googleaccounts, window.google.accounts.id)
|
||||
if (window.google && window.google.accounts && window.google.accounts.id) {
|
||||
try {
|
||||
console.log("attempting handle response")
|
||||
const handleResponse = (response) => handleCredentialResponse(response, toolId, authProfileName);
|
||||
window.google.accounts.id.initialize({
|
||||
client_id: clientId,
|
||||
callback: handleResponse,
|
||||
auto_select: false
|
||||
});
|
||||
console.log("initialized account")
|
||||
window.google.accounts.id.renderButton(
|
||||
gisContainer,
|
||||
{ theme: "outline", size: "large", text: "signin_with" }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error initializing Google Sign-In:", error);
|
||||
alert("Error initializing Google Sign-In. Check the Client ID and browser console.");
|
||||
gisContainer.innerHTML = '<p style="color: red;">Error loading Sign-In button.</p>';
|
||||
if (setupGisBtn) setupGisBtn.style.display = '';
|
||||
}
|
||||
} else {
|
||||
console.error("GIS library not fully loaded yet.");
|
||||
alert("Google Identity Services library not ready. Please try again in a moment.");
|
||||
gisContainer.innerHTML = '<p style="color: red;">GIS library not ready.</p>';
|
||||
if (setupGisBtn) setupGisBtn.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
// creates the Google Auth method dropdown
|
||||
export function createGoogleAuthMethodItem(toolId, authProfileName) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'auth-method-item';
|
||||
const uniqueIdBase = `${toolId}-${authProfileName}`;
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="auth-method-header">
|
||||
<span class="auth-method-label">Google ID Token (${authProfileName})</span>
|
||||
<button class="toggle-details-tab">Setup</button>
|
||||
</div>
|
||||
<div class="auth-method-details" id="google-auth-details-${uniqueIdBase}" style="display: none;">
|
||||
<div class="auth-controls">
|
||||
<div class="auth-input-row">
|
||||
<label for="clientIdInput-${uniqueIdBase}">OAuth Client ID:</label>
|
||||
<input type="text" id="clientIdInput-${uniqueIdBase}" placeholder="Enter Client ID" class="auth-input">
|
||||
</div>
|
||||
<div class="auth-method-actions">
|
||||
<button class="btn btn--setup-gis">Add Token</button>
|
||||
<div id="gisContainer-${uniqueIdBase}" class="auth-interactive-element gis-container" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const toggleBtn = item.querySelector('.toggle-details-tab');
|
||||
const detailsDiv = item.querySelector(`#google-auth-details-${uniqueIdBase}`);
|
||||
const setupGisBtn = item.querySelector('.btn--setup-gis');
|
||||
const clientIdInput = item.querySelector(`#clientIdInput-${uniqueIdBase}`);
|
||||
const gisContainer = item.querySelector(`#gisContainer-${uniqueIdBase}`);
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const isVisible = detailsDiv.style.display === 'flex';
|
||||
detailsDiv.style.display = isVisible ? 'none' : 'flex';
|
||||
toggleBtn.textContent = isVisible ? 'Setup' : 'Close';
|
||||
if (!isVisible) {
|
||||
if (gisContainer) {
|
||||
gisContainer.innerHTML = '';
|
||||
gisContainer.style.display = 'none';
|
||||
}
|
||||
if (setupGisBtn) {
|
||||
setupGisBtn.style.display = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setupGisBtn.addEventListener('click', () => {
|
||||
const clientId = clientIdInput.value;
|
||||
if (!clientId) {
|
||||
alert('Please enter an OAuth Client ID first.');
|
||||
return;
|
||||
}
|
||||
renderGoogleSignInButton(toolId, clientId, authProfileName);
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
173
internal/server/static/js/loadTools.js
Normal file
173
internal/server/static/js/loadTools.js
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { renderToolInterface } from "./toolDisplay.js";
|
||||
|
||||
let toolDetailsAbortController = null;
|
||||
|
||||
/**
|
||||
* Fetches a toolset from the /api/toolset endpoint and initiates creating the tool list.
|
||||
* @param {!HTMLElement} secondNavContent The HTML element where the tool list will be rendered.
|
||||
* @param {!HTMLElement} toolDisplayArea The HTML element where the details of a selected tool will be displayed.
|
||||
* @param {string} toolsetName The name of the toolset to load (empty string loads all tools).
|
||||
* @returns {!Promise<void>} A promise that resolves when the tools are loaded and rendered, or rejects on error.
|
||||
*/
|
||||
export async function loadTools(secondNavContent, toolDisplayArea, toolsetName) {
|
||||
secondNavContent.innerHTML = '<p>Fetching tools...</p>';
|
||||
try {
|
||||
const response = await fetch(`/api/toolset/${toolsetName}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiResponse = await response.json();
|
||||
renderToolList(apiResponse, secondNavContent, toolDisplayArea);
|
||||
} catch (error) {
|
||||
console.error('Failed to load tools:', error);
|
||||
secondNavContent.innerHTML = `<p class="error">Failed to load tools: <pre><code>${error}</code></pre></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the list of tools as buttons within the provided HTML element.
|
||||
* @param {?{tools: ?Object<string,*>} } apiResponse The API response object containing the tools.
|
||||
* @param {!HTMLElement} secondNavContent The HTML element to render the tool list into.
|
||||
* @param {!HTMLElement} toolDisplayArea The HTML element for displaying tool details (passed to event handlers).
|
||||
*/
|
||||
function renderToolList(apiResponse, secondNavContent, toolDisplayArea) {
|
||||
secondNavContent.innerHTML = '';
|
||||
|
||||
if (!apiResponse || typeof apiResponse.tools !== 'object' || apiResponse.tools === null) {
|
||||
console.error('Error: Expected an object with a "tools" property, but received:', apiResponse);
|
||||
secondNavContent.textContent = 'Error: Invalid response format from toolset API.';
|
||||
return;
|
||||
}
|
||||
|
||||
const toolsObject = apiResponse.tools;
|
||||
const toolNames = Object.keys(toolsObject);
|
||||
|
||||
if (toolNames.length === 0) {
|
||||
secondNavContent.textContent = 'No tools found.';
|
||||
return;
|
||||
}
|
||||
|
||||
const ul = document.createElement('ul');
|
||||
toolNames.forEach(toolName => {
|
||||
const li = document.createElement('li');
|
||||
const button = document.createElement('button');
|
||||
button.textContent = toolName;
|
||||
button.dataset.toolname = toolName;
|
||||
button.classList.add('tool-button');
|
||||
button.addEventListener('click', (event) => handleToolClick(event, secondNavContent, toolDisplayArea));
|
||||
li.appendChild(button);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
secondNavContent.appendChild(ul);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event on a tool button.
|
||||
* @param {!Event} event The click event object.
|
||||
* @param {!HTMLElement} secondNavContent The parent element containing the tool buttons.
|
||||
* @param {!HTMLElement} toolDisplayArea The HTML element where tool details will be shown.
|
||||
*/
|
||||
function handleToolClick(event, secondNavContent, toolDisplayArea) {
|
||||
const toolName = event.target.dataset.toolname;
|
||||
if (toolName) {
|
||||
const currentActive = secondNavContent.querySelector('.tool-button.active');
|
||||
if (currentActive) {
|
||||
currentActive.classList.remove('active');
|
||||
}
|
||||
event.target.classList.add('active');
|
||||
fetchToolDetails(toolName, toolDisplayArea);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches details for a specific tool /api/tool endpoint.
|
||||
* It aborts any previous in-flight request for tool details to stop race condition.
|
||||
* @param {string} toolName The name of the tool to fetch details for.
|
||||
* @param {!HTMLElement} toolDisplayArea The HTML element to display the tool interface in.
|
||||
* @returns {!Promise<void>} A promise that resolves when the tool details are fetched and rendered, or rejects on error.
|
||||
*/
|
||||
async function fetchToolDetails(toolName, toolDisplayArea) {
|
||||
if (toolDetailsAbortController) {
|
||||
toolDetailsAbortController.abort();
|
||||
console.debug("Aborted previous tool fetch.");
|
||||
}
|
||||
|
||||
toolDetailsAbortController = new AbortController();
|
||||
const signal = toolDetailsAbortController.signal;
|
||||
|
||||
toolDisplayArea.innerHTML = '<p>Loading tool details...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tool/${encodeURIComponent(toolName)}`, { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const apiResponse = await response.json();
|
||||
|
||||
if (!apiResponse.tools || !apiResponse.tools[toolName]) {
|
||||
throw new Error(`Tool "${toolName}" data not found in API response.`);
|
||||
}
|
||||
const toolObject = apiResponse.tools[toolName];
|
||||
console.debug("Received tool object: ", toolObject)
|
||||
|
||||
const toolInterfaceData = {
|
||||
id: toolName,
|
||||
name: toolName,
|
||||
description: toolObject.description || "No description provided.",
|
||||
parameters: (toolObject.parameters || []).map(param => {
|
||||
let inputType = 'text';
|
||||
const apiType = param.type ? param.type.toLowerCase() : 'string';
|
||||
let valueType = 'string';
|
||||
let label = param.description || param.name;
|
||||
|
||||
if (apiType === 'integer' || apiType === 'float') {
|
||||
inputType = 'number';
|
||||
valueType = 'number';
|
||||
} else if (apiType === 'boolean') {
|
||||
inputType = 'checkbox';
|
||||
valueType = 'boolean';
|
||||
} else if (apiType === 'array') {
|
||||
inputType = 'textarea';
|
||||
const itemType = param.items && param.items.type ? param.items.type.toLowerCase() : 'string';
|
||||
valueType = `array<${itemType}>`;
|
||||
label += ' (Array)';
|
||||
}
|
||||
|
||||
return {
|
||||
name: param.name,
|
||||
type: inputType,
|
||||
valueType: valueType,
|
||||
label: label,
|
||||
authServices: param.authSources,
|
||||
required: param.required || false,
|
||||
// defaultValue: param.default, can't do this yet bc tool manifest doesn't have default
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
console.debug("Transformed toolInterfaceData:", toolInterfaceData);
|
||||
|
||||
renderToolInterface(toolInterfaceData, toolDisplayArea);
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.debug("Previous fetch was aborted, expected behavior.");
|
||||
} else {
|
||||
console.error(`Failed to load details for tool "${toolName}":`, error);
|
||||
toolDisplayArea.innerHTML = `<p class="error">Failed to load details for ${toolName}. ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
internal/server/static/js/mainContent.js
Normal file
40
internal/server/static/js/mainContent.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* Renders the main content area into the HTML.
|
||||
* @param {string} containerId The ID of the DOM element to inject the content into.
|
||||
* @param {string} idString The id of the item inside the main content area.
|
||||
*/
|
||||
function renderMainContent(containerId, idString) {
|
||||
const mainContentContainer = document.getElementById(containerId);
|
||||
if (!mainContentContainer) {
|
||||
console.error(`Content container with ID "${containerId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const idAttribute = idString ? `id="${idString}"` : '';
|
||||
const contentHTML = `
|
||||
<div class="main-content-area">
|
||||
<div class="top-bar">
|
||||
</div>
|
||||
<main class="content" ${idAttribute}">
|
||||
<h1>Welcome to MCP Toolbox UI</h1>
|
||||
<p>This is the main content area. Click a tab on the left to navigate.</p>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
mainContentContainer.innerHTML = contentHTML;
|
||||
}
|
||||
53
internal/server/static/js/navbar.js
Normal file
53
internal/server/static/js/navbar.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* Renders the navigation bar HTML content into the specified container element.
|
||||
* @param {string} containerId The ID of the DOM element to inject the navbar into.
|
||||
* @param {string | null} activePath The active tab from the navbar.
|
||||
*/
|
||||
function renderNavbar(containerId, activePath) {
|
||||
const navbarContainer = document.getElementById(containerId);
|
||||
if (!navbarContainer) {
|
||||
console.error(`Navbar container with ID "${containerId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const navbarHTML = `
|
||||
<nav class="left-nav">
|
||||
<div class="nav-logo">
|
||||
<img src="/ui/assets/mcptoolboxlogo.png" alt="App Logo">
|
||||
</div>
|
||||
<ul>
|
||||
<li><a href="/ui/sources">Sources</a></li>
|
||||
<li><a href="/ui/authservices">Auth Services</a></li>
|
||||
<li><a href="/ui/tools">Tools</a></li>
|
||||
<li><a href="/ui/toolsets">Toolsets</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
`;
|
||||
|
||||
navbarContainer.innerHTML = navbarHTML;
|
||||
if (activePath) {
|
||||
const navLinks = navbarContainer.querySelectorAll('.left-nav ul li a');
|
||||
navLinks.forEach(link => {
|
||||
const linkPath = new URL(link.href).pathname;
|
||||
if (linkPath === activePath) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
162
internal/server/static/js/runTool.js
Normal file
162
internal/server/static/js/runTool.js
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { isParamIncluded } from "./toolDisplay.js";
|
||||
|
||||
/**
|
||||
* Runs a specific tool using the /api/tools/toolName/invoke endpoint
|
||||
* @param {string} toolId The unique identifier for the tool.
|
||||
* @param {!HTMLFormElement} form The form element containing parameter inputs.
|
||||
* @param {!HTMLTextAreaElement} responseArea The textarea to display results or errors.
|
||||
* @param {!Array<!Object>} parameters An array of parameter definition objects
|
||||
* @param {!HTMLInputElement} prettifyCheckbox The checkbox to control JSON formatting.
|
||||
* @param {function(?Object): void} updateLastResults Callback to store the last results.
|
||||
*/
|
||||
export async function handleRunTool(toolId, form, responseArea, parameters, prettifyCheckbox, updateLastResults, headers) {
|
||||
const formData = new FormData(form);
|
||||
const typedParams = {};
|
||||
responseArea.value = 'Running tool...';
|
||||
updateLastResults(null);
|
||||
|
||||
for (const param of parameters) {
|
||||
const NAME = param.name;
|
||||
const VALUE_TYPE = param.valueType;
|
||||
const RAW_VALUE = formData.get(NAME);
|
||||
const INCLUDE_CHECKED = isParamIncluded(toolId, NAME)
|
||||
|
||||
try {
|
||||
if (!INCLUDE_CHECKED) {
|
||||
console.debug(`Param ${NAME} was intentionally skipped.`)
|
||||
// if param was purposely unchecked, don't include it in body
|
||||
continue;
|
||||
}
|
||||
|
||||
if (VALUE_TYPE === 'boolean') {
|
||||
typedParams[NAME] = RAW_VALUE !== null;
|
||||
console.debug(`Parameter ${NAME} (boolean) set to: ${typedParams[NAME]}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// process remaining types
|
||||
if (VALUE_TYPE && VALUE_TYPE.startsWith('array<')) {
|
||||
typedParams[NAME] = parseArrayParameter(RAW_VALUE, VALUE_TYPE, NAME);
|
||||
} else {
|
||||
switch (VALUE_TYPE) {
|
||||
case 'number':
|
||||
if (RAW_VALUE === "") {
|
||||
console.debug(`Param ${NAME} was empty, setting to empty string.`)
|
||||
typedParams[NAME] = "";
|
||||
} else {
|
||||
const num = Number(RAW_VALUE);
|
||||
if (isNaN(num)) {
|
||||
throw new Error(`Invalid number input for ${NAME}: ${RAW_VALUE}`);
|
||||
}
|
||||
typedParams[NAME] = num;
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
default:
|
||||
typedParams[NAME] = RAW_VALUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing parameter:', NAME, error);
|
||||
responseArea.value = `Error for ${NAME}: ${error.message}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.debug('Running tool:', toolId, 'with typed params:', typedParams);
|
||||
try {
|
||||
const response = await fetch(`/api/tool/${toolId}/invoke`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(typedParams)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
throw new Error(`HTTP error ${response.status}: ${errorBody}`);
|
||||
}
|
||||
const results = await response.json();
|
||||
updateLastResults(results);
|
||||
displayResults(results, responseArea, prettifyCheckbox.checked);
|
||||
} catch (error) {
|
||||
console.error('Error running tool:', error);
|
||||
responseArea.value = `Error: ${error.message}`;
|
||||
updateLastResults(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and validates a single array parameter from a raw string value.
|
||||
* @param {string} rawValue The raw string value from FormData.
|
||||
* @param {string} valueType The full array type string (e.g., "array<number>").
|
||||
* @param {string} paramName The name of the parameter for error messaging.
|
||||
* @return {!Array<*>} The parsed array.
|
||||
* @throws {Error} If parsing or type validation fails.
|
||||
*/
|
||||
function parseArrayParameter(rawValue, valueType, paramName) {
|
||||
const ELEMENT_TYPE = valueType.substring(6, valueType.length - 1);
|
||||
let parsedArray;
|
||||
try {
|
||||
parsedArray = JSON.parse(rawValue);
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid JSON format for ${paramName}. Expected an array. ${e.message}`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsedArray)) {
|
||||
throw new Error(`Input for ${paramName} must be a JSON array (e.g., ["a", "b"]).`);
|
||||
}
|
||||
|
||||
return parsedArray.map((item, index) => {
|
||||
switch (ELEMENT_TYPE) {
|
||||
case 'number':
|
||||
const NUM = Number(item);
|
||||
if (isNaN(NUM)) {
|
||||
throw new Error(`Invalid number "${item}" found in array for ${paramName} at index ${index}.`);
|
||||
}
|
||||
return NUM;
|
||||
case 'boolean':
|
||||
return item === true || String(item).toLowerCase() === 'true';
|
||||
case 'string':
|
||||
default:
|
||||
return item;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the results from the tool run in the response area.
|
||||
*/
|
||||
export function displayResults(results, responseArea, prettify) {
|
||||
if (results === null || results === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resultJson = JSON.parse(results.result);
|
||||
if (prettify) {
|
||||
responseArea.value = JSON.stringify(resultJson, null, 2);
|
||||
} else {
|
||||
responseArea.value = JSON.stringify(resultJson);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing or stringifying results:", error);
|
||||
if (typeof results.result === 'string') {
|
||||
responseArea.value = results.result;
|
||||
} else {
|
||||
responseArea.value = "Error displaying results. Invalid format.";
|
||||
}
|
||||
}
|
||||
}
|
||||
541
internal/server/static/js/toolDisplay.js
Normal file
541
internal/server/static/js/toolDisplay.js
Normal file
@@ -0,0 +1,541 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { handleRunTool, displayResults } from './runTool.js';
|
||||
import { createGoogleAuthMethodItem } from './auth.js'
|
||||
|
||||
/**
|
||||
* Helper function to create form inputs for parameters.
|
||||
*/
|
||||
function createParamInput(param, toolId) {
|
||||
const paramItem = document.createElement('div');
|
||||
paramItem.className = 'param-item';
|
||||
|
||||
const label = document.createElement('label');
|
||||
const INPUT_ID = `param-${toolId}-${param.name}`;
|
||||
const NAME_TEXT = document.createTextNode(param.name);
|
||||
label.setAttribute('for', INPUT_ID);
|
||||
label.appendChild(NAME_TEXT);
|
||||
|
||||
const IS_AUTH_PARAM = param.authServices && param.authServices.length > 0;
|
||||
let additionalLabelText = '';
|
||||
if (IS_AUTH_PARAM) {
|
||||
additionalLabelText += ' (auth)';
|
||||
}
|
||||
if (!param.required) {
|
||||
additionalLabelText += ' (optional)';
|
||||
}
|
||||
|
||||
if (additionalLabelText) {
|
||||
const additionalSpan = document.createElement('span');
|
||||
additionalSpan.textContent = additionalLabelText;
|
||||
additionalSpan.classList.add('param-label-extras');
|
||||
label.appendChild(additionalSpan);
|
||||
}
|
||||
paramItem.appendChild(label);
|
||||
|
||||
const inputCheckboxWrapper = document.createElement('div');
|
||||
const inputContainer = document.createElement('div');
|
||||
inputCheckboxWrapper.className = 'input-checkbox-wrapper';
|
||||
inputContainer.className = 'param-input-element-container';
|
||||
|
||||
// Build parameter's value input box.
|
||||
const PLACEHOLDER_LABEL = param.label;
|
||||
let inputElement;
|
||||
let boolValueLabel = null;
|
||||
|
||||
if (param.type === 'textarea') {
|
||||
inputElement = document.createElement('textarea');
|
||||
inputElement.rows = 3;
|
||||
inputContainer.appendChild(inputElement);
|
||||
} else if(param.type === 'checkbox') {
|
||||
inputElement = document.createElement('input');
|
||||
inputElement.type = 'checkbox';
|
||||
inputElement.title = PLACEHOLDER_LABEL;
|
||||
inputElement.checked = false;
|
||||
|
||||
// handle true/false label for boolean params
|
||||
boolValueLabel = document.createElement('span');
|
||||
boolValueLabel.className = 'checkbox-bool-label';
|
||||
boolValueLabel.textContent = inputElement.checked ? ' true' : ' false';
|
||||
|
||||
inputContainer.appendChild(inputElement);
|
||||
inputContainer.appendChild(boolValueLabel);
|
||||
|
||||
inputElement.addEventListener('change', () => {
|
||||
boolValueLabel.textContent = inputElement.checked ? ' true' : ' false';
|
||||
});
|
||||
} else {
|
||||
inputElement = document.createElement('input');
|
||||
inputElement.type = param.type;
|
||||
inputContainer.appendChild(inputElement);
|
||||
}
|
||||
|
||||
inputElement.id = INPUT_ID;
|
||||
inputElement.name = param.name;
|
||||
inputElement.classList.add('param-input-element');
|
||||
|
||||
if (IS_AUTH_PARAM) {
|
||||
inputElement.disabled = true;
|
||||
inputElement.classList.add('auth-param-input');
|
||||
if (param.type !== 'checkbox') {
|
||||
inputElement.placeholder = param.authServices;
|
||||
}
|
||||
} else if (param.type !== 'checkbox') {
|
||||
inputElement.placeholder = PLACEHOLDER_LABEL ? PLACEHOLDER_LABEL.trim() : '';
|
||||
}
|
||||
inputCheckboxWrapper.appendChild(inputContainer);
|
||||
|
||||
// create the "Include Param" checkbox
|
||||
const INCLUDE_CHECKBOX_ID = `include-${INPUT_ID}`;
|
||||
const includeContainer = document.createElement('div');
|
||||
const includeCheckbox = document.createElement('input');
|
||||
|
||||
includeContainer.className = 'include-param-container';
|
||||
includeCheckbox.type = 'checkbox';
|
||||
includeCheckbox.id = INCLUDE_CHECKBOX_ID;
|
||||
includeCheckbox.name = `include-${param.name}`;
|
||||
includeCheckbox.title = 'Include this parameter'; // Add a tooltip
|
||||
|
||||
// default to checked, unless it's an optional parameter
|
||||
includeCheckbox.checked = param.required;
|
||||
|
||||
includeContainer.appendChild(includeCheckbox);
|
||||
inputCheckboxWrapper.appendChild(includeContainer);
|
||||
|
||||
paramItem.appendChild(inputCheckboxWrapper);
|
||||
|
||||
// function to update UI based on checkbox state
|
||||
const updateParamIncludedState = () => {
|
||||
const isIncluded = includeCheckbox.checked;
|
||||
if (isIncluded) {
|
||||
paramItem.classList.remove('disabled-param');
|
||||
if (!IS_AUTH_PARAM) {
|
||||
inputElement.disabled = false;
|
||||
}
|
||||
if (boolValueLabel) {
|
||||
boolValueLabel.classList.remove('disabled');
|
||||
}
|
||||
} else {
|
||||
paramItem.classList.add('disabled-param');
|
||||
inputElement.disabled = true;
|
||||
if (boolValueLabel) {
|
||||
boolValueLabel.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// add event listener to the include checkbox
|
||||
includeCheckbox.addEventListener('change', updateParamIncludedState);
|
||||
updateParamIncludedState();
|
||||
|
||||
return paramItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to create the header editor popup modal.
|
||||
* @param {string} toolId The unique identifier for the tool.
|
||||
* @param {!Object<string, string>} currentHeaders The current headers.
|
||||
* @param {function(!Object<string, string>): void} saveCallback A function to be
|
||||
* called when the "Save" button is clicked and the headers are successfully
|
||||
* parsed. The function receives the updated headers object as its argument.
|
||||
* @return {!HTMLDivElement} The outermost div element of the created modal.
|
||||
*/
|
||||
function createHeaderEditorModal(toolId, currentHeaders, toolParameters, saveCallback) {
|
||||
const MODAL_ID = `header-modal-${toolId}`;
|
||||
let modal = document.getElementById(MODAL_ID);
|
||||
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
|
||||
modal = document.createElement('div');
|
||||
modal.id = MODAL_ID;
|
||||
modal.className = 'header-modal';
|
||||
|
||||
const modalContent = document.createElement('div');
|
||||
const modalHeader = document.createElement('h5');
|
||||
const headersTextarea = document.createElement('textarea');
|
||||
|
||||
modalContent.className = 'header-modal-content';
|
||||
modalHeader.textContent = 'Edit Request Headers';
|
||||
headersTextarea.id = `headers-textarea-${toolId}`;
|
||||
headersTextarea.className = 'headers-textarea';
|
||||
headersTextarea.rows = 10;
|
||||
headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
|
||||
|
||||
const authProfileNames = new Set();
|
||||
toolParameters.forEach(param => {
|
||||
const isAuthParam = param.authServices && param.authServices.length > 0;
|
||||
if (isAuthParam && param.authServices) {
|
||||
param.authServices.forEach(name => authProfileNames.add(name));
|
||||
}
|
||||
});
|
||||
|
||||
modalContent.appendChild(modalHeader);
|
||||
modalContent.appendChild(headersTextarea);
|
||||
|
||||
if (authProfileNames.size > 0) {
|
||||
const authHelperSection = document.createElement('div');
|
||||
authHelperSection.className = 'auth-helper-section';
|
||||
const title = document.createElement('h6');
|
||||
title.className = 'auth-helper-title';
|
||||
title.textContent = 'Authentication Helpers';
|
||||
authHelperSection.appendChild(title);
|
||||
const authList = document.createElement('div');
|
||||
authList.className = 'auth-method-list';
|
||||
|
||||
authProfileNames.forEach(profileName => {
|
||||
if (profileName.toLowerCase().includes('google')) {
|
||||
const authItem = createGoogleAuthMethodItem(toolId, profileName);
|
||||
authList.appendChild(authItem);
|
||||
} else {
|
||||
console.warn(`Unsupported auth service type for helper UI: ${profileName}`);
|
||||
}
|
||||
});
|
||||
authHelperSection.appendChild(authList);
|
||||
modalContent.appendChild(authHelperSection);
|
||||
}
|
||||
|
||||
const modalActions = document.createElement('div');
|
||||
const closeButton = document.createElement('button');
|
||||
const saveButton = document.createElement('button');
|
||||
const authTokenDropdown = createAuthTokenInfoDropdown();
|
||||
|
||||
modalActions.className = 'header-modal-actions';
|
||||
closeButton.textContent = 'Close';
|
||||
closeButton.className = 'btn btn--closeHeaders';
|
||||
closeButton.addEventListener('click', () => closeHeaderEditor(toolId));
|
||||
saveButton.textContent = 'Save';
|
||||
saveButton.className = 'btn btn--saveHeaders';
|
||||
saveButton.addEventListener('click', () => {
|
||||
try {
|
||||
const updatedHeaders = JSON.parse(headersTextarea.value);
|
||||
saveCallback(updatedHeaders);
|
||||
closeHeaderEditor(toolId);
|
||||
} catch (e) {
|
||||
alert('Invalid JSON format for headers.');
|
||||
console.error("Header JSON parse error:", e);
|
||||
}
|
||||
});
|
||||
|
||||
modalActions.appendChild(closeButton);
|
||||
modalActions.appendChild(saveButton);
|
||||
modalContent.appendChild(modalActions);
|
||||
modalContent.appendChild(authTokenDropdown);
|
||||
modal.appendChild(modalContent);
|
||||
|
||||
// Close modal if clicked outside
|
||||
window.addEventListener('click', (event) => {
|
||||
if (event.target === modal) {
|
||||
closeHeaderEditor(toolId);
|
||||
}
|
||||
});
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to open the header popup.
|
||||
*/
|
||||
function openHeaderEditor(toolId) {
|
||||
const modal = document.getElementById(`header-modal-${toolId}`);
|
||||
if (modal) {
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to close the header popup.
|
||||
*/
|
||||
function closeHeaderEditor(toolId) {
|
||||
const modal = document.getElementById(`header-modal-${toolId}`);
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dropdown element showing information on how to extract Google auth tokens.
|
||||
* @return {HTMLDetailsElement} The details element representing the dropdown.
|
||||
*/
|
||||
function createAuthTokenInfoDropdown() {
|
||||
const details = document.createElement('details');
|
||||
const summary = document.createElement('summary');
|
||||
const content = document.createElement('div');
|
||||
|
||||
details.className = 'auth-token-details';
|
||||
details.appendChild(summary);
|
||||
summary.textContent = 'How to extract Google OAuth ID Token manually';
|
||||
content.className = 'auth-token-content';
|
||||
|
||||
// auth instruction dropdown
|
||||
const tabButtons = document.createElement('div');
|
||||
const leftTab = document.createElement('button');
|
||||
const rightTab = document.createElement('button');
|
||||
|
||||
tabButtons.className = 'auth-tab-group';
|
||||
leftTab.className = 'auth-tab-picker active';
|
||||
leftTab.textContent = 'With Standard Account';
|
||||
leftTab.setAttribute('data-tab', 'standard');
|
||||
rightTab.className = 'auth-tab-picker';
|
||||
rightTab.textContent = 'With Service Account';
|
||||
rightTab.setAttribute('data-tab', 'service');
|
||||
|
||||
tabButtons.appendChild(leftTab);
|
||||
tabButtons.appendChild(rightTab);
|
||||
content.appendChild(tabButtons);
|
||||
|
||||
const tabContentContainer = document.createElement('div');
|
||||
const standardAccInstructions = document.createElement('div');
|
||||
const serviceAccInstructions = document.createElement('div');
|
||||
|
||||
standardAccInstructions.id = 'auth-tab-standard';
|
||||
standardAccInstructions.className = 'auth-tab-content active';
|
||||
standardAccInstructions.innerHTML = AUTH_TOKEN_INSTRUCTIONS_STANDARD;
|
||||
serviceAccInstructions.id = 'auth-tab-service';
|
||||
serviceAccInstructions.className = 'auth-tab-content';
|
||||
serviceAccInstructions.innerHTML = AUTH_TOKEN_INSTRUCTIONS_SERVICE_ACCOUNT;
|
||||
|
||||
tabContentContainer.appendChild(standardAccInstructions);
|
||||
tabContentContainer.appendChild(serviceAccInstructions);
|
||||
content.appendChild(tabContentContainer);
|
||||
|
||||
// switching tabs logic
|
||||
const tabBtns = [leftTab, rightTab];
|
||||
const tabContents = [standardAccInstructions, serviceAccInstructions];
|
||||
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
// deactivate all buttons and contents
|
||||
tabBtns.forEach(b => b.classList.remove('active'));
|
||||
tabContents.forEach(c => c.classList.remove('active'));
|
||||
|
||||
btn.classList.add('active');
|
||||
|
||||
const tabId = btn.getAttribute('data-tab');
|
||||
const activeContent = content.querySelector(`#auth-tab-${tabId}`);
|
||||
if (activeContent) {
|
||||
activeContent.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
details.appendChild(content);
|
||||
return details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the tool display area.
|
||||
*/
|
||||
export function renderToolInterface(tool, containerElement) {
|
||||
const TOOL_ID = tool.id;
|
||||
containerElement.innerHTML = '';
|
||||
|
||||
let lastResults = null;
|
||||
let currentHeaders = {
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
// function to update lastResults so we can toggle json
|
||||
const updateLastResults = (newResults) => {
|
||||
lastResults = newResults;
|
||||
};
|
||||
|
||||
const updateCurrentHeaders = (newHeaders) => {
|
||||
currentHeaders = newHeaders;
|
||||
const newModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, updateCurrentHeaders);
|
||||
containerElement.appendChild(newModal);
|
||||
};
|
||||
|
||||
const gridContainer = document.createElement('div');
|
||||
gridContainer.className = 'tool-details-grid';
|
||||
|
||||
const toolInfoContainer = document.createElement('div');
|
||||
const nameBox = document.createElement('div');
|
||||
const descBox = document.createElement('div');
|
||||
|
||||
nameBox.className = 'tool-box tool-name';
|
||||
nameBox.innerHTML = `<h5>Name:</h5><p>${tool.name}</p>`;
|
||||
descBox.className = 'tool-box tool-description';
|
||||
descBox.innerHTML = `<h5>Description:</h5><p>${tool.description}</p>`;
|
||||
|
||||
toolInfoContainer.className = 'tool-info';
|
||||
toolInfoContainer.appendChild(nameBox);
|
||||
toolInfoContainer.appendChild(descBox);
|
||||
gridContainer.appendChild(toolInfoContainer);
|
||||
|
||||
const DISLCAIMER_INFO = "*Checked parameters are sent with the value from their text field. Empty fields will be sent as an empty string. To exclude a parameter, uncheck it."
|
||||
const paramsContainer = document.createElement('div');
|
||||
const form = document.createElement('form');
|
||||
const paramsHeader = document.createElement('div');
|
||||
const disclaimerText = document.createElement('div');
|
||||
|
||||
paramsContainer.className = 'tool-params tool-box';
|
||||
paramsContainer.innerHTML = '<h5>Parameters:</h5>';
|
||||
paramsHeader.className = 'params-header';
|
||||
paramsContainer.appendChild(paramsHeader);
|
||||
disclaimerText.textContent = DISLCAIMER_INFO;
|
||||
disclaimerText.className = 'params-disclaimer';
|
||||
paramsContainer.appendChild(disclaimerText);
|
||||
|
||||
form.id = `tool-params-form-${TOOL_ID}`;
|
||||
|
||||
tool.parameters.forEach(param => {
|
||||
form.appendChild(createParamInput(param, TOOL_ID));
|
||||
});
|
||||
paramsContainer.appendChild(form);
|
||||
gridContainer.appendChild(paramsContainer);
|
||||
|
||||
containerElement.appendChild(gridContainer);
|
||||
|
||||
const RESPONSE_AREA_ID = `tool-response-area-${TOOL_ID}`;
|
||||
const runButtonContainer = document.createElement('div');
|
||||
const editHeadersButton = document.createElement('button');
|
||||
const runButton = document.createElement('button');
|
||||
|
||||
editHeadersButton.className = 'btn btn--editHeaders';
|
||||
editHeadersButton.textContent = 'Edit Headers';
|
||||
editHeadersButton.addEventListener('click', () => openHeaderEditor(TOOL_ID));
|
||||
runButtonContainer.className = 'run-button-container';
|
||||
runButtonContainer.appendChild(editHeadersButton);
|
||||
|
||||
runButton.className = 'btn btn--run';
|
||||
runButton.textContent = 'Run Tool';
|
||||
runButtonContainer.appendChild(runButton);
|
||||
containerElement.appendChild(runButtonContainer);
|
||||
|
||||
// response Area (bottom)
|
||||
const responseContainer = document.createElement('div');
|
||||
const responseHeaderControls = document.createElement('div');
|
||||
const responseHeader = document.createElement('h5');
|
||||
const responseArea = document.createElement('textarea');
|
||||
|
||||
responseContainer.className = 'tool-response tool-box';
|
||||
responseHeaderControls.className = 'response-header-controls';
|
||||
responseHeader.textContent = 'Response:';
|
||||
responseHeaderControls.appendChild(responseHeader);
|
||||
|
||||
// prettify box
|
||||
const PRETTIFY_ID = `prettify-${TOOL_ID}`;
|
||||
const prettifyDiv = document.createElement('div');
|
||||
const prettifyLabel = document.createElement('label');
|
||||
const prettifyCheckbox = document.createElement('input');
|
||||
|
||||
prettifyDiv.className = 'prettify-container';
|
||||
prettifyLabel.setAttribute('for', PRETTIFY_ID);
|
||||
prettifyLabel.textContent = 'Prettify JSON';
|
||||
prettifyLabel.className = 'prettify-label';
|
||||
|
||||
prettifyCheckbox.type = 'checkbox';
|
||||
prettifyCheckbox.id = PRETTIFY_ID;
|
||||
prettifyCheckbox.checked = true;
|
||||
prettifyCheckbox.className = 'prettify-checkbox';
|
||||
|
||||
prettifyDiv.appendChild(prettifyLabel);
|
||||
prettifyDiv.appendChild(prettifyCheckbox);
|
||||
|
||||
responseHeaderControls.appendChild(prettifyDiv);
|
||||
responseContainer.appendChild(responseHeaderControls);
|
||||
|
||||
responseArea.id = RESPONSE_AREA_ID;
|
||||
responseArea.readOnly = true;
|
||||
responseArea.placeholder = 'Results will appear here...';
|
||||
responseArea.className = 'tool-response-area';
|
||||
responseArea.rows = 10;
|
||||
responseContainer.appendChild(responseArea);
|
||||
|
||||
containerElement.appendChild(responseContainer);
|
||||
|
||||
// create and append the header editor modal
|
||||
const headerModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, updateCurrentHeaders);
|
||||
containerElement.appendChild(headerModal);
|
||||
|
||||
prettifyCheckbox.addEventListener('change', () => {
|
||||
if (lastResults) {
|
||||
displayResults(lastResults, responseArea, prettifyCheckbox.checked);
|
||||
}
|
||||
});
|
||||
|
||||
runButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
handleRunTool(TOOL_ID, form, responseArea, tool.parameters, prettifyCheckbox, updateLastResults, currentHeaders);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific parameter is marked as included for a given tool.
|
||||
* @param {string} toolId The ID of the tool.
|
||||
* @param {string} paramName The name of the parameter.
|
||||
* @return {boolean|null} True if the parameter's include checkbox is checked,
|
||||
* False if unchecked, Null if the checkbox element is not found.
|
||||
*/
|
||||
export function isParamIncluded(toolId, paramName) {
|
||||
const inputId = `param-${toolId}-${paramName}`;
|
||||
const includeCheckboxId = `include-${inputId}`;
|
||||
const includeCheckbox = document.getElementById(includeCheckboxId);
|
||||
|
||||
if (includeCheckbox && includeCheckbox.type === 'checkbox') {
|
||||
return includeCheckbox.checked;
|
||||
}
|
||||
|
||||
console.warn(`Include checkbox not found for ID: ${includeCheckboxId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Templates for inserting token retrieval instructions into edit header modal
|
||||
const AUTH_TOKEN_INSTRUCTIONS_SERVICE_ACCOUNT = `
|
||||
<p>To obtain a Google OAuth ID token using a service account:</p>
|
||||
<ol>
|
||||
<li>Make sure you are on the intended SERVICE account (typically contain iam.gserviceaccount.com). Verify by running the command below.
|
||||
<pre><code>gcloud auth list</code></pre>
|
||||
</li>
|
||||
<li>Print an id token with the audience set to your clientID defined in tools file:
|
||||
<pre><code>gcloud auth print-identity-token --audiences=YOUR_CLIENT_ID_HERE</code></pre>
|
||||
</li>
|
||||
<li>Copy the output token.</li>
|
||||
<li>Paste this token into the header in JSON editor. The key should be the name of your auth service followed by <code>_token</code>
|
||||
<pre><code>{
|
||||
"Content-Type": "application/json",
|
||||
"my-google-auth_token": "YOUR_ID_TOKEN_HERE"
|
||||
} </code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
<p>This token is typically short-lived.</p>`;
|
||||
|
||||
const AUTH_TOKEN_INSTRUCTIONS_STANDARD = `
|
||||
<p>To obtain a Google OAuth ID token using a standard account:</p>
|
||||
<ol>
|
||||
<li>Make sure you are on your intended standard account. Verify by running the command below.
|
||||
<pre><code>gcloud auth list</code></pre>
|
||||
</li>
|
||||
<li>Within your Cloud Console, add the following link to the "Authorized Redirect URIs".</li>
|
||||
<pre><code>https://developers.google.com/oauthplayground</code></pre>
|
||||
<li>Go to the Google OAuth Playground site: <a href="https://developers.google.com/oauthplayground/" target="_blank">https://developers.google.com/oauthplayground/</a></li>
|
||||
<li>In the top right settings menu, select "Use your own OAuth Credentials".</li>
|
||||
<li>Input your clientID (from tools file), along with the client secret from Cloud Console.</li>
|
||||
<li>Inside the Google OAuth Playground, select "Google OAuth2 API v2.</li>
|
||||
<ul>
|
||||
<li>Select "Authorize APIs".</li>
|
||||
<li>Select "Exchange Authorization codes for tokens"</li>
|
||||
<li>Copy the id_token field provided in the response.</li>
|
||||
</ul>
|
||||
<li>Paste this token into the header in JSON editor. The key should be the name of your auth service followed by <code>_token</code>
|
||||
<pre><code>{
|
||||
"Content-Type": "application/json",
|
||||
"my-google-auth_token": "YOUR_ID_TOKEN_HERE"
|
||||
} </code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
<p>This token is typically short-lived.</p>`;
|
||||
32
internal/server/static/js/tools.js
Normal file
32
internal/server/static/js/tools.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { loadTools } from "./loadTools.js";
|
||||
|
||||
/**
|
||||
* These functions runs after the browser finishes loading and parsing HTML structure.
|
||||
* This ensures that elements can be safely accessed.
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toolDisplayArea = document.getElementById('tool-display-area');
|
||||
const secondaryPanelContent = document.getElementById('secondary-panel-content');
|
||||
const DEFAULT_TOOLSET = ""; // will return all toolsets
|
||||
|
||||
if (!secondaryPanelContent || !toolDisplayArea) {
|
||||
console.error('Required DOM elements not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
loadTools(secondaryPanelContent, toolDisplayArea, DEFAULT_TOOLSET);
|
||||
});
|
||||
49
internal/server/static/js/toolsets.js
Normal file
49
internal/server/static/js/toolsets.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { loadTools } from "./loadTools.js";
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const searchInput = document.getElementById('toolset-search-input');
|
||||
const searchButton = document.getElementById('toolset-search-button');
|
||||
const secondNavContent = document.getElementById('secondary-panel-content');
|
||||
const toolDisplayArea = document.getElementById('tool-display-area');
|
||||
|
||||
if (!searchInput || !searchButton || !secondNavContent || !toolDisplayArea) {
|
||||
console.error('Required DOM elements not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Event listener for search button click
|
||||
searchButton.addEventListener('click', () => {
|
||||
const toolsetName = searchInput.value.trim();
|
||||
if (toolsetName) {
|
||||
loadTools(secondNavContent, toolDisplayArea, toolsetName)
|
||||
} else {
|
||||
secondNavContent.innerHTML = '<p>Please enter a toolset name to search.</p>';
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener for Enter key in search input
|
||||
searchInput.addEventListener('keypress', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
const toolsetName = searchInput.value.trim();
|
||||
if (toolsetName) {
|
||||
loadTools(secondNavContent, toolDisplayArea, toolsetName);
|
||||
} else {
|
||||
secondNavContent.innerHTML = '<p>Please enter a toolset name to search.</p>';
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
34
internal/server/static/tools.html
Normal file
34
internal/server/static/tools.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tools View</title>
|
||||
<link rel="stylesheet" href="/ui/css/style.css">
|
||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container" data-active-nav="/ui/tools"></div>
|
||||
|
||||
<aside class="second-nav">
|
||||
<h4>My Tools</h4>
|
||||
<div id="secondary-panel-content">
|
||||
<p>Fetching tools...</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div id="main-content-container"></div>
|
||||
|
||||
<script type="module" src="/ui/js/tools.js"></script>
|
||||
<script src="/ui/js/navbar.js"></script>
|
||||
<script src="/ui/js/mainContent.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', 'tool-display-area')
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
37
internal/server/static/toolsets.html
Normal file
37
internal/server/static/toolsets.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Toolsets View</title>
|
||||
<link rel="stylesheet" href="/ui/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container" data-active-nav="/ui/toolsets"></div>
|
||||
|
||||
<aside class="second-nav">
|
||||
<h4>Search Toolsets</h4>
|
||||
<div class="search-container">
|
||||
<input type="text" id="toolset-search-input" placeholder="Enter toolset name...">
|
||||
<button id="toolset-search-button">Search</button>
|
||||
</div>
|
||||
<div id="secondary-panel-content">
|
||||
<p>Search for a toolset to see available tools.</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div id="main-content-container"></div>
|
||||
|
||||
<script type="module" src="/ui/js/toolsets.js"></script>
|
||||
<script src="/ui/js/navbar.js"></script>
|
||||
<script src="/ui/js/mainContent.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', 'tool-display-area');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
164
internal/server/web.go
Normal file
164
internal/server/web.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/server/agent"
|
||||
)
|
||||
|
||||
//go:embed all:static
|
||||
var staticContent embed.FS
|
||||
|
||||
type session struct {
|
||||
events chan agent.ChatEvent
|
||||
}
|
||||
|
||||
var (
|
||||
sessions = struct {
|
||||
sync.RWMutex
|
||||
m map[string]*session
|
||||
}{m: make(map[string]*session)}
|
||||
)
|
||||
|
||||
func webRouter() (chi.Router, error) {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.StripSlashes)
|
||||
|
||||
// HTML entry points
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/index.html") })
|
||||
r.Get("/tools", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/tools.html") })
|
||||
r.Get("/toolsets", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/toolsets.html") })
|
||||
r.Get("/agent", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/agent.html") })
|
||||
|
||||
// Chat endpoints -------------------------------------------------
|
||||
r.Post("/chat", startChatHandler) // POST /ui/chat
|
||||
r.Get("/chat/{id}/events", streamChatHandler) // GET /ui/chat/{id}/events
|
||||
|
||||
// static assets
|
||||
staticFS, _ := fs.Sub(staticContent, "static")
|
||||
r.Handle("/*", http.StripPrefix("/ui", http.FileServer(http.FS(staticFS))))
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
type startReq struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
type startResp struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func startChatHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req startReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Message == "" {
|
||||
http.Error(w, "invalid body: need {\"message\":\"...\"}", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
eng, err := getEngine(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "engine init: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// create session
|
||||
id := uuid.NewString()
|
||||
s := &session{events: make(chan agent.ChatEvent, 32)}
|
||||
|
||||
sessions.Lock()
|
||||
sessions.m[id] = s
|
||||
sessions.Unlock()
|
||||
|
||||
// go eng.Run(r.Context(), req.Message, s.events)
|
||||
go eng.Run(context.Background(), req.Message, s.events)
|
||||
|
||||
_ = json.NewEncoder(w).Encode(startResp{ID: id})
|
||||
}
|
||||
|
||||
func streamChatHandler(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
sessions.RLock()
|
||||
s, ok := sessions.m[id]
|
||||
sessions.RUnlock()
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "stream unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
ctx := r.Context()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case ev, open := <-s.events:
|
||||
if !open {
|
||||
return // chat finished
|
||||
}
|
||||
b, _ := json.Marshal(ev)
|
||||
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", ev.Type, b)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var (
|
||||
engineOnce sync.Once
|
||||
globalEng *agent.Engine
|
||||
engineErr error
|
||||
)
|
||||
|
||||
func getEngine(ctx context.Context) (*agent.Engine, error) {
|
||||
engineOnce.Do(func() {
|
||||
genaiKey := os.Getenv("GOOGLE_API_KEY")
|
||||
toolboxURL := "http://localhost:5000"
|
||||
toolsetID := "my-toolset-5"
|
||||
|
||||
globalEng, engineErr = agent.New(ctx, genaiKey, toolboxURL, toolsetID)
|
||||
})
|
||||
return globalEng, engineErr
|
||||
}
|
||||
|
||||
func serveHTML(w http.ResponseWriter, r *http.Request, filepath string) {
|
||||
file, err := staticContent.Open(filepath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), bytes.NewReader(fileBytes))
|
||||
}
|
||||
179
internal/server/web_test.go
Normal file
179
internal/server/web_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-goquery/goquery"
|
||||
)
|
||||
|
||||
// TestWebEndpoint tests the routes defined in webRouter mounted under /ui.
|
||||
func TestWebEndpoint(t *testing.T) {
|
||||
mainRouter := chi.NewRouter()
|
||||
webR, err := webRouter()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create webRouter: %v", err)
|
||||
}
|
||||
mainRouter.Mount("/ui", webR)
|
||||
|
||||
ts := httptest.NewServer(mainRouter)
|
||||
defer ts.Close()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
wantStatus int
|
||||
wantContentType string
|
||||
wantPageTitle string
|
||||
}{
|
||||
{
|
||||
name: "web index page",
|
||||
path: "/ui",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "Toolbox UI",
|
||||
},
|
||||
{
|
||||
name: "web index page with trailing slash",
|
||||
path: "/ui/",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "Toolbox UI",
|
||||
},
|
||||
{
|
||||
name: "web tools page",
|
||||
path: "/ui/tools",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "Tools View",
|
||||
},
|
||||
{
|
||||
name: "web tools page with trailing slash",
|
||||
path: "/ui/tools/",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "Tools View",
|
||||
},
|
||||
{
|
||||
name: "web toolsets page",
|
||||
path: "/ui/toolsets",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "Toolsets View",
|
||||
},
|
||||
{
|
||||
name: "web toolsets page with trailing slash",
|
||||
path: "/ui/toolsets/",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "Toolsets View",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
reqURL := ts.URL + tc.path
|
||||
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
client := ts.Client()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to send request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != tc.wantStatus {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("Unexpected status code for %s: got %d, want %d, body: %s", tc.path, resp.StatusCode, tc.wantStatus, string(body))
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, tc.wantContentType) {
|
||||
t.Errorf("Unexpected Content-Type header for %s: got %s, want prefix %s", tc.path, contentType, tc.wantContentType)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse HTML: %v", err)
|
||||
}
|
||||
|
||||
gotPageTitle := doc.Find("title").Text()
|
||||
if gotPageTitle != tc.wantPageTitle {
|
||||
t.Errorf("Unexpected page title for %s: got %q, want %q", tc.path, gotPageTitle, tc.wantPageTitle)
|
||||
}
|
||||
|
||||
pageURL := resp.Request.URL
|
||||
verifyLinkedResources(t, ts, pageURL, doc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// verifyLinkedResources checks that resources linked in the HTML are served correctly.
|
||||
func verifyLinkedResources(t *testing.T, ts *httptest.Server, pageURL *url.URL, doc *goquery.Document) {
|
||||
t.Helper()
|
||||
|
||||
selectors := map[string]string{
|
||||
"stylesheet": "link[rel=stylesheet]",
|
||||
"script": "script[src]",
|
||||
}
|
||||
|
||||
attrMap := map[string]string{
|
||||
"stylesheet": "href",
|
||||
"script": "src",
|
||||
}
|
||||
|
||||
foundResource := false
|
||||
for resourceType, selector := range selectors {
|
||||
doc.Find(selector).Each(func(i int, s *goquery.Selection) {
|
||||
foundResource = true
|
||||
attrName := attrMap[resourceType]
|
||||
resourcePath, exists := s.Attr(attrName)
|
||||
if !exists || resourcePath == "" {
|
||||
t.Errorf("Resource element %s is missing attribute %s on page %s", selector, attrName, pageURL.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the URL relative to the page URL
|
||||
resURL, err := url.Parse(resourcePath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to parse resource path %q on page %s: %v", resourcePath, pageURL.String(), err)
|
||||
return
|
||||
}
|
||||
absoluteResourceURL := pageURL.ResolveReference(resURL)
|
||||
|
||||
// Skip external hosts
|
||||
if absoluteResourceURL.Host != pageURL.Host {
|
||||
t.Logf("Skipping resource on different host: %s", absoluteResourceURL.String())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := ts.Client().Get(absoluteResourceURL.String())
|
||||
if err != nil {
|
||||
t.Errorf("Failed to GET %s resource %s: %v", resourceType, absoluteResourceURL.String(), err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Resource %s %s: expected status OK (200), but got %d", resourceType, absoluteResourceURL.String(), resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if !foundResource {
|
||||
t.Logf("No stylesheet or script resources found to check on page %s", pageURL.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user