Initial Kaya board-mention agent daemon.
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
Thin WS client to l-ai-tasks that drafts short core RunAgent replies on @kaya mentions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
adb6c5ab8a
17
.env.example
Normal file
17
.env.example
Normal file
@ -0,0 +1,17 @@
|
||||
# Tasks board (WebSocket + REST)
|
||||
TASKS_WS_URL=ws://localhost:8080/api/ws
|
||||
TASKS_API_URL=http://localhost:8080
|
||||
TASKS_AGENT_TOKEN=
|
||||
|
||||
# l-ai-core AIService.RunAgent
|
||||
CORE_ENDPOINT=localhost:50051
|
||||
CORE_TOKEN=
|
||||
CORE_INSECURE=true
|
||||
|
||||
MODEL=deepseek-v4-flash
|
||||
AGENT_PROFILE_PATH=profiles/kaya.yaml
|
||||
|
||||
HEALTH_LISTEN_ADDR=:8087
|
||||
METRICS_LISTEN_ADDR=:9097
|
||||
SHUTDOWN_TIMEOUT=15s
|
||||
RESPONSE_TIMEOUT=120s
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
*.exe
|
||||
*.test
|
||||
*.out
|
||||
.env
|
||||
.idea/
|
||||
.vscode/
|
||||
bin/
|
||||
dist/
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "proto/l-ai-core-proto"]
|
||||
path = proto/l-ai-core-proto
|
||||
url = https://gitea.fe.kaien.ru/kaien/l-ai-core-proto.git
|
||||
73
.woodpecker.yml
Normal file
73
.woodpecker.yml
Normal file
@ -0,0 +1,73 @@
|
||||
clone:
|
||||
git:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
submodule_recursive: true
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang:1.25-alpine
|
||||
commands:
|
||||
- apk add --no-cache git build-base
|
||||
- go build ./...
|
||||
- go vet ./...
|
||||
- go test ./...
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
||||
|
||||
- name: build-and-push
|
||||
image: gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||
environment:
|
||||
ZOT_USERNAME:
|
||||
from_secret: ZOT_USERNAME
|
||||
ZOT_PASSWORD:
|
||||
from_secret: ZOT_PASSWORD
|
||||
commands:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- >-
|
||||
printf '{"auths":{"registry.be.kaien.ru":{"auth":"%s"}}}'
|
||||
"$(printf '%s:%s' "$ZOT_USERNAME" "$ZOT_PASSWORD" | base64 | tr -d '\n')"
|
||||
> /kaniko/.docker/config.json
|
||||
- >-
|
||||
/kaniko/executor
|
||||
--context "$CI_WORKSPACE"
|
||||
--dockerfile "$CI_WORKSPACE/Dockerfile"
|
||||
--destination "registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:$CI_COMMIT_SHA"
|
||||
--destination "registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:latest"
|
||||
depends_on:
|
||||
- test
|
||||
when:
|
||||
- branch: master
|
||||
event: [push, manual]
|
||||
|
||||
- name: deploy
|
||||
image: alpine:latest
|
||||
environment:
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: SSH_PRIVATE_KEY
|
||||
DEPLOY_HOST:
|
||||
from_secret: SWARM_DEPLOY_HOST
|
||||
DEPLOY_USER:
|
||||
from_secret: SWARM_DEPLOY_USER
|
||||
commands:
|
||||
- apk add --no-cache openssh-client
|
||||
- eval $(ssh-agent -s)
|
||||
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- ssh-add ~/.ssh/id_rsa
|
||||
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
|
||||
- chmod 644 ~/.ssh/known_hosts
|
||||
- >-
|
||||
ssh -n $DEPLOY_USER@$DEPLOY_HOST
|
||||
"docker service update
|
||||
--image registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:$CI_COMMIT_SHA
|
||||
--with-registry-auth
|
||||
--update-order start-first
|
||||
--detach=false
|
||||
l-ai_agent-kaya"
|
||||
depends_on:
|
||||
- build-and-push
|
||||
when:
|
||||
- branch: master
|
||||
event: [push, manual]
|
||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM golang:1.25-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN apk add --no-cache git
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /kaya ./cmd/kaya
|
||||
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache ca-certificates
|
||||
WORKDIR /app
|
||||
COPY --from=build /kaya /app/kaya
|
||||
COPY profiles /app/profiles
|
||||
EXPOSE 8087 9097
|
||||
ENTRYPOINT ["/app/kaya"]
|
||||
45
README.md
Normal file
45
README.md
Normal file
@ -0,0 +1,45 @@
|
||||
# l-ai-agent-kaya
|
||||
|
||||
Thin Tasks board agent: replies to `@kaya` mentions in task comments.
|
||||
|
||||
## Behavior
|
||||
|
||||
Long-lived daemon:
|
||||
|
||||
1. Connects to `l-ai-tasks` WebSocket (`TASKS_WS_URL`) with reconnect.
|
||||
2. On `comment.added` where mentions include agent `kaya` and author ≠ `kaya`:
|
||||
- dedupes by comment id
|
||||
- GET task + comments via REST
|
||||
- calls `l-ai-core` `AIService.RunAgent` (tools disabled) with Kaya persona
|
||||
- POST comment with `Idempotency-Key: mention-reply:<comment_id>`
|
||||
|
||||
No Telegram / Live Chat integration.
|
||||
|
||||
## Identity
|
||||
|
||||
- `agent_id`: `kaya`
|
||||
- Display: Kaya / Кая
|
||||
- Profile: `profiles/kaya.yaml`
|
||||
|
||||
## Env
|
||||
|
||||
| Variable | Required | Default |
|
||||
|---|---|---|
|
||||
| `TASKS_WS_URL` | yes | — |
|
||||
| `TASKS_API_URL` | yes | — |
|
||||
| `TASKS_AGENT_TOKEN` | yes | — |
|
||||
| `CORE_ENDPOINT` | no | `core:50051` |
|
||||
| `CORE_TOKEN` | yes | — |
|
||||
| `CORE_INSECURE` | no | `false` |
|
||||
| `MODEL` | no | `deepseek-v4-flash` |
|
||||
| `AGENT_PROFILE_PATH` | no | `profiles/kaya.yaml` |
|
||||
| `HEALTH_LISTEN_ADDR` | no | `:8087` |
|
||||
| `METRICS_LISTEN_ADDR` | no | `:9097` |
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
go build -o kaya ./cmd/kaya
|
||||
```
|
||||
|
||||
Image binary: `/app/kaya`. Health `:8087/healthz`, metrics `:9097/metrics`.
|
||||
86
cmd/kaya/main.go
Normal file
86
cmd/kaya/main.go
Normal file
@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"l-ai-agent-kaya/internal/config"
|
||||
"l-ai-agent-kaya/internal/inference"
|
||||
"l-ai-agent-kaya/internal/profile"
|
||||
"l-ai-agent-kaya/internal/tasksboard"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
slog.Error("kaya stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prof, err := profile.Load(cfg.ProfilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
core, err := inference.DialCore(cfg.CoreEndpoint, cfg.CoreToken, cfg.CoreInsecure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer core.Close()
|
||||
|
||||
go serveHTTP(cfg.MetricsListenAddr, promhttp.Handler())
|
||||
go serveHTTP(cfg.HealthListenAddr, healthHandler())
|
||||
|
||||
loop := &tasksboard.Loop{
|
||||
WSURL: cfg.TasksWSURL,
|
||||
Token: cfg.TasksAgentToken,
|
||||
REST: &tasksboard.Client{BaseURL: cfg.TasksAPIURL, Token: cfg.TasksAgentToken},
|
||||
Core: core,
|
||||
Profile: prof,
|
||||
Model: cfg.Model,
|
||||
ResponseTimeout: cfg.ResponseTimeout,
|
||||
Log: slog.Default(),
|
||||
}
|
||||
go loop.Run(ctx)
|
||||
|
||||
<-ctx.Done()
|
||||
shutdown, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
|
||||
defer cancel()
|
||||
<-shutdown.Done()
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func serveHTTP(addr string, h http.Handler) {
|
||||
if addr == "" {
|
||||
return
|
||||
}
|
||||
if err := http.ListenAndServe(addr, h); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
slog.Error("http server stopped", "addr", addr, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func healthHandler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
})
|
||||
return mux
|
||||
}
|
||||
26
go.mod
Normal file
26
go.mod
Normal file
@ -0,0 +1,26 @@
|
||||
module l-ai-agent-kaya
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
google.golang.org/grpc v1.70.0
|
||||
google.golang.org/protobuf v1.36.8
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
|
||||
)
|
||||
74
go.sum
Normal file
74
go.sum
Normal file
@ -0,0 +1,74 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
81
internal/config/config.go
Normal file
81
internal/config/config.go
Normal file
@ -0,0 +1,81 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ProfilePath string
|
||||
TasksWSURL string
|
||||
TasksAPIURL string
|
||||
TasksAgentToken string
|
||||
CoreEndpoint string
|
||||
CoreToken string
|
||||
CoreInsecure bool
|
||||
Model string
|
||||
MetricsListenAddr string
|
||||
HealthListenAddr string
|
||||
ShutdownTimeout time.Duration
|
||||
ResponseTimeout time.Duration
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
c := Config{
|
||||
ProfilePath: env("AGENT_PROFILE_PATH", "profiles/kaya.yaml"),
|
||||
TasksWSURL: os.Getenv("TASKS_WS_URL"),
|
||||
TasksAPIURL: os.Getenv("TASKS_API_URL"),
|
||||
TasksAgentToken: os.Getenv("TASKS_AGENT_TOKEN"),
|
||||
CoreEndpoint: env("CORE_ENDPOINT", "core:50051"),
|
||||
CoreToken: os.Getenv("CORE_TOKEN"),
|
||||
CoreInsecure: envBool("CORE_INSECURE", false),
|
||||
Model: env("MODEL", "deepseek-v4-flash"),
|
||||
MetricsListenAddr: env("METRICS_LISTEN_ADDR", ":9097"),
|
||||
HealthListenAddr: env("HEALTH_LISTEN_ADDR", ":8087"),
|
||||
ShutdownTimeout: envDur("SHUTDOWN_TIMEOUT", 15*time.Second),
|
||||
ResponseTimeout: envDur("RESPONSE_TIMEOUT", 120*time.Second),
|
||||
}
|
||||
if c.TasksWSURL == "" {
|
||||
return c, errors.New("TASKS_WS_URL is required")
|
||||
}
|
||||
if c.TasksAPIURL == "" {
|
||||
return c, errors.New("TASKS_API_URL is required")
|
||||
}
|
||||
if c.TasksAgentToken == "" {
|
||||
return c, errors.New("TASKS_AGENT_TOKEN is required")
|
||||
}
|
||||
if c.CoreToken == "" {
|
||||
return c, errors.New("CORE_TOKEN is required")
|
||||
}
|
||||
if c.ResponseTimeout <= 0 {
|
||||
return c, errors.New("RESPONSE_TIMEOUT must be positive")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func env(k, d string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func envBool(k string, d bool) bool {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
b, _ := strconv.ParseBool(v)
|
||||
return b
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func envDur(k string, d time.Duration) time.Duration {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
x, err := time.ParseDuration(v)
|
||||
if err == nil {
|
||||
return x
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
99
internal/inference/core.go
Normal file
99
internal/inference/core.go
Normal file
@ -0,0 +1,99 @@
|
||||
package inference
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
corepb "l-ai-agent-kaya/proto/l-ai-core-proto"
|
||||
)
|
||||
|
||||
type CoreRunner struct {
|
||||
api corepb.AIServiceClient
|
||||
token string
|
||||
conn *grpc.ClientConn
|
||||
}
|
||||
|
||||
func DialCore(target, token string, insecureTransport bool) (*CoreRunner, error) {
|
||||
var tc credentials.TransportCredentials
|
||||
if insecureTransport {
|
||||
tc = insecure.NewCredentials()
|
||||
} else {
|
||||
tc = credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})
|
||||
}
|
||||
conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(tc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CoreRunner{api: corepb.NewAIServiceClient(conn), token: token, conn: conn}, nil
|
||||
}
|
||||
|
||||
func (r *CoreRunner) Close() error {
|
||||
if r.conn != nil {
|
||||
return r.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CoreRunner) auth(ctx context.Context) context.Context {
|
||||
if strings.TrimSpace(r.token) == "" {
|
||||
return ctx
|
||||
}
|
||||
return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+r.token)
|
||||
}
|
||||
|
||||
func (r *CoreRunner) RunAgent(ctx context.Context, req RunRequest) (RunResult, error) {
|
||||
msgs := make([]*corepb.StatelessMessage, 0, len(req.Messages))
|
||||
for _, m := range req.Messages {
|
||||
msgs = append(msgs, &corepb.StatelessMessage{Role: m.Role, Content: m.Content})
|
||||
}
|
||||
|
||||
toolsEnabled := req.ToolsEnabled
|
||||
maxIters := uint32(1)
|
||||
maxCalls := uint32(0)
|
||||
if toolsEnabled {
|
||||
maxIters = 12
|
||||
maxCalls = 20
|
||||
}
|
||||
|
||||
resp, err := r.api.RunAgent(r.auth(ctx), &corepb.AgentRequest{
|
||||
Messages: msgs,
|
||||
ModelName: req.Model,
|
||||
AgentId: req.AgentID,
|
||||
ToolLoop: &corepb.AgentToolLoopOptions{
|
||||
ToolsEnabled: toolsEnabled,
|
||||
MaxToolIterations: maxIters,
|
||||
MaxTotalToolCalls: maxCalls,
|
||||
TotalTimeoutSeconds: req.TimeoutSeconds,
|
||||
},
|
||||
ResponseContract: &corepb.AgentResponseContract{
|
||||
Format: corepb.AgentOutputFormat_AGENT_OUTPUT_FORMAT_TEXT,
|
||||
},
|
||||
RequestId: req.RequestID,
|
||||
IdempotencyKey: req.IdempotencyKey,
|
||||
SupportsDeferred: false,
|
||||
Metadata: req.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return RunResult{}, err
|
||||
}
|
||||
if resp.Error != "" {
|
||||
return RunResult{}, fmt.Errorf("core RunAgent response error: %s", resp.Error)
|
||||
}
|
||||
if resp.Status == corepb.AnswerStatus_ANSWER_STATUS_PENDING {
|
||||
return RunResult{}, fmt.Errorf("unexpected deferred RunAgent response")
|
||||
}
|
||||
out := RunResult{Content: resp.Content, DurationMS: resp.DurationMs}
|
||||
if resp.Usage != nil {
|
||||
out.ToolsUsed = resp.Usage.ToolsUsed
|
||||
}
|
||||
if !toolsEnabled && len(out.ToolsUsed) != 0 {
|
||||
return RunResult{}, fmt.Errorf("tools used despite tools-disabled request")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
29
internal/inference/types.go
Normal file
29
internal/inference/types.go
Normal file
@ -0,0 +1,29 @@
|
||||
package inference
|
||||
|
||||
import "context"
|
||||
|
||||
type ChatMessage struct {
|
||||
Role string
|
||||
Content string
|
||||
}
|
||||
|
||||
type RunRequest struct {
|
||||
Messages []ChatMessage
|
||||
Model string
|
||||
AgentID string
|
||||
RequestID string
|
||||
IdempotencyKey string
|
||||
TimeoutSeconds uint32
|
||||
Metadata map[string]string
|
||||
ToolsEnabled bool
|
||||
}
|
||||
|
||||
type RunResult struct {
|
||||
Content string
|
||||
DurationMS float64
|
||||
ToolsUsed []string
|
||||
}
|
||||
|
||||
type Runner interface {
|
||||
RunAgent(context.Context, RunRequest) (RunResult, error)
|
||||
}
|
||||
107
internal/profile/profile.go
Normal file
107
internal/profile/profile.go
Normal file
@ -0,0 +1,107 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
Version int `yaml:"version"`
|
||||
Agent struct {
|
||||
ID string `yaml:"id"`
|
||||
Identity struct {
|
||||
Name string `yaml:"name"`
|
||||
Aliases map[string]string `yaml:"aliases"`
|
||||
Role string `yaml:"role"`
|
||||
SelfAwareness string `yaml:"self_awareness"`
|
||||
} `yaml:"identity"`
|
||||
PublicProfile struct {
|
||||
Bio string `yaml:"bio"`
|
||||
Status string `yaml:"status"`
|
||||
} `yaml:"public_profile"`
|
||||
} `yaml:"agent"`
|
||||
Personality struct {
|
||||
Traits []string `yaml:"traits"`
|
||||
Style struct {
|
||||
Tone string `yaml:"tone"`
|
||||
Verbosity string `yaml:"verbosity"`
|
||||
Formality string `yaml:"formality"`
|
||||
} `yaml:"style"`
|
||||
Avoid []string `yaml:"avoid"`
|
||||
} `yaml:"personality"`
|
||||
Behavior struct {
|
||||
Language struct {
|
||||
MatchConversation bool `yaml:"match_conversation"`
|
||||
RussianDisplayName string `yaml:"russian_display_name"`
|
||||
EnglishDisplayName string `yaml:"english_display_name"`
|
||||
} `yaml:"language"`
|
||||
} `yaml:"behavior"`
|
||||
LLM struct {
|
||||
Response struct {
|
||||
Model string `yaml:"model"`
|
||||
} `yaml:"response"`
|
||||
} `yaml:"llm"`
|
||||
}
|
||||
|
||||
func Load(path string) (Profile, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Profile{}, err
|
||||
}
|
||||
var p Profile
|
||||
if err := yaml.Unmarshal(data, &p); err != nil {
|
||||
return Profile{}, err
|
||||
}
|
||||
if strings.TrimSpace(p.Agent.ID) == "" {
|
||||
return Profile{}, fmt.Errorf("profile %s: agent.id is required", path)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// SystemPrompt builds a compact persona prompt for Tasks board mention replies.
|
||||
func (p Profile) SystemPrompt() string {
|
||||
name := p.Agent.Identity.Name
|
||||
if name == "" {
|
||||
name = p.Agent.ID
|
||||
}
|
||||
ru := p.Behavior.Language.RussianDisplayName
|
||||
if ru == "" {
|
||||
ru = p.Agent.Identity.Aliases["ru"]
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("You are %s", name))
|
||||
if ru != "" && !strings.EqualFold(ru, name) {
|
||||
b.WriteString(fmt.Sprintf(" / %s", ru))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" (agent_id=%s), an AI agent on the l-ai Tasks board.\n", p.Agent.ID))
|
||||
if bio := strings.TrimSpace(p.Agent.PublicProfile.Bio); bio != "" {
|
||||
b.WriteString(bio)
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
if role := strings.TrimSpace(p.Agent.Identity.Role); role != "" {
|
||||
b.WriteString("Role: ")
|
||||
b.WriteString(role)
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
if len(p.Personality.Traits) > 0 {
|
||||
b.WriteString("Traits: ")
|
||||
b.WriteString(strings.Join(p.Personality.Traits, ", "))
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
if s := p.Personality.Style; s.Tone != "" || s.Verbosity != "" || s.Formality != "" {
|
||||
b.WriteString(fmt.Sprintf("Style: tone=%s verbosity=%s formality=%s\n", s.Tone, s.Verbosity, s.Formality))
|
||||
}
|
||||
if len(p.Personality.Avoid) > 0 {
|
||||
b.WriteString("Avoid: ")
|
||||
b.WriteString(strings.Join(p.Personality.Avoid, "; "))
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteString("You reply only to task comment @mentions. Be helpful and concise. ")
|
||||
b.WriteString("Match the language of the triggering comment (Russian or English). ")
|
||||
b.WriteString("Do not invent task state that is not in the provided context. ")
|
||||
b.WriteString("Output only the comment body to post — no preamble or quotes.\n")
|
||||
return b.String()
|
||||
}
|
||||
201
internal/tasksboard/loop.go
Normal file
201
internal/tasksboard/loop.go
Normal file
@ -0,0 +1,201 @@
|
||||
package tasksboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"l-ai-agent-kaya/internal/inference"
|
||||
"l-ai-agent-kaya/internal/profile"
|
||||
"l-ai-agent-kaya/internal/tasksws"
|
||||
)
|
||||
|
||||
const agentID = "kaya"
|
||||
|
||||
// Loop listens to Tasks board WS and replies to @kaya mentions.
|
||||
type Loop struct {
|
||||
WSURL string
|
||||
Token string
|
||||
REST *Client
|
||||
Core inference.Runner
|
||||
Profile profile.Profile
|
||||
Model string
|
||||
ResponseTimeout time.Duration
|
||||
Log *slog.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
seen map[string]struct{}
|
||||
seenQ []string
|
||||
maxSeen int
|
||||
}
|
||||
|
||||
func (l *Loop) log() *slog.Logger {
|
||||
if l.Log != nil {
|
||||
return l.Log
|
||||
}
|
||||
return slog.Default()
|
||||
}
|
||||
|
||||
// Run blocks until ctx is cancelled.
|
||||
func (l *Loop) Run(ctx context.Context) {
|
||||
if l.seen == nil {
|
||||
l.seen = map[string]struct{}{}
|
||||
}
|
||||
if l.maxSeen <= 0 {
|
||||
l.maxSeen = 4096
|
||||
}
|
||||
client := &tasksws.Client{
|
||||
URL: l.WSURL,
|
||||
Token: l.Token,
|
||||
Agent: agentID,
|
||||
Log: l.log(),
|
||||
OnEvent: func(ctx context.Context, ev tasksws.Event) error {
|
||||
return l.handle(ctx, ev)
|
||||
},
|
||||
}
|
||||
l.log().Info("tasks board loop starting", "ws", l.WSURL, "agent", agentID)
|
||||
client.Run(ctx)
|
||||
}
|
||||
|
||||
func (l *Loop) handle(ctx context.Context, ev tasksws.Event) error {
|
||||
if ev.Type != "comment.added" {
|
||||
return nil
|
||||
}
|
||||
if ev.Comment == nil || ev.Task == nil {
|
||||
return nil
|
||||
}
|
||||
if !tasksws.MentionsSelf(ev, agentID) {
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(ev.Comment.AuthorID, agentID) {
|
||||
return nil
|
||||
}
|
||||
commentID := ev.Comment.ID
|
||||
if commentID == "" {
|
||||
return nil
|
||||
}
|
||||
if !l.markSeen(commentID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
taskID := ev.Task.ID
|
||||
l.log().Info("mention reply", "comment_id", commentID, "task_id", taskID, "author", ev.Comment.AuthorID)
|
||||
|
||||
replyCtx, cancel := context.WithTimeout(ctx, l.ResponseTimeout)
|
||||
defer cancel()
|
||||
|
||||
task, err := l.REST.GetTask(replyCtx, taskID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get task: %w", err)
|
||||
}
|
||||
comments, err := l.REST.ListComments(replyCtx, taskID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list comments: %w", err)
|
||||
}
|
||||
|
||||
userPrompt := buildUserPrompt(task, comments, commentID)
|
||||
timeoutSec := uint32(l.ResponseTimeout.Seconds())
|
||||
if timeoutSec < 30 {
|
||||
timeoutSec = 30
|
||||
}
|
||||
model := l.Model
|
||||
if model == "" && l.Profile.LLM.Response.Model != "" {
|
||||
model = l.Profile.LLM.Response.Model
|
||||
}
|
||||
if model == "" {
|
||||
model = "deepseek-v4-flash"
|
||||
}
|
||||
|
||||
res, err := l.Core.RunAgent(replyCtx, inference.RunRequest{
|
||||
Messages: []inference.ChatMessage{
|
||||
{Role: "system", Content: l.Profile.SystemPrompt()},
|
||||
{Role: "user", Content: userPrompt},
|
||||
},
|
||||
Model: model,
|
||||
AgentID: agentID,
|
||||
RequestID: "mention-reply:" + commentID,
|
||||
IdempotencyKey: "mention-reply:" + commentID,
|
||||
TimeoutSeconds: timeoutSec,
|
||||
Metadata: map[string]string{"trace_reason": "kaya_mention_reply", "comment_id": commentID, "task_id": taskID},
|
||||
ToolsEnabled: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("RunAgent: %w", err)
|
||||
}
|
||||
body := strings.TrimSpace(res.Content)
|
||||
if body == "" {
|
||||
return fmt.Errorf("empty agent reply for comment %s", commentID)
|
||||
}
|
||||
|
||||
_, err = l.REST.PostComment(replyCtx, taskID, body, "mention-reply:"+commentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("post comment: %w", err)
|
||||
}
|
||||
l.log().Info("mention reply posted", "comment_id", commentID, "task_id", taskID, "duration_ms", res.DurationMS)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Loop) markSeen(id string) bool {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if _, ok := l.seen[id]; ok {
|
||||
return false
|
||||
}
|
||||
l.seen[id] = struct{}{}
|
||||
l.seenQ = append(l.seenQ, id)
|
||||
for len(l.seenQ) > l.maxSeen {
|
||||
old := l.seenQ[0]
|
||||
l.seenQ = l.seenQ[1:]
|
||||
delete(l.seen, old)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func buildUserPrompt(task Task, comments []Comment, triggerID string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("Task context:\n")
|
||||
b.WriteString(fmt.Sprintf("- id: %s\n", task.ID))
|
||||
b.WriteString(fmt.Sprintf("- title: %s\n", task.Title))
|
||||
b.WriteString(fmt.Sprintf("- status: %s\n", task.Status))
|
||||
b.WriteString(fmt.Sprintf("- priority: %d\n", task.Priority))
|
||||
if task.Project != nil {
|
||||
b.WriteString(fmt.Sprintf("- project: %s (%s)\n", task.Project.Name, task.Project.Slug))
|
||||
}
|
||||
if task.Assignee != nil {
|
||||
name := task.Assignee.DisplayName
|
||||
if name == "" {
|
||||
name = task.Assignee.ID
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("- assignee: %s/%s (%s)\n", task.Assignee.Type, task.Assignee.ID, name))
|
||||
}
|
||||
if desc := strings.TrimSpace(task.Description); desc != "" {
|
||||
b.WriteString("- description:\n")
|
||||
b.WriteString(trimRunes(desc, 4000))
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteString("\nComment thread (oldest first):\n")
|
||||
for _, c := range comments {
|
||||
marker := ""
|
||||
if c.ID == triggerID {
|
||||
marker = " ← reply to this"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("[%s] %s/%s:%s\n%s\n\n",
|
||||
c.CreatedAt.UTC().Format(time.RFC3339),
|
||||
c.AuthorType, c.AuthorID, marker,
|
||||
trimRunes(c.Body, 2000),
|
||||
))
|
||||
}
|
||||
b.WriteString("Write a helpful board comment reply to the marked mention.\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func trimRunes(s string, max int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= max {
|
||||
return s
|
||||
}
|
||||
return string(r[:max]) + "…"
|
||||
}
|
||||
119
internal/tasksboard/rest.go
Normal file
119
internal/tasksboard/rest.go
Normal file
@ -0,0 +1,119 @@
|
||||
package tasksboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
Token string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func (c *Client) http() *http.Client {
|
||||
if c.HTTPClient != nil {
|
||||
return c.HTTPClient
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Priority int `json:"priority"`
|
||||
Project *struct {
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
} `json:"project,omitempty"`
|
||||
Assignee *struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
} `json:"assignee,omitempty"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID string `json:"id"`
|
||||
TaskID string `json:"task_id"`
|
||||
AuthorType string `json:"author_type"`
|
||||
AuthorID string `json:"author_id"`
|
||||
Body string `json:"body"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (c *Client) GetTask(ctx context.Context, taskID string) (Task, error) {
|
||||
var t Task
|
||||
if err := c.doJSON(ctx, http.MethodGet, "/api/v1/tasks/"+url.PathEscape(taskID), "", nil, &t); err != nil {
|
||||
return Task{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListComments(ctx context.Context, taskID string) ([]Comment, error) {
|
||||
var out struct {
|
||||
Comments []Comment `json:"comments"`
|
||||
}
|
||||
if err := c.doJSON(ctx, http.MethodGet, "/api/v1/tasks/"+url.PathEscape(taskID)+"/comments", "", nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.Comments, nil
|
||||
}
|
||||
|
||||
func (c *Client) PostComment(ctx context.Context, taskID, body, idempotencyKey string) (Comment, error) {
|
||||
var out Comment
|
||||
payload := map[string]string{"body": body}
|
||||
if err := c.doJSON(ctx, http.MethodPost, "/api/v1/tasks/"+url.PathEscape(taskID)+"/comments", idempotencyKey, payload, &out); err != nil {
|
||||
return Comment{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Client) doJSON(ctx context.Context, method, path, idempotencyKey string, body any, dst any) error {
|
||||
base := strings.TrimRight(c.BaseURL, "/")
|
||||
var rdr io.Reader
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rdr = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, base+path, rdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if idempotencyKey != "" {
|
||||
req.Header.Set("Idempotency-Key", idempotencyKey)
|
||||
}
|
||||
resp, err := c.http().Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("tasks %s %s: status %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(data)))
|
||||
}
|
||||
if dst == nil || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, dst)
|
||||
}
|
||||
185
internal/tasksws/client.go
Normal file
185
internal/tasksws/client.go
Normal file
@ -0,0 +1,185 @@
|
||||
// Package tasksws is a reconnecting WebSocket client for l-ai-tasks board events.
|
||||
package tasksws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// Event mirrors l-ai-tasks realtime.Event JSON.
|
||||
type Event struct {
|
||||
Type string `json:"type"`
|
||||
Seq int64 `json:"seq"`
|
||||
At time.Time `json:"at"`
|
||||
Project *Ref `json:"project,omitempty"`
|
||||
Task *TaskRef `json:"task,omitempty"`
|
||||
Comment *Comment `json:"comment,omitempty"`
|
||||
Mentions []Mention `json:"mentions,omitempty"`
|
||||
}
|
||||
|
||||
type Ref struct {
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type TaskRef struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
AssigneeID string `json:"assignee_id,omitempty"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID string `json:"id"`
|
||||
Body string `json:"body"`
|
||||
AuthorType string `json:"author_type"`
|
||||
AuthorID string `json:"author_id"`
|
||||
}
|
||||
|
||||
type Mention struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// Handler is called for each board event (after ready).
|
||||
type Handler func(ctx context.Context, ev Event) error
|
||||
|
||||
// Client reconnects forever until ctx cancel.
|
||||
type Client struct {
|
||||
URL string
|
||||
Token string
|
||||
Agent string // logical id for filtering (nora, coordinator, kaya)
|
||||
Log *slog.Logger
|
||||
OnEvent Handler
|
||||
}
|
||||
|
||||
// Run blocks until ctx is done.
|
||||
func (c *Client) Run(ctx context.Context) {
|
||||
log := c.Log
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
var since int64
|
||||
attempt := 0
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
err := c.session(ctx, &since)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
attempt++
|
||||
delay := backoff(attempt)
|
||||
log.Warn("tasks ws disconnected; reconnecting", "error", err, "attempt", attempt, "delay", delay, "since", since)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) session(ctx context.Context, since *int64) error {
|
||||
u, err := url.Parse(c.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("since", strconv.FormatInt(*since, 10))
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
hdr := http.Header{}
|
||||
if c.Token != "" {
|
||||
hdr.Set("Authorization", "Bearer "+c.Token)
|
||||
}
|
||||
dialer := websocket.Dialer{HandshakeTimeout: 15 * time.Second}
|
||||
conn, _, err := dialer.DialContext(ctx, u.String(), hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(90 * time.Second))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(90 * time.Second))
|
||||
return nil
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(90 * time.Second))
|
||||
|
||||
var probe struct {
|
||||
Type string `json:"type"`
|
||||
Seq int64 `json:"seq"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &probe); err != nil {
|
||||
continue
|
||||
}
|
||||
if probe.Type == "ready" {
|
||||
continue
|
||||
}
|
||||
var ev Event
|
||||
if err := json.Unmarshal(data, &ev); err != nil {
|
||||
continue
|
||||
}
|
||||
if ev.Seq > *since {
|
||||
*since = ev.Seq
|
||||
}
|
||||
if c.OnEvent != nil {
|
||||
if err := c.OnEvent(ctx, ev); err != nil && c.Log != nil {
|
||||
c.Log.Warn("tasks ws handler", "error", err, "type", ev.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func backoff(attempt int) time.Duration {
|
||||
if attempt < 1 {
|
||||
attempt = 1
|
||||
}
|
||||
if attempt > 6 {
|
||||
attempt = 6
|
||||
}
|
||||
base := 200 * time.Millisecond
|
||||
d := base * time.Duration(1<<uint(attempt-1))
|
||||
if d > 5*time.Second {
|
||||
d = 5 * time.Second
|
||||
}
|
||||
jitter := time.Duration(rand.Int63n(int64(d/2) + 1))
|
||||
return d/2 + jitter
|
||||
}
|
||||
|
||||
// MentionsSelf reports whether ev.mentions includes agentID.
|
||||
func MentionsSelf(ev Event, agentID string) bool {
|
||||
for _, m := range ev.Mentions {
|
||||
if m.Type == "agent" && m.ID == agentID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ErrAuth is returned conceptually for logging; dial errors wrap status.
|
||||
func FormatDial(url, agent string) string {
|
||||
return fmt.Sprintf("tasksws agent=%s url=%s", agent, url)
|
||||
}
|
||||
48
profiles/kaya.yaml
Normal file
48
profiles/kaya.yaml
Normal file
@ -0,0 +1,48 @@
|
||||
version: 1
|
||||
|
||||
agent:
|
||||
id: kaya
|
||||
identity:
|
||||
name: Kaya
|
||||
aliases:
|
||||
ru: Кая
|
||||
role: BoardAssistant
|
||||
self_awareness: ai_agent
|
||||
public_profile:
|
||||
bio: "Friendly helper on the Tasks board — answers @kaya mentions in comments."
|
||||
status: Ready
|
||||
|
||||
personality:
|
||||
traits:
|
||||
- friendly
|
||||
- helpful
|
||||
- clear
|
||||
- concise
|
||||
- pragmatic
|
||||
- mildly_warm
|
||||
style:
|
||||
tone: friendly_helpful
|
||||
verbosity: low_to_medium
|
||||
formality: informal_professional
|
||||
avoid:
|
||||
- corporate_language
|
||||
- fake_certainty
|
||||
- excessive_disclaimers
|
||||
- long_unsolicited_essays
|
||||
- repeating_the_whole_thread
|
||||
|
||||
behavior:
|
||||
language:
|
||||
match_conversation: true
|
||||
russian_display_name: Кая
|
||||
english_display_name: Kaya
|
||||
board_comments:
|
||||
reply_to_mentions_only: true
|
||||
keep_replies_short: true
|
||||
|
||||
llm:
|
||||
response:
|
||||
model: deepseek-v4-flash
|
||||
|
||||
tools:
|
||||
enabled: false
|
||||
1854
proto/l-ai-core-proto/ai.pb.go
Normal file
1854
proto/l-ai-core-proto/ai.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
243
proto/l-ai-core-proto/ai.proto
Normal file
243
proto/l-ai-core-proto/ai.proto
Normal file
@ -0,0 +1,243 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package ai;
|
||||
|
||||
option go_package = "git.be.kaien.ru/l-ai/l-ai-core-proto/proto";
|
||||
|
||||
// AIRequest содержит запрос к AI модели
|
||||
message AIRequest {
|
||||
string text = 1; // Текст запроса
|
||||
string bot_username = 2; // Username бота для упоминаний
|
||||
string model_name = 3; // Название модели (по умолчанию "deepseek-v4-flash")
|
||||
bool enable_reasoning = 4; // Включить режим рассуждений (по умолчанию false)
|
||||
int64 global_user_id = 5; // Global user ID для получения доп. данных (профиль, воспоминания)
|
||||
AnswerGenerationOptions options = 6; // Дополнительные опции генерации ответа
|
||||
int64 telegram_user_id = 7; // Telegram user ID для применения пользовательских настроек (стиль ответа)
|
||||
int64 steam_user_id = 8; // SteamID64 отправителя для живых Steam-диалогов (l-ai-steam) - тот же "сигнал живого чата", что telegram_user_id, для второй платформы
|
||||
bool supports_deferred = 9; // Клиент умеет обрабатывать status=PENDING + GetAnswerJob (async тяжёлые тулзы). Старые клиенты не ставят — тяжёлые тулзы исполняются синхронно, как раньше.
|
||||
}
|
||||
|
||||
// AnswerGenerationOptions содержит дополнительные опции для генерации ответа
|
||||
message AnswerGenerationOptions {
|
||||
bool include_user_memories = 1; // Включить воспоминания о пользователе в промпт
|
||||
}
|
||||
|
||||
// AIResponse содержит ответ от AI модели
|
||||
message AIResponse {
|
||||
string content = 1; // Текст ответа (при status=PENDING — interim-сообщение)
|
||||
string error = 2; // Текст ошибки, если произошла
|
||||
double duration_ms = 3; // Время выполнения в миллисекундах
|
||||
string reasoning_content = 4; // Текст рассуждений (если включён режим reasoning)
|
||||
AnswerStatus status = 5; // COMPLETE (или UNSPECIFIED для старых клиентов) — content финальный;
|
||||
// PENDING — запущена долгая job, content это interim, финал тянуть через GetAnswerJob(job_id)
|
||||
string job_id = 6; // Идентификатор отложенной job (только при status=PENDING)
|
||||
}
|
||||
|
||||
// AnswerStatus описывает, готов ли ответ синхронно или отложен в job.
|
||||
enum AnswerStatus {
|
||||
ANSWER_STATUS_UNSPECIFIED = 0; // трактуется как COMPLETE (обратная совместимость)
|
||||
ANSWER_STATUS_COMPLETE = 1; // content — финальный ответ
|
||||
ANSWER_STATUS_PENDING = 2; // запущена долгая job; поллить GetAnswerJob(job_id)
|
||||
}
|
||||
|
||||
// GetAnswerJobRequest запрашивает статус отложенной генерации ответа.
|
||||
message GetAnswerJobRequest {
|
||||
string job_id = 1;
|
||||
}
|
||||
|
||||
// AnswerJobState — состояние отложенной генерации ответа.
|
||||
// Значения 0-4 исторические (GenerateAnswer deferred). Значения 5-7 добавлены для
|
||||
// durable RunAgent jobs; старые клиенты их не создают и потому не увидят. RUNNING
|
||||
// для старого клиента семантически эквивалентен PENDING (job ещё не терминальна).
|
||||
enum AnswerJobState {
|
||||
ANSWER_JOB_STATE_UNSPECIFIED = 0;
|
||||
ANSWER_JOB_STATE_PENDING = 1; // ещё не взята в работу / выполняется (evolver работает, модель дописывает)
|
||||
ANSWER_JOB_STATE_DONE = 2; // готово, content заполнен
|
||||
ANSWER_JOB_STATE_FAILED = 3; // ошибка, error заполнен
|
||||
ANSWER_JOB_STATE_NOT_FOUND = 4; // неизвестный/протухший job_id (напр. после рестарта core)
|
||||
ANSWER_JOB_STATE_RUNNING = 5; // взята воркером (lease активен), tool loop исполняется
|
||||
ANSWER_JOB_STATE_CANCELLED = 6; // отменена через CancelAnswerJob
|
||||
ANSWER_JOB_STATE_EXPIRED = 7; // истёк expiration до достижения терминального состояния
|
||||
}
|
||||
|
||||
// AnswerJobResponse — результат опроса отложенной генерации.
|
||||
message AnswerJobResponse {
|
||||
AnswerJobState state = 1;
|
||||
string content = 2; // финальный ответ, когда DONE
|
||||
string error = 3; // причина, когда FAILED
|
||||
double duration_ms = 4; // суммарное время генерации, когда DONE
|
||||
// Authoritative usage завершённой deferred RunAgent job — тот же счётчик, что
|
||||
// возвращает synchronous RunAgent (реальные iterations/tool calls core, не
|
||||
// self-reported из ответа модели). Для legacy GenerateAnswer jobs не заполняется.
|
||||
AgentUsage agent_usage = 5;
|
||||
}
|
||||
|
||||
// CreateSessionRequest для создания новой AI сессии
|
||||
message CreateSessionRequest {
|
||||
string user_id = 1; // Идентификатор пользователя
|
||||
string bot_username = 2; // Username бота для упоминаний
|
||||
string model_name = 3; // Название модели (по умолчанию "deepseek-v4-flash")
|
||||
int64 ttl_seconds = 4; // Время жизни сессии в секундах (по умолчанию 3600 = 1 час)
|
||||
}
|
||||
|
||||
// CreateSessionResponse содержит ID созданной сессии
|
||||
message CreateSessionResponse {
|
||||
string session_id = 1; // ID сессии
|
||||
string status = 2; // Статус сессии
|
||||
string error = 3; // Текст ошибки, если произошла
|
||||
}
|
||||
|
||||
// AskInSessionRequest для запроса в рамках сессии
|
||||
message AskInSessionRequest {
|
||||
string session_id = 1; // ID сессии
|
||||
string text = 2; // Текст запроса
|
||||
bool enable_reasoning = 3; // Включить режим рассуждений (по умолчанию false)
|
||||
int64 global_user_id = 4; // Global user ID для получения воспоминаний
|
||||
AnswerGenerationOptions options = 5; // Дополнительные опции генерации
|
||||
}
|
||||
|
||||
// SessionStatusResponse содержит статус сессии и ответ AI
|
||||
message SessionStatusResponse {
|
||||
string session_id = 1; // ID сессии
|
||||
string status = 2; // Статус сессии (active, expired, closed)
|
||||
string content = 3; // Текст ответа от AI
|
||||
string error = 4; // Текст ошибки, если произошла
|
||||
double duration_ms = 5; // Время выполнения в миллисекундах
|
||||
string reasoning_content = 6; // Текст рассуждений (если включён режим reasoning)
|
||||
}
|
||||
|
||||
// StatelessMessage одно сообщение для stateless запроса (role + content)
|
||||
message StatelessMessage {
|
||||
string role = 1; // "system", "user", "assistant"
|
||||
string content = 2; // Текст сообщения
|
||||
}
|
||||
|
||||
// StatelessRequest для stateless генерации с полным контролем над messages
|
||||
message StatelessRequest {
|
||||
repeated StatelessMessage messages = 1; // Массив сообщений (system + history + user)
|
||||
string model_name = 2; // Название модели (по умолчанию "deepseek-v4-flash")
|
||||
}
|
||||
|
||||
// HealthCheckRequest для проверки здоровья сервиса
|
||||
message HealthCheckRequest {}
|
||||
|
||||
// HealthCheckResponse для ответа на проверку здоровья
|
||||
message HealthCheckResponse {
|
||||
bool serving = 1;
|
||||
string version = 2;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RunAgent: надёжный агентный вызов (server-side tool filtering, deferred jobs).
|
||||
// В отличие от GenerateAnswer, НЕ подмешивает Telegram/Steam-персонализацию и не
|
||||
// требует telegram_user_id/steam_user_id. Вызывающая сторона полностью владеет
|
||||
// массивом messages (включая system). Требует service-to-service авторизации
|
||||
// (Bearer service-token в gRPC metadata) — agent_id/tool_profile сами по себе не
|
||||
// являются авторизацией.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// AgentToolLoopOptions задаёт лимиты tool loop. Значения клиента могут только
|
||||
// сузить серверные потолки (max_tool_iterations/max_total_tool_calls ≤ server max).
|
||||
message AgentToolLoopOptions {
|
||||
bool tools_enabled = 1; // false — модель отвечает без tools
|
||||
uint32 max_tool_iterations = 2; // потолок round-trip'ов модель↔tools (0 = серверный default)
|
||||
uint32 max_total_tool_calls = 3; // потолок РЕАЛЬНЫХ tool call'ов, не итераций (0 = серверный default)
|
||||
uint32 tool_timeout_seconds = 4; // таймаут одного tool call (0 = серверный default)
|
||||
uint32 total_timeout_seconds = 5; // wall-clock таймаут всего вызова (0 = серверный default), независим от budget
|
||||
}
|
||||
|
||||
// AgentOutputFormat — требуемый формат финального ответа.
|
||||
enum AgentOutputFormat {
|
||||
AGENT_OUTPUT_FORMAT_UNSPECIFIED = 0; // трактуется как TEXT
|
||||
AGENT_OUTPUT_FORMAT_TEXT = 1; // произвольный текст
|
||||
AGENT_OUTPUT_FORMAT_JSON = 2; // должен быть валидный JSON (без schema-проверки)
|
||||
AGENT_OUTPUT_FORMAT_JSON_SCHEMA = 3; // валидный JSON, проходящий json_schema
|
||||
}
|
||||
|
||||
// AgentResponseContract описывает требования к структурированному ответу.
|
||||
message AgentResponseContract {
|
||||
AgentOutputFormat format = 1;
|
||||
string json_schema = 2; // JSON Schema (draft-07), обязателен при format=JSON_SCHEMA
|
||||
bool repair_invalid_output = 3; // при провале валидации — один repair-запрос модели (без новых tools)
|
||||
}
|
||||
|
||||
// AgentRequest — запрос агентного вызова.
|
||||
message AgentRequest {
|
||||
repeated StatelessMessage messages = 1; // полный контекст, включая system
|
||||
string model_name = 2; // модель (по умолчанию deepseek-v4-flash)
|
||||
bool enable_reasoning = 3; // reasoning не попадает в обычный content
|
||||
|
||||
string agent_id = 4; // идентификатор логического агента (для логов/метрик, НЕ авторизация)
|
||||
string tool_profile = 5; // серверный профиль tools (НЕ авторизация; проверяется против credential)
|
||||
repeated string requested_tools = 6; // сужение профиля: effective = profile.allowed ∩ requested; пусто = все allowed
|
||||
|
||||
AgentToolLoopOptions tool_loop = 7;
|
||||
AgentResponseContract response_contract = 8;
|
||||
|
||||
string request_id = 9; // сквозной идентификатор для трассировки
|
||||
string idempotency_key = 10; // повторный RunAgent с тем же ключом не создаёт дубликат job
|
||||
bool supports_deferred = 11; // клиент умеет status=PENDING + GetAnswerJob для тяжёлых операций
|
||||
|
||||
map<string, string> metadata = 12; // произвольные метки (не логируются как секреты)
|
||||
}
|
||||
|
||||
// AgentUsage — счётчики использования за вызов.
|
||||
message AgentUsage {
|
||||
uint32 tool_iterations = 1; // сколько round-trip'ов модель↔tools произошло
|
||||
uint32 total_tool_calls = 2; // сколько РЕАЛЬНЫХ tool call'ов исполнено (3 calls в одной итерации = 3)
|
||||
repeated string tools_used = 3;
|
||||
uint64 input_tokens = 4;
|
||||
uint64 output_tokens = 5;
|
||||
}
|
||||
|
||||
// AgentResponse — результат агентного вызова.
|
||||
message AgentResponse {
|
||||
AnswerStatus status = 1; // COMPLETE — content финальный; PENDING — тянуть через GetAnswerJob(job_id)
|
||||
string content = 2;
|
||||
string error = 3;
|
||||
double duration_ms = 4;
|
||||
string job_id = 5; // при status=PENDING
|
||||
AgentUsage usage = 6;
|
||||
}
|
||||
|
||||
// CancelAnswerJobRequest — запрос отмены отложенной job (идемпотентный).
|
||||
message CancelAnswerJobRequest {
|
||||
string job_id = 1;
|
||||
string reason = 2; // причина отмены (сохраняется), опционально
|
||||
}
|
||||
|
||||
// CancelAnswerJobResponse — результат отмены.
|
||||
message CancelAnswerJobResponse {
|
||||
AnswerJobState state = 1; // состояние job после запроса (CANCELLED, либо терминальное, если уже завершилась)
|
||||
bool cancelled = 2; // true — отмена применена этим вызовом; false — job уже была терминальна/отменена
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
// AIService - основной сервис для работы с AI
|
||||
service AIService {
|
||||
// RunAgent — надёжный агентный вызов с серверной фильтрацией tools и deferred
|
||||
// jobs. Требует service-to-service авторизации.
|
||||
rpc RunAgent(AgentRequest) returns (AgentResponse);
|
||||
|
||||
// CancelAnswerJob отменяет отложенную job (идемпотентно). Требует авторизации.
|
||||
rpc CancelAnswerJob(CancelAnswerJobRequest) returns (CancelAnswerJobResponse);
|
||||
|
||||
// GenerateAnswer генерирует ответ на запрос
|
||||
rpc GenerateAnswer(AIRequest) returns (AIResponse);
|
||||
|
||||
// GenerateStateless генерирует ответ из массива messages (system+history+user)
|
||||
rpc GenerateStateless(StatelessRequest) returns (AIResponse);
|
||||
|
||||
// HealthCheck проверяет здоровье сервиса
|
||||
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
|
||||
// CreateSession созда новую AI сессию
|
||||
rpc CreateSession(CreateSessionRequest) returns (CreateSessionResponse);
|
||||
|
||||
// AskInSession отправляет запрос в рамках существующей сессии
|
||||
rpc AskInSession(AskInSessionRequest) returns (SessionStatusResponse);
|
||||
|
||||
// GetAnswerJob возвращает статус отложенной генерации ответа (когда GenerateAnswer
|
||||
// вернул status=PENDING). Клиент поллит его до DONE/FAILED и постит финал.
|
||||
rpc GetAnswerJob(GetAnswerJobRequest) returns (AnswerJobResponse);
|
||||
}
|
||||
411
proto/l-ai-core-proto/ai_grpc.pb.go
Normal file
411
proto/l-ai-core-proto/ai_grpc.pb.go
Normal file
@ -0,0 +1,411 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.1
|
||||
// - protoc v7.34.0
|
||||
// source: ai.proto
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
AIService_RunAgent_FullMethodName = "/ai.AIService/RunAgent"
|
||||
AIService_CancelAnswerJob_FullMethodName = "/ai.AIService/CancelAnswerJob"
|
||||
AIService_GenerateAnswer_FullMethodName = "/ai.AIService/GenerateAnswer"
|
||||
AIService_GenerateStateless_FullMethodName = "/ai.AIService/GenerateStateless"
|
||||
AIService_HealthCheck_FullMethodName = "/ai.AIService/HealthCheck"
|
||||
AIService_CreateSession_FullMethodName = "/ai.AIService/CreateSession"
|
||||
AIService_AskInSession_FullMethodName = "/ai.AIService/AskInSession"
|
||||
AIService_GetAnswerJob_FullMethodName = "/ai.AIService/GetAnswerJob"
|
||||
)
|
||||
|
||||
// AIServiceClient is the client API for AIService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// AIService - основной сервис для работы с AI
|
||||
type AIServiceClient interface {
|
||||
// RunAgent — надёжный агентный вызов с серверной фильтрацией tools и deferred
|
||||
// jobs. Требует service-to-service авторизации.
|
||||
RunAgent(ctx context.Context, in *AgentRequest, opts ...grpc.CallOption) (*AgentResponse, error)
|
||||
// CancelAnswerJob отменяет отложенную job (идемпотентно). Требует авторизации.
|
||||
CancelAnswerJob(ctx context.Context, in *CancelAnswerJobRequest, opts ...grpc.CallOption) (*CancelAnswerJobResponse, error)
|
||||
// GenerateAnswer генерирует ответ на запрос
|
||||
GenerateAnswer(ctx context.Context, in *AIRequest, opts ...grpc.CallOption) (*AIResponse, error)
|
||||
// GenerateStateless генерирует ответ из массива messages (system+history+user)
|
||||
GenerateStateless(ctx context.Context, in *StatelessRequest, opts ...grpc.CallOption) (*AIResponse, error)
|
||||
// HealthCheck проверяет здоровье сервиса
|
||||
HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error)
|
||||
// CreateSession созда новую AI сессию
|
||||
CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*CreateSessionResponse, error)
|
||||
// AskInSession отправляет запрос в рамках существующей сессии
|
||||
AskInSession(ctx context.Context, in *AskInSessionRequest, opts ...grpc.CallOption) (*SessionStatusResponse, error)
|
||||
// GetAnswerJob возвращает статус отложенной генерации ответа (когда GenerateAnswer
|
||||
// вернул status=PENDING). Клиент поллит его до DONE/FAILED и постит финал.
|
||||
GetAnswerJob(ctx context.Context, in *GetAnswerJobRequest, opts ...grpc.CallOption) (*AnswerJobResponse, error)
|
||||
}
|
||||
|
||||
type aIServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAIServiceClient(cc grpc.ClientConnInterface) AIServiceClient {
|
||||
return &aIServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *aIServiceClient) RunAgent(ctx context.Context, in *AgentRequest, opts ...grpc.CallOption) (*AgentResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AgentResponse)
|
||||
err := c.cc.Invoke(ctx, AIService_RunAgent_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *aIServiceClient) CancelAnswerJob(ctx context.Context, in *CancelAnswerJobRequest, opts ...grpc.CallOption) (*CancelAnswerJobResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CancelAnswerJobResponse)
|
||||
err := c.cc.Invoke(ctx, AIService_CancelAnswerJob_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *aIServiceClient) GenerateAnswer(ctx context.Context, in *AIRequest, opts ...grpc.CallOption) (*AIResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AIResponse)
|
||||
err := c.cc.Invoke(ctx, AIService_GenerateAnswer_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *aIServiceClient) GenerateStateless(ctx context.Context, in *StatelessRequest, opts ...grpc.CallOption) (*AIResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AIResponse)
|
||||
err := c.cc.Invoke(ctx, AIService_GenerateStateless_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *aIServiceClient) HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(HealthCheckResponse)
|
||||
err := c.cc.Invoke(ctx, AIService_HealthCheck_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *aIServiceClient) CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*CreateSessionResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CreateSessionResponse)
|
||||
err := c.cc.Invoke(ctx, AIService_CreateSession_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *aIServiceClient) AskInSession(ctx context.Context, in *AskInSessionRequest, opts ...grpc.CallOption) (*SessionStatusResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SessionStatusResponse)
|
||||
err := c.cc.Invoke(ctx, AIService_AskInSession_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *aIServiceClient) GetAnswerJob(ctx context.Context, in *GetAnswerJobRequest, opts ...grpc.CallOption) (*AnswerJobResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AnswerJobResponse)
|
||||
err := c.cc.Invoke(ctx, AIService_GetAnswerJob_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AIServiceServer is the server API for AIService service.
|
||||
// All implementations must embed UnimplementedAIServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// AIService - основной сервис для работы с AI
|
||||
type AIServiceServer interface {
|
||||
// RunAgent — надёжный агентный вызов с серверной фильтрацией tools и deferred
|
||||
// jobs. Требует service-to-service авторизации.
|
||||
RunAgent(context.Context, *AgentRequest) (*AgentResponse, error)
|
||||
// CancelAnswerJob отменяет отложенную job (идемпотентно). Требует авторизации.
|
||||
CancelAnswerJob(context.Context, *CancelAnswerJobRequest) (*CancelAnswerJobResponse, error)
|
||||
// GenerateAnswer генерирует ответ на запрос
|
||||
GenerateAnswer(context.Context, *AIRequest) (*AIResponse, error)
|
||||
// GenerateStateless генерирует ответ из массива messages (system+history+user)
|
||||
GenerateStateless(context.Context, *StatelessRequest) (*AIResponse, error)
|
||||
// HealthCheck проверяет здоровье сервиса
|
||||
HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
|
||||
// CreateSession созда новую AI сессию
|
||||
CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error)
|
||||
// AskInSession отправляет запрос в рамках существующей сессии
|
||||
AskInSession(context.Context, *AskInSessionRequest) (*SessionStatusResponse, error)
|
||||
// GetAnswerJob возвращает статус отложенной генерации ответа (когда GenerateAnswer
|
||||
// вернул status=PENDING). Клиент поллит его до DONE/FAILED и постит финал.
|
||||
GetAnswerJob(context.Context, *GetAnswerJobRequest) (*AnswerJobResponse, error)
|
||||
mustEmbedUnimplementedAIServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedAIServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedAIServiceServer struct{}
|
||||
|
||||
func (UnimplementedAIServiceServer) RunAgent(context.Context, *AgentRequest) (*AgentResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method RunAgent not implemented")
|
||||
}
|
||||
func (UnimplementedAIServiceServer) CancelAnswerJob(context.Context, *CancelAnswerJobRequest) (*CancelAnswerJobResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CancelAnswerJob not implemented")
|
||||
}
|
||||
func (UnimplementedAIServiceServer) GenerateAnswer(context.Context, *AIRequest) (*AIResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GenerateAnswer not implemented")
|
||||
}
|
||||
func (UnimplementedAIServiceServer) GenerateStateless(context.Context, *StatelessRequest) (*AIResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GenerateStateless not implemented")
|
||||
}
|
||||
func (UnimplementedAIServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method HealthCheck not implemented")
|
||||
}
|
||||
func (UnimplementedAIServiceServer) CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateSession not implemented")
|
||||
}
|
||||
func (UnimplementedAIServiceServer) AskInSession(context.Context, *AskInSessionRequest) (*SessionStatusResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AskInSession not implemented")
|
||||
}
|
||||
func (UnimplementedAIServiceServer) GetAnswerJob(context.Context, *GetAnswerJobRequest) (*AnswerJobResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetAnswerJob not implemented")
|
||||
}
|
||||
func (UnimplementedAIServiceServer) mustEmbedUnimplementedAIServiceServer() {}
|
||||
func (UnimplementedAIServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeAIServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AIServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAIServiceServer interface {
|
||||
mustEmbedUnimplementedAIServiceServer()
|
||||
}
|
||||
|
||||
func RegisterAIServiceServer(s grpc.ServiceRegistrar, srv AIServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedAIServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&AIService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AIService_RunAgent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AgentRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AIServiceServer).RunAgent(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AIService_RunAgent_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AIServiceServer).RunAgent(ctx, req.(*AgentRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AIService_CancelAnswerJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CancelAnswerJobRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AIServiceServer).CancelAnswerJob(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AIService_CancelAnswerJob_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AIServiceServer).CancelAnswerJob(ctx, req.(*CancelAnswerJobRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AIService_GenerateAnswer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AIRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AIServiceServer).GenerateAnswer(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AIService_GenerateAnswer_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AIServiceServer).GenerateAnswer(ctx, req.(*AIRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AIService_GenerateStateless_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(StatelessRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AIServiceServer).GenerateStateless(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AIService_GenerateStateless_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AIServiceServer).GenerateStateless(ctx, req.(*StatelessRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AIService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(HealthCheckRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AIServiceServer).HealthCheck(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AIService_HealthCheck_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AIServiceServer).HealthCheck(ctx, req.(*HealthCheckRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AIService_CreateSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CreateSessionRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AIServiceServer).CreateSession(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AIService_CreateSession_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AIServiceServer).CreateSession(ctx, req.(*CreateSessionRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AIService_AskInSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AskInSessionRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AIServiceServer).AskInSession(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AIService_AskInSession_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AIServiceServer).AskInSession(ctx, req.(*AskInSessionRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AIService_GetAnswerJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetAnswerJobRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AIServiceServer).GetAnswerJob(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AIService_GetAnswerJob_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AIServiceServer).GetAnswerJob(ctx, req.(*GetAnswerJobRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AIService_ServiceDesc is the grpc.ServiceDesc for AIService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var AIService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "ai.AIService",
|
||||
HandlerType: (*AIServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "RunAgent",
|
||||
Handler: _AIService_RunAgent_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CancelAnswerJob",
|
||||
Handler: _AIService_CancelAnswerJob_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GenerateAnswer",
|
||||
Handler: _AIService_GenerateAnswer_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GenerateStateless",
|
||||
Handler: _AIService_GenerateStateless_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "HealthCheck",
|
||||
Handler: _AIService_HealthCheck_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CreateSession",
|
||||
Handler: _AIService_CreateSession_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AskInSession",
|
||||
Handler: _AIService_AskInSession_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetAnswerJob",
|
||||
Handler: _AIService_GetAnswerJob_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "ai.proto",
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user