From adb6c5ab8a6cecc22e41fd8e4132013313947ded Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 21 Sep 2026 23:07:24 +0300 Subject: [PATCH] Initial Kaya board-mention agent daemon. Thin WS client to l-ai-tasks that drafts short core RunAgent replies on @kaya mentions. Co-authored-by: Cursor --- .env.example | 17 + .gitignore | 8 + .gitmodules | 3 + .woodpecker.yml | 73 ++ Dockerfile | 17 + README.md | 45 + cmd/kaya/main.go | 86 ++ go.mod | 26 + go.sum | 74 ++ internal/config/config.go | 81 ++ internal/inference/core.go | 99 ++ internal/inference/types.go | 29 + internal/profile/profile.go | 107 ++ internal/tasksboard/loop.go | 201 +++ internal/tasksboard/rest.go | 119 ++ internal/tasksws/client.go | 185 +++ profiles/kaya.yaml | 48 + proto/l-ai-core-proto/ai.pb.go | 1854 +++++++++++++++++++++++++++ proto/l-ai-core-proto/ai.proto | 243 ++++ proto/l-ai-core-proto/ai_grpc.pb.go | 411 ++++++ 20 files changed, 3726 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .woodpecker.yml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 cmd/kaya/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/inference/core.go create mode 100644 internal/inference/types.go create mode 100644 internal/profile/profile.go create mode 100644 internal/tasksboard/loop.go create mode 100644 internal/tasksboard/rest.go create mode 100644 internal/tasksws/client.go create mode 100644 profiles/kaya.yaml create mode 100644 proto/l-ai-core-proto/ai.pb.go create mode 100644 proto/l-ai-core-proto/ai.proto create mode 100644 proto/l-ai-core-proto/ai_grpc.pb.go diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5b6f3a8 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85c1c29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.exe +*.test +*.out +.env +.idea/ +.vscode/ +bin/ +dist/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3651c15 --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..64efaf8 --- /dev/null +++ b/.woodpecker.yml @@ -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] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7ea73bc --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..5796b03 --- /dev/null +++ b/README.md @@ -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:` + +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`. diff --git a/cmd/kaya/main.go b/cmd/kaya/main.go new file mode 100644 index 0000000..bb6c2b3 --- /dev/null +++ b/cmd/kaya/main.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..08dca07 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c954819 --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..9d18168 --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/inference/core.go b/internal/inference/core.go new file mode 100644 index 0000000..8bc9a96 --- /dev/null +++ b/internal/inference/core.go @@ -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 +} diff --git a/internal/inference/types.go b/internal/inference/types.go new file mode 100644 index 0000000..575bd14 --- /dev/null +++ b/internal/inference/types.go @@ -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) +} diff --git a/internal/profile/profile.go b/internal/profile/profile.go new file mode 100644 index 0000000..265a434 --- /dev/null +++ b/internal/profile/profile.go @@ -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() +} diff --git a/internal/tasksboard/loop.go b/internal/tasksboard/loop.go new file mode 100644 index 0000000..5e4926c --- /dev/null +++ b/internal/tasksboard/loop.go @@ -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]) + "…" +} diff --git a/internal/tasksboard/rest.go b/internal/tasksboard/rest.go new file mode 100644 index 0000000..bfe5316 --- /dev/null +++ b/internal/tasksboard/rest.go @@ -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) +} diff --git a/internal/tasksws/client.go b/internal/tasksws/client.go new file mode 100644 index 0000000..92c68cc --- /dev/null +++ b/internal/tasksws/client.go @@ -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< 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) +} diff --git a/profiles/kaya.yaml b/profiles/kaya.yaml new file mode 100644 index 0000000..9edc214 --- /dev/null +++ b/profiles/kaya.yaml @@ -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 diff --git a/proto/l-ai-core-proto/ai.pb.go b/proto/l-ai-core-proto/ai.pb.go new file mode 100644 index 0000000..55dfa44 --- /dev/null +++ b/proto/l-ai-core-proto/ai.pb.go @@ -0,0 +1,1854 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v7.34.0 +// source: ai.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// AnswerStatus описывает, готов ли ответ синхронно или отложен в job. +type AnswerStatus int32 + +const ( + AnswerStatus_ANSWER_STATUS_UNSPECIFIED AnswerStatus = 0 // трактуется как COMPLETE (обратная совместимость) + AnswerStatus_ANSWER_STATUS_COMPLETE AnswerStatus = 1 // content — финальный ответ + AnswerStatus_ANSWER_STATUS_PENDING AnswerStatus = 2 // запущена долгая job; поллить GetAnswerJob(job_id) +) + +// Enum value maps for AnswerStatus. +var ( + AnswerStatus_name = map[int32]string{ + 0: "ANSWER_STATUS_UNSPECIFIED", + 1: "ANSWER_STATUS_COMPLETE", + 2: "ANSWER_STATUS_PENDING", + } + AnswerStatus_value = map[string]int32{ + "ANSWER_STATUS_UNSPECIFIED": 0, + "ANSWER_STATUS_COMPLETE": 1, + "ANSWER_STATUS_PENDING": 2, + } +) + +func (x AnswerStatus) Enum() *AnswerStatus { + p := new(AnswerStatus) + *p = x + return p +} + +func (x AnswerStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AnswerStatus) Descriptor() protoreflect.EnumDescriptor { + return file_ai_proto_enumTypes[0].Descriptor() +} + +func (AnswerStatus) Type() protoreflect.EnumType { + return &file_ai_proto_enumTypes[0] +} + +func (x AnswerStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AnswerStatus.Descriptor instead. +func (AnswerStatus) EnumDescriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{0} +} + +// AnswerJobState — состояние отложенной генерации ответа. +// Значения 0-4 исторические (GenerateAnswer deferred). Значения 5-7 добавлены для +// durable RunAgent jobs; старые клиенты их не создают и потому не увидят. RUNNING +// для старого клиента семантически эквивалентен PENDING (job ещё не терминальна). +type AnswerJobState int32 + +const ( + AnswerJobState_ANSWER_JOB_STATE_UNSPECIFIED AnswerJobState = 0 + AnswerJobState_ANSWER_JOB_STATE_PENDING AnswerJobState = 1 // ещё не взята в работу / выполняется (evolver работает, модель дописывает) + AnswerJobState_ANSWER_JOB_STATE_DONE AnswerJobState = 2 // готово, content заполнен + AnswerJobState_ANSWER_JOB_STATE_FAILED AnswerJobState = 3 // ошибка, error заполнен + AnswerJobState_ANSWER_JOB_STATE_NOT_FOUND AnswerJobState = 4 // неизвестный/протухший job_id (напр. после рестарта core) + AnswerJobState_ANSWER_JOB_STATE_RUNNING AnswerJobState = 5 // взята воркером (lease активен), tool loop исполняется + AnswerJobState_ANSWER_JOB_STATE_CANCELLED AnswerJobState = 6 // отменена через CancelAnswerJob + AnswerJobState_ANSWER_JOB_STATE_EXPIRED AnswerJobState = 7 // истёк expiration до достижения терминального состояния +) + +// Enum value maps for AnswerJobState. +var ( + AnswerJobState_name = map[int32]string{ + 0: "ANSWER_JOB_STATE_UNSPECIFIED", + 1: "ANSWER_JOB_STATE_PENDING", + 2: "ANSWER_JOB_STATE_DONE", + 3: "ANSWER_JOB_STATE_FAILED", + 4: "ANSWER_JOB_STATE_NOT_FOUND", + 5: "ANSWER_JOB_STATE_RUNNING", + 6: "ANSWER_JOB_STATE_CANCELLED", + 7: "ANSWER_JOB_STATE_EXPIRED", + } + AnswerJobState_value = map[string]int32{ + "ANSWER_JOB_STATE_UNSPECIFIED": 0, + "ANSWER_JOB_STATE_PENDING": 1, + "ANSWER_JOB_STATE_DONE": 2, + "ANSWER_JOB_STATE_FAILED": 3, + "ANSWER_JOB_STATE_NOT_FOUND": 4, + "ANSWER_JOB_STATE_RUNNING": 5, + "ANSWER_JOB_STATE_CANCELLED": 6, + "ANSWER_JOB_STATE_EXPIRED": 7, + } +) + +func (x AnswerJobState) Enum() *AnswerJobState { + p := new(AnswerJobState) + *p = x + return p +} + +func (x AnswerJobState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AnswerJobState) Descriptor() protoreflect.EnumDescriptor { + return file_ai_proto_enumTypes[1].Descriptor() +} + +func (AnswerJobState) Type() protoreflect.EnumType { + return &file_ai_proto_enumTypes[1] +} + +func (x AnswerJobState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AnswerJobState.Descriptor instead. +func (AnswerJobState) EnumDescriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{1} +} + +// AgentOutputFormat — требуемый формат финального ответа. +type AgentOutputFormat int32 + +const ( + AgentOutputFormat_AGENT_OUTPUT_FORMAT_UNSPECIFIED AgentOutputFormat = 0 // трактуется как TEXT + AgentOutputFormat_AGENT_OUTPUT_FORMAT_TEXT AgentOutputFormat = 1 // произвольный текст + AgentOutputFormat_AGENT_OUTPUT_FORMAT_JSON AgentOutputFormat = 2 // должен быть валидный JSON (без schema-проверки) + AgentOutputFormat_AGENT_OUTPUT_FORMAT_JSON_SCHEMA AgentOutputFormat = 3 // валидный JSON, проходящий json_schema +) + +// Enum value maps for AgentOutputFormat. +var ( + AgentOutputFormat_name = map[int32]string{ + 0: "AGENT_OUTPUT_FORMAT_UNSPECIFIED", + 1: "AGENT_OUTPUT_FORMAT_TEXT", + 2: "AGENT_OUTPUT_FORMAT_JSON", + 3: "AGENT_OUTPUT_FORMAT_JSON_SCHEMA", + } + AgentOutputFormat_value = map[string]int32{ + "AGENT_OUTPUT_FORMAT_UNSPECIFIED": 0, + "AGENT_OUTPUT_FORMAT_TEXT": 1, + "AGENT_OUTPUT_FORMAT_JSON": 2, + "AGENT_OUTPUT_FORMAT_JSON_SCHEMA": 3, + } +) + +func (x AgentOutputFormat) Enum() *AgentOutputFormat { + p := new(AgentOutputFormat) + *p = x + return p +} + +func (x AgentOutputFormat) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AgentOutputFormat) Descriptor() protoreflect.EnumDescriptor { + return file_ai_proto_enumTypes[2].Descriptor() +} + +func (AgentOutputFormat) Type() protoreflect.EnumType { + return &file_ai_proto_enumTypes[2] +} + +func (x AgentOutputFormat) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AgentOutputFormat.Descriptor instead. +func (AgentOutputFormat) EnumDescriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{2} +} + +// AIRequest содержит запрос к AI модели +type AIRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` // Текст запроса + BotUsername string `protobuf:"bytes,2,opt,name=bot_username,json=botUsername,proto3" json:"bot_username,omitempty"` // Username бота для упоминаний + ModelName string `protobuf:"bytes,3,opt,name=model_name,json=modelName,proto3" json:"model_name,omitempty"` // Название модели (по умолчанию "deepseek-v4-flash") + EnableReasoning bool `protobuf:"varint,4,opt,name=enable_reasoning,json=enableReasoning,proto3" json:"enable_reasoning,omitempty"` // Включить режим рассуждений (по умолчанию false) + GlobalUserId int64 `protobuf:"varint,5,opt,name=global_user_id,json=globalUserId,proto3" json:"global_user_id,omitempty"` // Global user ID для получения доп. данных (профиль, воспоминания) + Options *AnswerGenerationOptions `protobuf:"bytes,6,opt,name=options,proto3" json:"options,omitempty"` // Дополнительные опции генерации ответа + TelegramUserId int64 `protobuf:"varint,7,opt,name=telegram_user_id,json=telegramUserId,proto3" json:"telegram_user_id,omitempty"` // Telegram user ID для применения пользовательских настроек (стиль ответа) + SteamUserId int64 `protobuf:"varint,8,opt,name=steam_user_id,json=steamUserId,proto3" json:"steam_user_id,omitempty"` // SteamID64 отправителя для живых Steam-диалогов (l-ai-steam) - тот же "сигнал живого чата", что telegram_user_id, для второй платформы + SupportsDeferred bool `protobuf:"varint,9,opt,name=supports_deferred,json=supportsDeferred,proto3" json:"supports_deferred,omitempty"` // Клиент умеет обрабатывать status=PENDING + GetAnswerJob (async тяжёлые тулзы). Старые клиенты не ставят — тяжёлые тулзы исполняются синхронно, как раньше. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AIRequest) Reset() { + *x = AIRequest{} + mi := &file_ai_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AIRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AIRequest) ProtoMessage() {} + +func (x *AIRequest) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AIRequest.ProtoReflect.Descriptor instead. +func (*AIRequest) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{0} +} + +func (x *AIRequest) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +func (x *AIRequest) GetBotUsername() string { + if x != nil { + return x.BotUsername + } + return "" +} + +func (x *AIRequest) GetModelName() string { + if x != nil { + return x.ModelName + } + return "" +} + +func (x *AIRequest) GetEnableReasoning() bool { + if x != nil { + return x.EnableReasoning + } + return false +} + +func (x *AIRequest) GetGlobalUserId() int64 { + if x != nil { + return x.GlobalUserId + } + return 0 +} + +func (x *AIRequest) GetOptions() *AnswerGenerationOptions { + if x != nil { + return x.Options + } + return nil +} + +func (x *AIRequest) GetTelegramUserId() int64 { + if x != nil { + return x.TelegramUserId + } + return 0 +} + +func (x *AIRequest) GetSteamUserId() int64 { + if x != nil { + return x.SteamUserId + } + return 0 +} + +func (x *AIRequest) GetSupportsDeferred() bool { + if x != nil { + return x.SupportsDeferred + } + return false +} + +// AnswerGenerationOptions содержит дополнительные опции для генерации ответа +type AnswerGenerationOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + IncludeUserMemories bool `protobuf:"varint,1,opt,name=include_user_memories,json=includeUserMemories,proto3" json:"include_user_memories,omitempty"` // Включить воспоминания о пользователе в промпт + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AnswerGenerationOptions) Reset() { + *x = AnswerGenerationOptions{} + mi := &file_ai_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AnswerGenerationOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AnswerGenerationOptions) ProtoMessage() {} + +func (x *AnswerGenerationOptions) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AnswerGenerationOptions.ProtoReflect.Descriptor instead. +func (*AnswerGenerationOptions) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{1} +} + +func (x *AnswerGenerationOptions) GetIncludeUserMemories() bool { + if x != nil { + return x.IncludeUserMemories + } + return false +} + +// AIResponse содержит ответ от AI модели +type AIResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` // Текст ответа (при status=PENDING — interim-сообщение) + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // Текст ошибки, если произошла + DurationMs float64 `protobuf:"fixed64,3,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` // Время выполнения в миллисекундах + ReasoningContent string `protobuf:"bytes,4,opt,name=reasoning_content,json=reasoningContent,proto3" json:"reasoning_content,omitempty"` // Текст рассуждений (если включён режим reasoning) + Status AnswerStatus `protobuf:"varint,5,opt,name=status,proto3,enum=ai.AnswerStatus" json:"status,omitempty"` // COMPLETE (или UNSPECIFIED для старых клиентов) — content финальный; + // PENDING — запущена долгая job, content это interim, финал тянуть через GetAnswerJob(job_id) + JobId string `protobuf:"bytes,6,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` // Идентификатор отложенной job (только при status=PENDING) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AIResponse) Reset() { + *x = AIResponse{} + mi := &file_ai_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AIResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AIResponse) ProtoMessage() {} + +func (x *AIResponse) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AIResponse.ProtoReflect.Descriptor instead. +func (*AIResponse) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{2} +} + +func (x *AIResponse) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *AIResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *AIResponse) GetDurationMs() float64 { + if x != nil { + return x.DurationMs + } + return 0 +} + +func (x *AIResponse) GetReasoningContent() string { + if x != nil { + return x.ReasoningContent + } + return "" +} + +func (x *AIResponse) GetStatus() AnswerStatus { + if x != nil { + return x.Status + } + return AnswerStatus_ANSWER_STATUS_UNSPECIFIED +} + +func (x *AIResponse) GetJobId() string { + if x != nil { + return x.JobId + } + return "" +} + +// GetAnswerJobRequest запрашивает статус отложенной генерации ответа. +type GetAnswerJobRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAnswerJobRequest) Reset() { + *x = GetAnswerJobRequest{} + mi := &file_ai_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAnswerJobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAnswerJobRequest) ProtoMessage() {} + +func (x *GetAnswerJobRequest) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAnswerJobRequest.ProtoReflect.Descriptor instead. +func (*GetAnswerJobRequest) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{3} +} + +func (x *GetAnswerJobRequest) GetJobId() string { + if x != nil { + return x.JobId + } + return "" +} + +// AnswerJobResponse — результат опроса отложенной генерации. +type AnswerJobResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + State AnswerJobState `protobuf:"varint,1,opt,name=state,proto3,enum=ai.AnswerJobState" json:"state,omitempty"` + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` // финальный ответ, когда DONE + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` // причина, когда FAILED + DurationMs float64 `protobuf:"fixed64,4,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` // суммарное время генерации, когда DONE + // Authoritative usage завершённой deferred RunAgent job — тот же счётчик, что + // возвращает synchronous RunAgent (реальные iterations/tool calls core, не + // self-reported из ответа модели). Для legacy GenerateAnswer jobs не заполняется. + AgentUsage *AgentUsage `protobuf:"bytes,5,opt,name=agent_usage,json=agentUsage,proto3" json:"agent_usage,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AnswerJobResponse) Reset() { + *x = AnswerJobResponse{} + mi := &file_ai_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AnswerJobResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AnswerJobResponse) ProtoMessage() {} + +func (x *AnswerJobResponse) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AnswerJobResponse.ProtoReflect.Descriptor instead. +func (*AnswerJobResponse) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{4} +} + +func (x *AnswerJobResponse) GetState() AnswerJobState { + if x != nil { + return x.State + } + return AnswerJobState_ANSWER_JOB_STATE_UNSPECIFIED +} + +func (x *AnswerJobResponse) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *AnswerJobResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *AnswerJobResponse) GetDurationMs() float64 { + if x != nil { + return x.DurationMs + } + return 0 +} + +func (x *AnswerJobResponse) GetAgentUsage() *AgentUsage { + if x != nil { + return x.AgentUsage + } + return nil +} + +// CreateSessionRequest для создания новой AI сессии +type CreateSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // Идентификатор пользователя + BotUsername string `protobuf:"bytes,2,opt,name=bot_username,json=botUsername,proto3" json:"bot_username,omitempty"` // Username бота для упоминаний + ModelName string `protobuf:"bytes,3,opt,name=model_name,json=modelName,proto3" json:"model_name,omitempty"` // Название модели (по умолчанию "deepseek-v4-flash") + TtlSeconds int64 `protobuf:"varint,4,opt,name=ttl_seconds,json=ttlSeconds,proto3" json:"ttl_seconds,omitempty"` // Время жизни сессии в секундах (по умолчанию 3600 = 1 час) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSessionRequest) Reset() { + *x = CreateSessionRequest{} + mi := &file_ai_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSessionRequest) ProtoMessage() {} + +func (x *CreateSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSessionRequest.ProtoReflect.Descriptor instead. +func (*CreateSessionRequest) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateSessionRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *CreateSessionRequest) GetBotUsername() string { + if x != nil { + return x.BotUsername + } + return "" +} + +func (x *CreateSessionRequest) GetModelName() string { + if x != nil { + return x.ModelName + } + return "" +} + +func (x *CreateSessionRequest) GetTtlSeconds() int64 { + if x != nil { + return x.TtlSeconds + } + return 0 +} + +// CreateSessionResponse содержит ID созданной сессии +type CreateSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // ID сессии + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` // Статус сессии + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` // Текст ошибки, если произошла + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSessionResponse) Reset() { + *x = CreateSessionResponse{} + mi := &file_ai_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSessionResponse) ProtoMessage() {} + +func (x *CreateSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSessionResponse.ProtoReflect.Descriptor instead. +func (*CreateSessionResponse) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateSessionResponse) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *CreateSessionResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *CreateSessionResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// AskInSessionRequest для запроса в рамках сессии +type AskInSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // ID сессии + Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"` // Текст запроса + EnableReasoning bool `protobuf:"varint,3,opt,name=enable_reasoning,json=enableReasoning,proto3" json:"enable_reasoning,omitempty"` // Включить режим рассуждений (по умолчанию false) + GlobalUserId int64 `protobuf:"varint,4,opt,name=global_user_id,json=globalUserId,proto3" json:"global_user_id,omitempty"` // Global user ID для получения воспоминаний + Options *AnswerGenerationOptions `protobuf:"bytes,5,opt,name=options,proto3" json:"options,omitempty"` // Дополнительные опции генерации + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AskInSessionRequest) Reset() { + *x = AskInSessionRequest{} + mi := &file_ai_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AskInSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AskInSessionRequest) ProtoMessage() {} + +func (x *AskInSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AskInSessionRequest.ProtoReflect.Descriptor instead. +func (*AskInSessionRequest) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{7} +} + +func (x *AskInSessionRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *AskInSessionRequest) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +func (x *AskInSessionRequest) GetEnableReasoning() bool { + if x != nil { + return x.EnableReasoning + } + return false +} + +func (x *AskInSessionRequest) GetGlobalUserId() int64 { + if x != nil { + return x.GlobalUserId + } + return 0 +} + +func (x *AskInSessionRequest) GetOptions() *AnswerGenerationOptions { + if x != nil { + return x.Options + } + return nil +} + +// SessionStatusResponse содержит статус сессии и ответ AI +type SessionStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // ID сессии + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` // Статус сессии (active, expired, closed) + Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` // Текст ответа от AI + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` // Текст ошибки, если произошла + DurationMs float64 `protobuf:"fixed64,5,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` // Время выполнения в миллисекундах + ReasoningContent string `protobuf:"bytes,6,opt,name=reasoning_content,json=reasoningContent,proto3" json:"reasoning_content,omitempty"` // Текст рассуждений (если включён режим reasoning) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionStatusResponse) Reset() { + *x = SessionStatusResponse{} + mi := &file_ai_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionStatusResponse) ProtoMessage() {} + +func (x *SessionStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionStatusResponse.ProtoReflect.Descriptor instead. +func (*SessionStatusResponse) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{8} +} + +func (x *SessionStatusResponse) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *SessionStatusResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *SessionStatusResponse) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *SessionStatusResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *SessionStatusResponse) GetDurationMs() float64 { + if x != nil { + return x.DurationMs + } + return 0 +} + +func (x *SessionStatusResponse) GetReasoningContent() string { + if x != nil { + return x.ReasoningContent + } + return "" +} + +// StatelessMessage одно сообщение для stateless запроса (role + content) +type StatelessMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + Role string `protobuf:"bytes,1,opt,name=role,proto3" json:"role,omitempty"` // "system", "user", "assistant" + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` // Текст сообщения + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StatelessMessage) Reset() { + *x = StatelessMessage{} + mi := &file_ai_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StatelessMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatelessMessage) ProtoMessage() {} + +func (x *StatelessMessage) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatelessMessage.ProtoReflect.Descriptor instead. +func (*StatelessMessage) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{9} +} + +func (x *StatelessMessage) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *StatelessMessage) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +// StatelessRequest для stateless генерации с полным контролем над messages +type StatelessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Messages []*StatelessMessage `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` // Массив сообщений (system + history + user) + ModelName string `protobuf:"bytes,2,opt,name=model_name,json=modelName,proto3" json:"model_name,omitempty"` // Название модели (по умолчанию "deepseek-v4-flash") + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StatelessRequest) Reset() { + *x = StatelessRequest{} + mi := &file_ai_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StatelessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatelessRequest) ProtoMessage() {} + +func (x *StatelessRequest) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatelessRequest.ProtoReflect.Descriptor instead. +func (*StatelessRequest) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{10} +} + +func (x *StatelessRequest) GetMessages() []*StatelessMessage { + if x != nil { + return x.Messages + } + return nil +} + +func (x *StatelessRequest) GetModelName() string { + if x != nil { + return x.ModelName + } + return "" +} + +// HealthCheckRequest для проверки здоровья сервиса +type HealthCheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthCheckRequest) Reset() { + *x = HealthCheckRequest{} + mi := &file_ai_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthCheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckRequest) ProtoMessage() {} + +func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead. +func (*HealthCheckRequest) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{11} +} + +// HealthCheckResponse для ответа на проверку здоровья +type HealthCheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Serving bool `protobuf:"varint,1,opt,name=serving,proto3" json:"serving,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HealthCheckResponse) Reset() { + *x = HealthCheckResponse{} + mi := &file_ai_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HealthCheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckResponse) ProtoMessage() {} + +func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead. +func (*HealthCheckResponse) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{12} +} + +func (x *HealthCheckResponse) GetServing() bool { + if x != nil { + return x.Serving + } + return false +} + +func (x *HealthCheckResponse) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +// AgentToolLoopOptions задаёт лимиты tool loop. Значения клиента могут только +// сузить серверные потолки (max_tool_iterations/max_total_tool_calls ≤ server max). +type AgentToolLoopOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + ToolsEnabled bool `protobuf:"varint,1,opt,name=tools_enabled,json=toolsEnabled,proto3" json:"tools_enabled,omitempty"` // false — модель отвечает без tools + MaxToolIterations uint32 `protobuf:"varint,2,opt,name=max_tool_iterations,json=maxToolIterations,proto3" json:"max_tool_iterations,omitempty"` // потолок round-trip'ов модель↔tools (0 = серверный default) + MaxTotalToolCalls uint32 `protobuf:"varint,3,opt,name=max_total_tool_calls,json=maxTotalToolCalls,proto3" json:"max_total_tool_calls,omitempty"` // потолок РЕАЛЬНЫХ tool call'ов, не итераций (0 = серверный default) + ToolTimeoutSeconds uint32 `protobuf:"varint,4,opt,name=tool_timeout_seconds,json=toolTimeoutSeconds,proto3" json:"tool_timeout_seconds,omitempty"` // таймаут одного tool call (0 = серверный default) + TotalTimeoutSeconds uint32 `protobuf:"varint,5,opt,name=total_timeout_seconds,json=totalTimeoutSeconds,proto3" json:"total_timeout_seconds,omitempty"` // wall-clock таймаут всего вызова (0 = серверный default), независим от budget + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentToolLoopOptions) Reset() { + *x = AgentToolLoopOptions{} + mi := &file_ai_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentToolLoopOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentToolLoopOptions) ProtoMessage() {} + +func (x *AgentToolLoopOptions) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentToolLoopOptions.ProtoReflect.Descriptor instead. +func (*AgentToolLoopOptions) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{13} +} + +func (x *AgentToolLoopOptions) GetToolsEnabled() bool { + if x != nil { + return x.ToolsEnabled + } + return false +} + +func (x *AgentToolLoopOptions) GetMaxToolIterations() uint32 { + if x != nil { + return x.MaxToolIterations + } + return 0 +} + +func (x *AgentToolLoopOptions) GetMaxTotalToolCalls() uint32 { + if x != nil { + return x.MaxTotalToolCalls + } + return 0 +} + +func (x *AgentToolLoopOptions) GetToolTimeoutSeconds() uint32 { + if x != nil { + return x.ToolTimeoutSeconds + } + return 0 +} + +func (x *AgentToolLoopOptions) GetTotalTimeoutSeconds() uint32 { + if x != nil { + return x.TotalTimeoutSeconds + } + return 0 +} + +// AgentResponseContract описывает требования к структурированному ответу. +type AgentResponseContract struct { + state protoimpl.MessageState `protogen:"open.v1"` + Format AgentOutputFormat `protobuf:"varint,1,opt,name=format,proto3,enum=ai.AgentOutputFormat" json:"format,omitempty"` + JsonSchema string `protobuf:"bytes,2,opt,name=json_schema,json=jsonSchema,proto3" json:"json_schema,omitempty"` // JSON Schema (draft-07), обязателен при format=JSON_SCHEMA + RepairInvalidOutput bool `protobuf:"varint,3,opt,name=repair_invalid_output,json=repairInvalidOutput,proto3" json:"repair_invalid_output,omitempty"` // при провале валидации — один repair-запрос модели (без новых tools) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentResponseContract) Reset() { + *x = AgentResponseContract{} + mi := &file_ai_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentResponseContract) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentResponseContract) ProtoMessage() {} + +func (x *AgentResponseContract) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentResponseContract.ProtoReflect.Descriptor instead. +func (*AgentResponseContract) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{14} +} + +func (x *AgentResponseContract) GetFormat() AgentOutputFormat { + if x != nil { + return x.Format + } + return AgentOutputFormat_AGENT_OUTPUT_FORMAT_UNSPECIFIED +} + +func (x *AgentResponseContract) GetJsonSchema() string { + if x != nil { + return x.JsonSchema + } + return "" +} + +func (x *AgentResponseContract) GetRepairInvalidOutput() bool { + if x != nil { + return x.RepairInvalidOutput + } + return false +} + +// AgentRequest — запрос агентного вызова. +type AgentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Messages []*StatelessMessage `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` // полный контекст, включая system + ModelName string `protobuf:"bytes,2,opt,name=model_name,json=modelName,proto3" json:"model_name,omitempty"` // модель (по умолчанию deepseek-v4-flash) + EnableReasoning bool `protobuf:"varint,3,opt,name=enable_reasoning,json=enableReasoning,proto3" json:"enable_reasoning,omitempty"` // reasoning не попадает в обычный content + AgentId string `protobuf:"bytes,4,opt,name=agent_id,json=agentId,proto3" json:"agent_id,omitempty"` // идентификатор логического агента (для логов/метрик, НЕ авторизация) + ToolProfile string `protobuf:"bytes,5,opt,name=tool_profile,json=toolProfile,proto3" json:"tool_profile,omitempty"` // серверный профиль tools (НЕ авторизация; проверяется против credential) + RequestedTools []string `protobuf:"bytes,6,rep,name=requested_tools,json=requestedTools,proto3" json:"requested_tools,omitempty"` // сужение профиля: effective = profile.allowed ∩ requested; пусто = все allowed + ToolLoop *AgentToolLoopOptions `protobuf:"bytes,7,opt,name=tool_loop,json=toolLoop,proto3" json:"tool_loop,omitempty"` + ResponseContract *AgentResponseContract `protobuf:"bytes,8,opt,name=response_contract,json=responseContract,proto3" json:"response_contract,omitempty"` + RequestId string `protobuf:"bytes,9,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` // сквозной идентификатор для трассировки + IdempotencyKey string `protobuf:"bytes,10,opt,name=idempotency_key,json=idempotencyKey,proto3" json:"idempotency_key,omitempty"` // повторный RunAgent с тем же ключом не создаёт дубликат job + SupportsDeferred bool `protobuf:"varint,11,opt,name=supports_deferred,json=supportsDeferred,proto3" json:"supports_deferred,omitempty"` // клиент умеет status=PENDING + GetAnswerJob для тяжёлых операций + Metadata map[string]string `protobuf:"bytes,12,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // произвольные метки (не логируются как секреты) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentRequest) Reset() { + *x = AgentRequest{} + mi := &file_ai_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentRequest) ProtoMessage() {} + +func (x *AgentRequest) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentRequest.ProtoReflect.Descriptor instead. +func (*AgentRequest) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{15} +} + +func (x *AgentRequest) GetMessages() []*StatelessMessage { + if x != nil { + return x.Messages + } + return nil +} + +func (x *AgentRequest) GetModelName() string { + if x != nil { + return x.ModelName + } + return "" +} + +func (x *AgentRequest) GetEnableReasoning() bool { + if x != nil { + return x.EnableReasoning + } + return false +} + +func (x *AgentRequest) GetAgentId() string { + if x != nil { + return x.AgentId + } + return "" +} + +func (x *AgentRequest) GetToolProfile() string { + if x != nil { + return x.ToolProfile + } + return "" +} + +func (x *AgentRequest) GetRequestedTools() []string { + if x != nil { + return x.RequestedTools + } + return nil +} + +func (x *AgentRequest) GetToolLoop() *AgentToolLoopOptions { + if x != nil { + return x.ToolLoop + } + return nil +} + +func (x *AgentRequest) GetResponseContract() *AgentResponseContract { + if x != nil { + return x.ResponseContract + } + return nil +} + +func (x *AgentRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *AgentRequest) GetIdempotencyKey() string { + if x != nil { + return x.IdempotencyKey + } + return "" +} + +func (x *AgentRequest) GetSupportsDeferred() bool { + if x != nil { + return x.SupportsDeferred + } + return false +} + +func (x *AgentRequest) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// AgentUsage — счётчики использования за вызов. +type AgentUsage struct { + state protoimpl.MessageState `protogen:"open.v1"` + ToolIterations uint32 `protobuf:"varint,1,opt,name=tool_iterations,json=toolIterations,proto3" json:"tool_iterations,omitempty"` // сколько round-trip'ов модель↔tools произошло + TotalToolCalls uint32 `protobuf:"varint,2,opt,name=total_tool_calls,json=totalToolCalls,proto3" json:"total_tool_calls,omitempty"` // сколько РЕАЛЬНЫХ tool call'ов исполнено (3 calls в одной итерации = 3) + ToolsUsed []string `protobuf:"bytes,3,rep,name=tools_used,json=toolsUsed,proto3" json:"tools_used,omitempty"` + InputTokens uint64 `protobuf:"varint,4,opt,name=input_tokens,json=inputTokens,proto3" json:"input_tokens,omitempty"` + OutputTokens uint64 `protobuf:"varint,5,opt,name=output_tokens,json=outputTokens,proto3" json:"output_tokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentUsage) Reset() { + *x = AgentUsage{} + mi := &file_ai_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentUsage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentUsage) ProtoMessage() {} + +func (x *AgentUsage) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentUsage.ProtoReflect.Descriptor instead. +func (*AgentUsage) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{16} +} + +func (x *AgentUsage) GetToolIterations() uint32 { + if x != nil { + return x.ToolIterations + } + return 0 +} + +func (x *AgentUsage) GetTotalToolCalls() uint32 { + if x != nil { + return x.TotalToolCalls + } + return 0 +} + +func (x *AgentUsage) GetToolsUsed() []string { + if x != nil { + return x.ToolsUsed + } + return nil +} + +func (x *AgentUsage) GetInputTokens() uint64 { + if x != nil { + return x.InputTokens + } + return 0 +} + +func (x *AgentUsage) GetOutputTokens() uint64 { + if x != nil { + return x.OutputTokens + } + return 0 +} + +// AgentResponse — результат агентного вызова. +type AgentResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status AnswerStatus `protobuf:"varint,1,opt,name=status,proto3,enum=ai.AnswerStatus" json:"status,omitempty"` // COMPLETE — content финальный; PENDING — тянуть через GetAnswerJob(job_id) + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + DurationMs float64 `protobuf:"fixed64,4,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` + JobId string `protobuf:"bytes,5,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` // при status=PENDING + Usage *AgentUsage `protobuf:"bytes,6,opt,name=usage,proto3" json:"usage,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentResponse) Reset() { + *x = AgentResponse{} + mi := &file_ai_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentResponse) ProtoMessage() {} + +func (x *AgentResponse) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentResponse.ProtoReflect.Descriptor instead. +func (*AgentResponse) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{17} +} + +func (x *AgentResponse) GetStatus() AnswerStatus { + if x != nil { + return x.Status + } + return AnswerStatus_ANSWER_STATUS_UNSPECIFIED +} + +func (x *AgentResponse) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *AgentResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *AgentResponse) GetDurationMs() float64 { + if x != nil { + return x.DurationMs + } + return 0 +} + +func (x *AgentResponse) GetJobId() string { + if x != nil { + return x.JobId + } + return "" +} + +func (x *AgentResponse) GetUsage() *AgentUsage { + if x != nil { + return x.Usage + } + return nil +} + +// CancelAnswerJobRequest — запрос отмены отложенной job (идемпотентный). +type CancelAnswerJobRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` // причина отмены (сохраняется), опционально + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CancelAnswerJobRequest) Reset() { + *x = CancelAnswerJobRequest{} + mi := &file_ai_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CancelAnswerJobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CancelAnswerJobRequest) ProtoMessage() {} + +func (x *CancelAnswerJobRequest) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CancelAnswerJobRequest.ProtoReflect.Descriptor instead. +func (*CancelAnswerJobRequest) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{18} +} + +func (x *CancelAnswerJobRequest) GetJobId() string { + if x != nil { + return x.JobId + } + return "" +} + +func (x *CancelAnswerJobRequest) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +// CancelAnswerJobResponse — результат отмены. +type CancelAnswerJobResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + State AnswerJobState `protobuf:"varint,1,opt,name=state,proto3,enum=ai.AnswerJobState" json:"state,omitempty"` // состояние job после запроса (CANCELLED, либо терминальное, если уже завершилась) + Cancelled bool `protobuf:"varint,2,opt,name=cancelled,proto3" json:"cancelled,omitempty"` // true — отмена применена этим вызовом; false — job уже была терминальна/отменена + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CancelAnswerJobResponse) Reset() { + *x = CancelAnswerJobResponse{} + mi := &file_ai_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CancelAnswerJobResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CancelAnswerJobResponse) ProtoMessage() {} + +func (x *CancelAnswerJobResponse) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CancelAnswerJobResponse.ProtoReflect.Descriptor instead. +func (*CancelAnswerJobResponse) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{19} +} + +func (x *CancelAnswerJobResponse) GetState() AnswerJobState { + if x != nil { + return x.State + } + return AnswerJobState_ANSWER_JOB_STATE_UNSPECIFIED +} + +func (x *CancelAnswerJobResponse) GetCancelled() bool { + if x != nil { + return x.Cancelled + } + return false +} + +func (x *CancelAnswerJobResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +var File_ai_proto protoreflect.FileDescriptor + +const file_ai_proto_rawDesc = "" + + "\n" + + "\bai.proto\x12\x02ai\"\xe4\x02\n" + + "\tAIRequest\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\x12!\n" + + "\fbot_username\x18\x02 \x01(\tR\vbotUsername\x12\x1d\n" + + "\n" + + "model_name\x18\x03 \x01(\tR\tmodelName\x12)\n" + + "\x10enable_reasoning\x18\x04 \x01(\bR\x0fenableReasoning\x12$\n" + + "\x0eglobal_user_id\x18\x05 \x01(\x03R\fglobalUserId\x125\n" + + "\aoptions\x18\x06 \x01(\v2\x1b.ai.AnswerGenerationOptionsR\aoptions\x12(\n" + + "\x10telegram_user_id\x18\a \x01(\x03R\x0etelegramUserId\x12\"\n" + + "\rsteam_user_id\x18\b \x01(\x03R\vsteamUserId\x12+\n" + + "\x11supports_deferred\x18\t \x01(\bR\x10supportsDeferred\"M\n" + + "\x17AnswerGenerationOptions\x122\n" + + "\x15include_user_memories\x18\x01 \x01(\bR\x13includeUserMemories\"\xcb\x01\n" + + "\n" + + "AIResponse\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05error\x12\x1f\n" + + "\vduration_ms\x18\x03 \x01(\x01R\n" + + "durationMs\x12+\n" + + "\x11reasoning_content\x18\x04 \x01(\tR\x10reasoningContent\x12(\n" + + "\x06status\x18\x05 \x01(\x0e2\x10.ai.AnswerStatusR\x06status\x12\x15\n" + + "\x06job_id\x18\x06 \x01(\tR\x05jobId\",\n" + + "\x13GetAnswerJobRequest\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\tR\x05jobId\"\xbf\x01\n" + + "\x11AnswerJobResponse\x12(\n" + + "\x05state\x18\x01 \x01(\x0e2\x12.ai.AnswerJobStateR\x05state\x12\x18\n" + + "\acontent\x18\x02 \x01(\tR\acontent\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error\x12\x1f\n" + + "\vduration_ms\x18\x04 \x01(\x01R\n" + + "durationMs\x12/\n" + + "\vagent_usage\x18\x05 \x01(\v2\x0e.ai.AgentUsageR\n" + + "agentUsage\"\x92\x01\n" + + "\x14CreateSessionRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12!\n" + + "\fbot_username\x18\x02 \x01(\tR\vbotUsername\x12\x1d\n" + + "\n" + + "model_name\x18\x03 \x01(\tR\tmodelName\x12\x1f\n" + + "\vttl_seconds\x18\x04 \x01(\x03R\n" + + "ttlSeconds\"d\n" + + "\x15CreateSessionResponse\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error\"\xd0\x01\n" + + "\x13AskInSessionRequest\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x12\n" + + "\x04text\x18\x02 \x01(\tR\x04text\x12)\n" + + "\x10enable_reasoning\x18\x03 \x01(\bR\x0fenableReasoning\x12$\n" + + "\x0eglobal_user_id\x18\x04 \x01(\x03R\fglobalUserId\x125\n" + + "\aoptions\x18\x05 \x01(\v2\x1b.ai.AnswerGenerationOptionsR\aoptions\"\xcc\x01\n" + + "\x15SessionStatusResponse\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x18\n" + + "\acontent\x18\x03 \x01(\tR\acontent\x12\x14\n" + + "\x05error\x18\x04 \x01(\tR\x05error\x12\x1f\n" + + "\vduration_ms\x18\x05 \x01(\x01R\n" + + "durationMs\x12+\n" + + "\x11reasoning_content\x18\x06 \x01(\tR\x10reasoningContent\"@\n" + + "\x10StatelessMessage\x12\x12\n" + + "\x04role\x18\x01 \x01(\tR\x04role\x12\x18\n" + + "\acontent\x18\x02 \x01(\tR\acontent\"c\n" + + "\x10StatelessRequest\x120\n" + + "\bmessages\x18\x01 \x03(\v2\x14.ai.StatelessMessageR\bmessages\x12\x1d\n" + + "\n" + + "model_name\x18\x02 \x01(\tR\tmodelName\"\x14\n" + + "\x12HealthCheckRequest\"I\n" + + "\x13HealthCheckResponse\x12\x18\n" + + "\aserving\x18\x01 \x01(\bR\aserving\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\"\x82\x02\n" + + "\x14AgentToolLoopOptions\x12#\n" + + "\rtools_enabled\x18\x01 \x01(\bR\ftoolsEnabled\x12.\n" + + "\x13max_tool_iterations\x18\x02 \x01(\rR\x11maxToolIterations\x12/\n" + + "\x14max_total_tool_calls\x18\x03 \x01(\rR\x11maxTotalToolCalls\x120\n" + + "\x14tool_timeout_seconds\x18\x04 \x01(\rR\x12toolTimeoutSeconds\x122\n" + + "\x15total_timeout_seconds\x18\x05 \x01(\rR\x13totalTimeoutSeconds\"\x9b\x01\n" + + "\x15AgentResponseContract\x12-\n" + + "\x06format\x18\x01 \x01(\x0e2\x15.ai.AgentOutputFormatR\x06format\x12\x1f\n" + + "\vjson_schema\x18\x02 \x01(\tR\n" + + "jsonSchema\x122\n" + + "\x15repair_invalid_output\x18\x03 \x01(\bR\x13repairInvalidOutput\"\xde\x04\n" + + "\fAgentRequest\x120\n" + + "\bmessages\x18\x01 \x03(\v2\x14.ai.StatelessMessageR\bmessages\x12\x1d\n" + + "\n" + + "model_name\x18\x02 \x01(\tR\tmodelName\x12)\n" + + "\x10enable_reasoning\x18\x03 \x01(\bR\x0fenableReasoning\x12\x19\n" + + "\bagent_id\x18\x04 \x01(\tR\aagentId\x12!\n" + + "\ftool_profile\x18\x05 \x01(\tR\vtoolProfile\x12'\n" + + "\x0frequested_tools\x18\x06 \x03(\tR\x0erequestedTools\x125\n" + + "\ttool_loop\x18\a \x01(\v2\x18.ai.AgentToolLoopOptionsR\btoolLoop\x12F\n" + + "\x11response_contract\x18\b \x01(\v2\x19.ai.AgentResponseContractR\x10responseContract\x12\x1d\n" + + "\n" + + "request_id\x18\t \x01(\tR\trequestId\x12'\n" + + "\x0fidempotency_key\x18\n" + + " \x01(\tR\x0eidempotencyKey\x12+\n" + + "\x11supports_deferred\x18\v \x01(\bR\x10supportsDeferred\x12:\n" + + "\bmetadata\x18\f \x03(\v2\x1e.ai.AgentRequest.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc6\x01\n" + + "\n" + + "AgentUsage\x12'\n" + + "\x0ftool_iterations\x18\x01 \x01(\rR\x0etoolIterations\x12(\n" + + "\x10total_tool_calls\x18\x02 \x01(\rR\x0etotalToolCalls\x12\x1d\n" + + "\n" + + "tools_used\x18\x03 \x03(\tR\ttoolsUsed\x12!\n" + + "\finput_tokens\x18\x04 \x01(\x04R\vinputTokens\x12#\n" + + "\routput_tokens\x18\x05 \x01(\x04R\foutputTokens\"\xc7\x01\n" + + "\rAgentResponse\x12(\n" + + "\x06status\x18\x01 \x01(\x0e2\x10.ai.AnswerStatusR\x06status\x12\x18\n" + + "\acontent\x18\x02 \x01(\tR\acontent\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error\x12\x1f\n" + + "\vduration_ms\x18\x04 \x01(\x01R\n" + + "durationMs\x12\x15\n" + + "\x06job_id\x18\x05 \x01(\tR\x05jobId\x12$\n" + + "\x05usage\x18\x06 \x01(\v2\x0e.ai.AgentUsageR\x05usage\"G\n" + + "\x16CancelAnswerJobRequest\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\"w\n" + + "\x17CancelAnswerJobResponse\x12(\n" + + "\x05state\x18\x01 \x01(\x0e2\x12.ai.AnswerJobStateR\x05state\x12\x1c\n" + + "\tcancelled\x18\x02 \x01(\bR\tcancelled\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error*d\n" + + "\fAnswerStatus\x12\x1d\n" + + "\x19ANSWER_STATUS_UNSPECIFIED\x10\x00\x12\x1a\n" + + "\x16ANSWER_STATUS_COMPLETE\x10\x01\x12\x19\n" + + "\x15ANSWER_STATUS_PENDING\x10\x02*\x84\x02\n" + + "\x0eAnswerJobState\x12 \n" + + "\x1cANSWER_JOB_STATE_UNSPECIFIED\x10\x00\x12\x1c\n" + + "\x18ANSWER_JOB_STATE_PENDING\x10\x01\x12\x19\n" + + "\x15ANSWER_JOB_STATE_DONE\x10\x02\x12\x1b\n" + + "\x17ANSWER_JOB_STATE_FAILED\x10\x03\x12\x1e\n" + + "\x1aANSWER_JOB_STATE_NOT_FOUND\x10\x04\x12\x1c\n" + + "\x18ANSWER_JOB_STATE_RUNNING\x10\x05\x12\x1e\n" + + "\x1aANSWER_JOB_STATE_CANCELLED\x10\x06\x12\x1c\n" + + "\x18ANSWER_JOB_STATE_EXPIRED\x10\a*\x99\x01\n" + + "\x11AgentOutputFormat\x12#\n" + + "\x1fAGENT_OUTPUT_FORMAT_UNSPECIFIED\x10\x00\x12\x1c\n" + + "\x18AGENT_OUTPUT_FORMAT_TEXT\x10\x01\x12\x1c\n" + + "\x18AGENT_OUTPUT_FORMAT_JSON\x10\x02\x12#\n" + + "\x1fAGENT_OUTPUT_FORMAT_JSON_SCHEMA\x10\x032\xfe\x03\n" + + "\tAIService\x12/\n" + + "\bRunAgent\x12\x10.ai.AgentRequest\x1a\x11.ai.AgentResponse\x12J\n" + + "\x0fCancelAnswerJob\x12\x1a.ai.CancelAnswerJobRequest\x1a\x1b.ai.CancelAnswerJobResponse\x12/\n" + + "\x0eGenerateAnswer\x12\r.ai.AIRequest\x1a\x0e.ai.AIResponse\x129\n" + + "\x11GenerateStateless\x12\x14.ai.StatelessRequest\x1a\x0e.ai.AIResponse\x12>\n" + + "\vHealthCheck\x12\x16.ai.HealthCheckRequest\x1a\x17.ai.HealthCheckResponse\x12D\n" + + "\rCreateSession\x12\x18.ai.CreateSessionRequest\x1a\x19.ai.CreateSessionResponse\x12B\n" + + "\fAskInSession\x12\x17.ai.AskInSessionRequest\x1a\x19.ai.SessionStatusResponse\x12>\n" + + "\fGetAnswerJob\x12\x17.ai.GetAnswerJobRequest\x1a\x15.ai.AnswerJobResponseB,Z*git.be.kaien.ru/l-ai/l-ai-core-proto/protob\x06proto3" + +var ( + file_ai_proto_rawDescOnce sync.Once + file_ai_proto_rawDescData []byte +) + +func file_ai_proto_rawDescGZIP() []byte { + file_ai_proto_rawDescOnce.Do(func() { + file_ai_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ai_proto_rawDesc), len(file_ai_proto_rawDesc))) + }) + return file_ai_proto_rawDescData +} + +var file_ai_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_ai_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_ai_proto_goTypes = []any{ + (AnswerStatus)(0), // 0: ai.AnswerStatus + (AnswerJobState)(0), // 1: ai.AnswerJobState + (AgentOutputFormat)(0), // 2: ai.AgentOutputFormat + (*AIRequest)(nil), // 3: ai.AIRequest + (*AnswerGenerationOptions)(nil), // 4: ai.AnswerGenerationOptions + (*AIResponse)(nil), // 5: ai.AIResponse + (*GetAnswerJobRequest)(nil), // 6: ai.GetAnswerJobRequest + (*AnswerJobResponse)(nil), // 7: ai.AnswerJobResponse + (*CreateSessionRequest)(nil), // 8: ai.CreateSessionRequest + (*CreateSessionResponse)(nil), // 9: ai.CreateSessionResponse + (*AskInSessionRequest)(nil), // 10: ai.AskInSessionRequest + (*SessionStatusResponse)(nil), // 11: ai.SessionStatusResponse + (*StatelessMessage)(nil), // 12: ai.StatelessMessage + (*StatelessRequest)(nil), // 13: ai.StatelessRequest + (*HealthCheckRequest)(nil), // 14: ai.HealthCheckRequest + (*HealthCheckResponse)(nil), // 15: ai.HealthCheckResponse + (*AgentToolLoopOptions)(nil), // 16: ai.AgentToolLoopOptions + (*AgentResponseContract)(nil), // 17: ai.AgentResponseContract + (*AgentRequest)(nil), // 18: ai.AgentRequest + (*AgentUsage)(nil), // 19: ai.AgentUsage + (*AgentResponse)(nil), // 20: ai.AgentResponse + (*CancelAnswerJobRequest)(nil), // 21: ai.CancelAnswerJobRequest + (*CancelAnswerJobResponse)(nil), // 22: ai.CancelAnswerJobResponse + nil, // 23: ai.AgentRequest.MetadataEntry +} +var file_ai_proto_depIdxs = []int32{ + 4, // 0: ai.AIRequest.options:type_name -> ai.AnswerGenerationOptions + 0, // 1: ai.AIResponse.status:type_name -> ai.AnswerStatus + 1, // 2: ai.AnswerJobResponse.state:type_name -> ai.AnswerJobState + 19, // 3: ai.AnswerJobResponse.agent_usage:type_name -> ai.AgentUsage + 4, // 4: ai.AskInSessionRequest.options:type_name -> ai.AnswerGenerationOptions + 12, // 5: ai.StatelessRequest.messages:type_name -> ai.StatelessMessage + 2, // 6: ai.AgentResponseContract.format:type_name -> ai.AgentOutputFormat + 12, // 7: ai.AgentRequest.messages:type_name -> ai.StatelessMessage + 16, // 8: ai.AgentRequest.tool_loop:type_name -> ai.AgentToolLoopOptions + 17, // 9: ai.AgentRequest.response_contract:type_name -> ai.AgentResponseContract + 23, // 10: ai.AgentRequest.metadata:type_name -> ai.AgentRequest.MetadataEntry + 0, // 11: ai.AgentResponse.status:type_name -> ai.AnswerStatus + 19, // 12: ai.AgentResponse.usage:type_name -> ai.AgentUsage + 1, // 13: ai.CancelAnswerJobResponse.state:type_name -> ai.AnswerJobState + 18, // 14: ai.AIService.RunAgent:input_type -> ai.AgentRequest + 21, // 15: ai.AIService.CancelAnswerJob:input_type -> ai.CancelAnswerJobRequest + 3, // 16: ai.AIService.GenerateAnswer:input_type -> ai.AIRequest + 13, // 17: ai.AIService.GenerateStateless:input_type -> ai.StatelessRequest + 14, // 18: ai.AIService.HealthCheck:input_type -> ai.HealthCheckRequest + 8, // 19: ai.AIService.CreateSession:input_type -> ai.CreateSessionRequest + 10, // 20: ai.AIService.AskInSession:input_type -> ai.AskInSessionRequest + 6, // 21: ai.AIService.GetAnswerJob:input_type -> ai.GetAnswerJobRequest + 20, // 22: ai.AIService.RunAgent:output_type -> ai.AgentResponse + 22, // 23: ai.AIService.CancelAnswerJob:output_type -> ai.CancelAnswerJobResponse + 5, // 24: ai.AIService.GenerateAnswer:output_type -> ai.AIResponse + 5, // 25: ai.AIService.GenerateStateless:output_type -> ai.AIResponse + 15, // 26: ai.AIService.HealthCheck:output_type -> ai.HealthCheckResponse + 9, // 27: ai.AIService.CreateSession:output_type -> ai.CreateSessionResponse + 11, // 28: ai.AIService.AskInSession:output_type -> ai.SessionStatusResponse + 7, // 29: ai.AIService.GetAnswerJob:output_type -> ai.AnswerJobResponse + 22, // [22:30] is the sub-list for method output_type + 14, // [14:22] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name +} + +func init() { file_ai_proto_init() } +func file_ai_proto_init() { + if File_ai_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ai_proto_rawDesc), len(file_ai_proto_rawDesc)), + NumEnums: 3, + NumMessages: 21, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ai_proto_goTypes, + DependencyIndexes: file_ai_proto_depIdxs, + EnumInfos: file_ai_proto_enumTypes, + MessageInfos: file_ai_proto_msgTypes, + }.Build() + File_ai_proto = out.File + file_ai_proto_goTypes = nil + file_ai_proto_depIdxs = nil +} diff --git a/proto/l-ai-core-proto/ai.proto b/proto/l-ai-core-proto/ai.proto new file mode 100644 index 0000000..2ca7bbe --- /dev/null +++ b/proto/l-ai-core-proto/ai.proto @@ -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 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); +} diff --git a/proto/l-ai-core-proto/ai_grpc.pb.go b/proto/l-ai-core-proto/ai_grpc.pb.go new file mode 100644 index 0000000..f139501 --- /dev/null +++ b/proto/l-ai-core-proto/ai_grpc.pb.go @@ -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", +}