Initial Kaya board-mention agent daemon.
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful

Thin WS client to l-ai-tasks that drafts short core RunAgent replies on @kaya mentions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
alex 2026-09-21 23:07:24 +03:00
commit adb6c5ab8a
20 changed files with 3726 additions and 0 deletions

17
.env.example Normal file
View File

@ -0,0 +1,17 @@
# Tasks board (WebSocket + REST)
TASKS_WS_URL=ws://localhost:8080/api/ws
TASKS_API_URL=http://localhost:8080
TASKS_AGENT_TOKEN=
# l-ai-core AIService.RunAgent
CORE_ENDPOINT=localhost:50051
CORE_TOKEN=
CORE_INSECURE=true
MODEL=deepseek-v4-flash
AGENT_PROFILE_PATH=profiles/kaya.yaml
HEALTH_LISTEN_ADDR=:8087
METRICS_LISTEN_ADDR=:9097
SHUTDOWN_TIMEOUT=15s
RESPONSE_TIMEOUT=120s

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
*.exe
*.test
*.out
.env
.idea/
.vscode/
bin/
dist/

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "proto/l-ai-core-proto"]
path = proto/l-ai-core-proto
url = https://gitea.fe.kaien.ru/kaien/l-ai-core-proto.git

73
.woodpecker.yml Normal file
View File

@ -0,0 +1,73 @@
clone:
git:
image: woodpeckerci/plugin-git
settings:
submodule_recursive: true
steps:
- name: test
image: golang:1.25-alpine
commands:
- apk add --no-cache git build-base
- go build ./...
- go vet ./...
- go test ./...
when:
- event: [push, pull_request, manual]
- name: build-and-push
image: gcr.io/kaniko-project/executor:v1.23.2-debug
environment:
ZOT_USERNAME:
from_secret: ZOT_USERNAME
ZOT_PASSWORD:
from_secret: ZOT_PASSWORD
commands:
- mkdir -p /kaniko/.docker
- >-
printf '{"auths":{"registry.be.kaien.ru":{"auth":"%s"}}}'
"$(printf '%s:%s' "$ZOT_USERNAME" "$ZOT_PASSWORD" | base64 | tr -d '\n')"
> /kaniko/.docker/config.json
- >-
/kaniko/executor
--context "$CI_WORKSPACE"
--dockerfile "$CI_WORKSPACE/Dockerfile"
--destination "registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:$CI_COMMIT_SHA"
--destination "registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:latest"
depends_on:
- test
when:
- branch: master
event: [push, manual]
- name: deploy
image: alpine:latest
environment:
SSH_PRIVATE_KEY:
from_secret: SSH_PRIVATE_KEY
DEPLOY_HOST:
from_secret: SWARM_DEPLOY_HOST
DEPLOY_USER:
from_secret: SWARM_DEPLOY_USER
commands:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-add ~/.ssh/id_rsa
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
- >-
ssh -n $DEPLOY_USER@$DEPLOY_HOST
"docker service update
--image registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:$CI_COMMIT_SHA
--with-registry-auth
--update-order start-first
--detach=false
l-ai_agent-kaya"
depends_on:
- build-and-push
when:
- branch: master
event: [push, manual]

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM golang:1.25-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN apk add --no-cache git
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /kaya ./cmd/kaya
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=build /kaya /app/kaya
COPY profiles /app/profiles
EXPOSE 8087 9097
ENTRYPOINT ["/app/kaya"]

45
README.md Normal file
View File

@ -0,0 +1,45 @@
# l-ai-agent-kaya
Thin Tasks board agent: replies to `@kaya` mentions in task comments.
## Behavior
Long-lived daemon:
1. Connects to `l-ai-tasks` WebSocket (`TASKS_WS_URL`) with reconnect.
2. On `comment.added` where mentions include agent `kaya` and author ≠ `kaya`:
- dedupes by comment id
- GET task + comments via REST
- calls `l-ai-core` `AIService.RunAgent` (tools disabled) with Kaya persona
- POST comment with `Idempotency-Key: mention-reply:<comment_id>`
No Telegram / Live Chat integration.
## Identity
- `agent_id`: `kaya`
- Display: Kaya / Кая
- Profile: `profiles/kaya.yaml`
## Env
| Variable | Required | Default |
|---|---|---|
| `TASKS_WS_URL` | yes | — |
| `TASKS_API_URL` | yes | — |
| `TASKS_AGENT_TOKEN` | yes | — |
| `CORE_ENDPOINT` | no | `core:50051` |
| `CORE_TOKEN` | yes | — |
| `CORE_INSECURE` | no | `false` |
| `MODEL` | no | `deepseek-v4-flash` |
| `AGENT_PROFILE_PATH` | no | `profiles/kaya.yaml` |
| `HEALTH_LISTEN_ADDR` | no | `:8087` |
| `METRICS_LISTEN_ADDR` | no | `:9097` |
## Build
```bash
go build -o kaya ./cmd/kaya
```
Image binary: `/app/kaya`. Health `:8087/healthz`, metrics `:9097/metrics`.

86
cmd/kaya/main.go Normal file
View File

@ -0,0 +1,86 @@
package main
import (
"context"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/prometheus/client_golang/prometheus/promhttp"
"l-ai-agent-kaya/internal/config"
"l-ai-agent-kaya/internal/inference"
"l-ai-agent-kaya/internal/profile"
"l-ai-agent-kaya/internal/tasksboard"
)
func main() {
if err := run(); err != nil {
slog.Error("kaya stopped", "error", err)
os.Exit(1)
}
}
func run() error {
cfg, err := config.Load()
if err != nil {
return err
}
prof, err := profile.Load(cfg.ProfilePath)
if err != nil {
return err
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
core, err := inference.DialCore(cfg.CoreEndpoint, cfg.CoreToken, cfg.CoreInsecure)
if err != nil {
return err
}
defer core.Close()
go serveHTTP(cfg.MetricsListenAddr, promhttp.Handler())
go serveHTTP(cfg.HealthListenAddr, healthHandler())
loop := &tasksboard.Loop{
WSURL: cfg.TasksWSURL,
Token: cfg.TasksAgentToken,
REST: &tasksboard.Client{BaseURL: cfg.TasksAPIURL, Token: cfg.TasksAgentToken},
Core: core,
Profile: prof,
Model: cfg.Model,
ResponseTimeout: cfg.ResponseTimeout,
Log: slog.Default(),
}
go loop.Run(ctx)
<-ctx.Done()
shutdown, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
defer cancel()
<-shutdown.Done()
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return ctx.Err()
}
func serveHTTP(addr string, h http.Handler) {
if addr == "" {
return
}
if err := http.ListenAndServe(addr, h); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("http server stopped", "addr", addr, "error", err)
}
}
func healthHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok\n"))
})
return mux
}

26
go.mod Normal file
View File

@ -0,0 +1,26 @@
module l-ai-agent-kaya
go 1.25.0
require (
github.com/gorilla/websocket v1.5.3
github.com/prometheus/client_golang v1.23.2
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.8
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
)

74
go.sum Normal file
View File

@ -0,0 +1,74 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

81
internal/config/config.go Normal file
View File

@ -0,0 +1,81 @@
package config
import (
"errors"
"os"
"strconv"
"time"
)
type Config struct {
ProfilePath string
TasksWSURL string
TasksAPIURL string
TasksAgentToken string
CoreEndpoint string
CoreToken string
CoreInsecure bool
Model string
MetricsListenAddr string
HealthListenAddr string
ShutdownTimeout time.Duration
ResponseTimeout time.Duration
}
func Load() (Config, error) {
c := Config{
ProfilePath: env("AGENT_PROFILE_PATH", "profiles/kaya.yaml"),
TasksWSURL: os.Getenv("TASKS_WS_URL"),
TasksAPIURL: os.Getenv("TASKS_API_URL"),
TasksAgentToken: os.Getenv("TASKS_AGENT_TOKEN"),
CoreEndpoint: env("CORE_ENDPOINT", "core:50051"),
CoreToken: os.Getenv("CORE_TOKEN"),
CoreInsecure: envBool("CORE_INSECURE", false),
Model: env("MODEL", "deepseek-v4-flash"),
MetricsListenAddr: env("METRICS_LISTEN_ADDR", ":9097"),
HealthListenAddr: env("HEALTH_LISTEN_ADDR", ":8087"),
ShutdownTimeout: envDur("SHUTDOWN_TIMEOUT", 15*time.Second),
ResponseTimeout: envDur("RESPONSE_TIMEOUT", 120*time.Second),
}
if c.TasksWSURL == "" {
return c, errors.New("TASKS_WS_URL is required")
}
if c.TasksAPIURL == "" {
return c, errors.New("TASKS_API_URL is required")
}
if c.TasksAgentToken == "" {
return c, errors.New("TASKS_AGENT_TOKEN is required")
}
if c.CoreToken == "" {
return c, errors.New("CORE_TOKEN is required")
}
if c.ResponseTimeout <= 0 {
return c, errors.New("RESPONSE_TIMEOUT must be positive")
}
return c, nil
}
func env(k, d string) string {
if v := os.Getenv(k); v != "" {
return v
}
return d
}
func envBool(k string, d bool) bool {
if v := os.Getenv(k); v != "" {
b, _ := strconv.ParseBool(v)
return b
}
return d
}
func envDur(k string, d time.Duration) time.Duration {
if v := os.Getenv(k); v != "" {
x, err := time.ParseDuration(v)
if err == nil {
return x
}
}
return d
}

View File

@ -0,0 +1,99 @@
package inference
import (
"context"
"crypto/tls"
"fmt"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
corepb "l-ai-agent-kaya/proto/l-ai-core-proto"
)
type CoreRunner struct {
api corepb.AIServiceClient
token string
conn *grpc.ClientConn
}
func DialCore(target, token string, insecureTransport bool) (*CoreRunner, error) {
var tc credentials.TransportCredentials
if insecureTransport {
tc = insecure.NewCredentials()
} else {
tc = credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})
}
conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(tc))
if err != nil {
return nil, err
}
return &CoreRunner{api: corepb.NewAIServiceClient(conn), token: token, conn: conn}, nil
}
func (r *CoreRunner) Close() error {
if r.conn != nil {
return r.conn.Close()
}
return nil
}
func (r *CoreRunner) auth(ctx context.Context) context.Context {
if strings.TrimSpace(r.token) == "" {
return ctx
}
return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+r.token)
}
func (r *CoreRunner) RunAgent(ctx context.Context, req RunRequest) (RunResult, error) {
msgs := make([]*corepb.StatelessMessage, 0, len(req.Messages))
for _, m := range req.Messages {
msgs = append(msgs, &corepb.StatelessMessage{Role: m.Role, Content: m.Content})
}
toolsEnabled := req.ToolsEnabled
maxIters := uint32(1)
maxCalls := uint32(0)
if toolsEnabled {
maxIters = 12
maxCalls = 20
}
resp, err := r.api.RunAgent(r.auth(ctx), &corepb.AgentRequest{
Messages: msgs,
ModelName: req.Model,
AgentId: req.AgentID,
ToolLoop: &corepb.AgentToolLoopOptions{
ToolsEnabled: toolsEnabled,
MaxToolIterations: maxIters,
MaxTotalToolCalls: maxCalls,
TotalTimeoutSeconds: req.TimeoutSeconds,
},
ResponseContract: &corepb.AgentResponseContract{
Format: corepb.AgentOutputFormat_AGENT_OUTPUT_FORMAT_TEXT,
},
RequestId: req.RequestID,
IdempotencyKey: req.IdempotencyKey,
SupportsDeferred: false,
Metadata: req.Metadata,
})
if err != nil {
return RunResult{}, err
}
if resp.Error != "" {
return RunResult{}, fmt.Errorf("core RunAgent response error: %s", resp.Error)
}
if resp.Status == corepb.AnswerStatus_ANSWER_STATUS_PENDING {
return RunResult{}, fmt.Errorf("unexpected deferred RunAgent response")
}
out := RunResult{Content: resp.Content, DurationMS: resp.DurationMs}
if resp.Usage != nil {
out.ToolsUsed = resp.Usage.ToolsUsed
}
if !toolsEnabled && len(out.ToolsUsed) != 0 {
return RunResult{}, fmt.Errorf("tools used despite tools-disabled request")
}
return out, nil
}

View File

@ -0,0 +1,29 @@
package inference
import "context"
type ChatMessage struct {
Role string
Content string
}
type RunRequest struct {
Messages []ChatMessage
Model string
AgentID string
RequestID string
IdempotencyKey string
TimeoutSeconds uint32
Metadata map[string]string
ToolsEnabled bool
}
type RunResult struct {
Content string
DurationMS float64
ToolsUsed []string
}
type Runner interface {
RunAgent(context.Context, RunRequest) (RunResult, error)
}

107
internal/profile/profile.go Normal file
View File

@ -0,0 +1,107 @@
package profile
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
type Profile struct {
Version int `yaml:"version"`
Agent struct {
ID string `yaml:"id"`
Identity struct {
Name string `yaml:"name"`
Aliases map[string]string `yaml:"aliases"`
Role string `yaml:"role"`
SelfAwareness string `yaml:"self_awareness"`
} `yaml:"identity"`
PublicProfile struct {
Bio string `yaml:"bio"`
Status string `yaml:"status"`
} `yaml:"public_profile"`
} `yaml:"agent"`
Personality struct {
Traits []string `yaml:"traits"`
Style struct {
Tone string `yaml:"tone"`
Verbosity string `yaml:"verbosity"`
Formality string `yaml:"formality"`
} `yaml:"style"`
Avoid []string `yaml:"avoid"`
} `yaml:"personality"`
Behavior struct {
Language struct {
MatchConversation bool `yaml:"match_conversation"`
RussianDisplayName string `yaml:"russian_display_name"`
EnglishDisplayName string `yaml:"english_display_name"`
} `yaml:"language"`
} `yaml:"behavior"`
LLM struct {
Response struct {
Model string `yaml:"model"`
} `yaml:"response"`
} `yaml:"llm"`
}
func Load(path string) (Profile, error) {
data, err := os.ReadFile(path)
if err != nil {
return Profile{}, err
}
var p Profile
if err := yaml.Unmarshal(data, &p); err != nil {
return Profile{}, err
}
if strings.TrimSpace(p.Agent.ID) == "" {
return Profile{}, fmt.Errorf("profile %s: agent.id is required", path)
}
return p, nil
}
// SystemPrompt builds a compact persona prompt for Tasks board mention replies.
func (p Profile) SystemPrompt() string {
name := p.Agent.Identity.Name
if name == "" {
name = p.Agent.ID
}
ru := p.Behavior.Language.RussianDisplayName
if ru == "" {
ru = p.Agent.Identity.Aliases["ru"]
}
var b strings.Builder
b.WriteString(fmt.Sprintf("You are %s", name))
if ru != "" && !strings.EqualFold(ru, name) {
b.WriteString(fmt.Sprintf(" / %s", ru))
}
b.WriteString(fmt.Sprintf(" (agent_id=%s), an AI agent on the l-ai Tasks board.\n", p.Agent.ID))
if bio := strings.TrimSpace(p.Agent.PublicProfile.Bio); bio != "" {
b.WriteString(bio)
b.WriteByte('\n')
}
if role := strings.TrimSpace(p.Agent.Identity.Role); role != "" {
b.WriteString("Role: ")
b.WriteString(role)
b.WriteByte('\n')
}
if len(p.Personality.Traits) > 0 {
b.WriteString("Traits: ")
b.WriteString(strings.Join(p.Personality.Traits, ", "))
b.WriteByte('\n')
}
if s := p.Personality.Style; s.Tone != "" || s.Verbosity != "" || s.Formality != "" {
b.WriteString(fmt.Sprintf("Style: tone=%s verbosity=%s formality=%s\n", s.Tone, s.Verbosity, s.Formality))
}
if len(p.Personality.Avoid) > 0 {
b.WriteString("Avoid: ")
b.WriteString(strings.Join(p.Personality.Avoid, "; "))
b.WriteByte('\n')
}
b.WriteString("You reply only to task comment @mentions. Be helpful and concise. ")
b.WriteString("Match the language of the triggering comment (Russian or English). ")
b.WriteString("Do not invent task state that is not in the provided context. ")
b.WriteString("Output only the comment body to post — no preamble or quotes.\n")
return b.String()
}

201
internal/tasksboard/loop.go Normal file
View File

@ -0,0 +1,201 @@
package tasksboard
import (
"context"
"fmt"
"log/slog"
"strings"
"sync"
"time"
"l-ai-agent-kaya/internal/inference"
"l-ai-agent-kaya/internal/profile"
"l-ai-agent-kaya/internal/tasksws"
)
const agentID = "kaya"
// Loop listens to Tasks board WS and replies to @kaya mentions.
type Loop struct {
WSURL string
Token string
REST *Client
Core inference.Runner
Profile profile.Profile
Model string
ResponseTimeout time.Duration
Log *slog.Logger
mu sync.Mutex
seen map[string]struct{}
seenQ []string
maxSeen int
}
func (l *Loop) log() *slog.Logger {
if l.Log != nil {
return l.Log
}
return slog.Default()
}
// Run blocks until ctx is cancelled.
func (l *Loop) Run(ctx context.Context) {
if l.seen == nil {
l.seen = map[string]struct{}{}
}
if l.maxSeen <= 0 {
l.maxSeen = 4096
}
client := &tasksws.Client{
URL: l.WSURL,
Token: l.Token,
Agent: agentID,
Log: l.log(),
OnEvent: func(ctx context.Context, ev tasksws.Event) error {
return l.handle(ctx, ev)
},
}
l.log().Info("tasks board loop starting", "ws", l.WSURL, "agent", agentID)
client.Run(ctx)
}
func (l *Loop) handle(ctx context.Context, ev tasksws.Event) error {
if ev.Type != "comment.added" {
return nil
}
if ev.Comment == nil || ev.Task == nil {
return nil
}
if !tasksws.MentionsSelf(ev, agentID) {
return nil
}
if strings.EqualFold(ev.Comment.AuthorID, agentID) {
return nil
}
commentID := ev.Comment.ID
if commentID == "" {
return nil
}
if !l.markSeen(commentID) {
return nil
}
taskID := ev.Task.ID
l.log().Info("mention reply", "comment_id", commentID, "task_id", taskID, "author", ev.Comment.AuthorID)
replyCtx, cancel := context.WithTimeout(ctx, l.ResponseTimeout)
defer cancel()
task, err := l.REST.GetTask(replyCtx, taskID)
if err != nil {
return fmt.Errorf("get task: %w", err)
}
comments, err := l.REST.ListComments(replyCtx, taskID)
if err != nil {
return fmt.Errorf("list comments: %w", err)
}
userPrompt := buildUserPrompt(task, comments, commentID)
timeoutSec := uint32(l.ResponseTimeout.Seconds())
if timeoutSec < 30 {
timeoutSec = 30
}
model := l.Model
if model == "" && l.Profile.LLM.Response.Model != "" {
model = l.Profile.LLM.Response.Model
}
if model == "" {
model = "deepseek-v4-flash"
}
res, err := l.Core.RunAgent(replyCtx, inference.RunRequest{
Messages: []inference.ChatMessage{
{Role: "system", Content: l.Profile.SystemPrompt()},
{Role: "user", Content: userPrompt},
},
Model: model,
AgentID: agentID,
RequestID: "mention-reply:" + commentID,
IdempotencyKey: "mention-reply:" + commentID,
TimeoutSeconds: timeoutSec,
Metadata: map[string]string{"trace_reason": "kaya_mention_reply", "comment_id": commentID, "task_id": taskID},
ToolsEnabled: false,
})
if err != nil {
return fmt.Errorf("RunAgent: %w", err)
}
body := strings.TrimSpace(res.Content)
if body == "" {
return fmt.Errorf("empty agent reply for comment %s", commentID)
}
_, err = l.REST.PostComment(replyCtx, taskID, body, "mention-reply:"+commentID)
if err != nil {
return fmt.Errorf("post comment: %w", err)
}
l.log().Info("mention reply posted", "comment_id", commentID, "task_id", taskID, "duration_ms", res.DurationMS)
return nil
}
func (l *Loop) markSeen(id string) bool {
l.mu.Lock()
defer l.mu.Unlock()
if _, ok := l.seen[id]; ok {
return false
}
l.seen[id] = struct{}{}
l.seenQ = append(l.seenQ, id)
for len(l.seenQ) > l.maxSeen {
old := l.seenQ[0]
l.seenQ = l.seenQ[1:]
delete(l.seen, old)
}
return true
}
func buildUserPrompt(task Task, comments []Comment, triggerID string) string {
var b strings.Builder
b.WriteString("Task context:\n")
b.WriteString(fmt.Sprintf("- id: %s\n", task.ID))
b.WriteString(fmt.Sprintf("- title: %s\n", task.Title))
b.WriteString(fmt.Sprintf("- status: %s\n", task.Status))
b.WriteString(fmt.Sprintf("- priority: %d\n", task.Priority))
if task.Project != nil {
b.WriteString(fmt.Sprintf("- project: %s (%s)\n", task.Project.Name, task.Project.Slug))
}
if task.Assignee != nil {
name := task.Assignee.DisplayName
if name == "" {
name = task.Assignee.ID
}
b.WriteString(fmt.Sprintf("- assignee: %s/%s (%s)\n", task.Assignee.Type, task.Assignee.ID, name))
}
if desc := strings.TrimSpace(task.Description); desc != "" {
b.WriteString("- description:\n")
b.WriteString(trimRunes(desc, 4000))
b.WriteByte('\n')
}
b.WriteString("\nComment thread (oldest first):\n")
for _, c := range comments {
marker := ""
if c.ID == triggerID {
marker = " ← reply to this"
}
b.WriteString(fmt.Sprintf("[%s] %s/%s:%s\n%s\n\n",
c.CreatedAt.UTC().Format(time.RFC3339),
c.AuthorType, c.AuthorID, marker,
trimRunes(c.Body, 2000),
))
}
b.WriteString("Write a helpful board comment reply to the marked mention.\n")
return b.String()
}
func trimRunes(s string, max int) string {
r := []rune(s)
if len(r) <= max {
return s
}
return string(r[:max]) + "…"
}

119
internal/tasksboard/rest.go Normal file
View File

@ -0,0 +1,119 @@
package tasksboard
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
type Client struct {
BaseURL string
Token string
HTTPClient *http.Client
}
func (c *Client) http() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return http.DefaultClient
}
type Task struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
Priority int `json:"priority"`
Project *struct {
ID string `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
} `json:"project,omitempty"`
Assignee *struct {
Type string `json:"type"`
ID string `json:"id"`
DisplayName string `json:"display_name,omitempty"`
} `json:"assignee,omitempty"`
}
type Comment struct {
ID string `json:"id"`
TaskID string `json:"task_id"`
AuthorType string `json:"author_type"`
AuthorID string `json:"author_id"`
Body string `json:"body"`
CreatedAt time.Time `json:"created_at"`
}
func (c *Client) GetTask(ctx context.Context, taskID string) (Task, error) {
var t Task
if err := c.doJSON(ctx, http.MethodGet, "/api/v1/tasks/"+url.PathEscape(taskID), "", nil, &t); err != nil {
return Task{}, err
}
return t, nil
}
func (c *Client) ListComments(ctx context.Context, taskID string) ([]Comment, error) {
var out struct {
Comments []Comment `json:"comments"`
}
if err := c.doJSON(ctx, http.MethodGet, "/api/v1/tasks/"+url.PathEscape(taskID)+"/comments", "", nil, &out); err != nil {
return nil, err
}
return out.Comments, nil
}
func (c *Client) PostComment(ctx context.Context, taskID, body, idempotencyKey string) (Comment, error) {
var out Comment
payload := map[string]string{"body": body}
if err := c.doJSON(ctx, http.MethodPost, "/api/v1/tasks/"+url.PathEscape(taskID)+"/comments", idempotencyKey, payload, &out); err != nil {
return Comment{}, err
}
return out, nil
}
func (c *Client) doJSON(ctx context.Context, method, path, idempotencyKey string, body any, dst any) error {
base := strings.TrimRight(c.BaseURL, "/")
var rdr io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return err
}
rdr = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, base+path, rdr)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.Token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
resp, err := c.http().Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("tasks %s %s: status %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(data)))
}
if dst == nil || len(data) == 0 {
return nil
}
return json.Unmarshal(data, dst)
}

185
internal/tasksws/client.go Normal file
View File

@ -0,0 +1,185 @@
// Package tasksws is a reconnecting WebSocket client for l-ai-tasks board events.
package tasksws
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"math/rand"
"net/http"
"net/url"
"strconv"
"time"
"github.com/gorilla/websocket"
)
// Event mirrors l-ai-tasks realtime.Event JSON.
type Event struct {
Type string `json:"type"`
Seq int64 `json:"seq"`
At time.Time `json:"at"`
Project *Ref `json:"project,omitempty"`
Task *TaskRef `json:"task,omitempty"`
Comment *Comment `json:"comment,omitempty"`
Mentions []Mention `json:"mentions,omitempty"`
}
type Ref struct {
ID string `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
}
type TaskRef struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
AssigneeID string `json:"assignee_id,omitempty"`
}
type Comment struct {
ID string `json:"id"`
Body string `json:"body"`
AuthorType string `json:"author_type"`
AuthorID string `json:"author_id"`
}
type Mention struct {
Type string `json:"type"`
ID string `json:"id"`
}
// Handler is called for each board event (after ready).
type Handler func(ctx context.Context, ev Event) error
// Client reconnects forever until ctx cancel.
type Client struct {
URL string
Token string
Agent string // logical id for filtering (nora, coordinator, kaya)
Log *slog.Logger
OnEvent Handler
}
// Run blocks until ctx is done.
func (c *Client) Run(ctx context.Context) {
log := c.Log
if log == nil {
log = slog.Default()
}
var since int64
attempt := 0
for {
if ctx.Err() != nil {
return
}
err := c.session(ctx, &since)
if ctx.Err() != nil {
return
}
attempt++
delay := backoff(attempt)
log.Warn("tasks ws disconnected; reconnecting", "error", err, "attempt", attempt, "delay", delay, "since", since)
select {
case <-ctx.Done():
return
case <-time.After(delay):
}
}
}
func (c *Client) session(ctx context.Context, since *int64) error {
u, err := url.Parse(c.URL)
if err != nil {
return err
}
q := u.Query()
q.Set("since", strconv.FormatInt(*since, 10))
u.RawQuery = q.Encode()
hdr := http.Header{}
if c.Token != "" {
hdr.Set("Authorization", "Bearer "+c.Token)
}
dialer := websocket.Dialer{HandshakeTimeout: 15 * time.Second}
conn, _, err := dialer.DialContext(ctx, u.String(), hdr)
if err != nil {
return err
}
defer conn.Close()
_ = conn.SetReadDeadline(time.Now().Add(90 * time.Second))
conn.SetPongHandler(func(string) error {
_ = conn.SetReadDeadline(time.Now().Add(90 * time.Second))
return nil
})
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
_, data, err := conn.ReadMessage()
if err != nil {
return err
}
_ = conn.SetReadDeadline(time.Now().Add(90 * time.Second))
var probe struct {
Type string `json:"type"`
Seq int64 `json:"seq"`
}
if err := json.Unmarshal(data, &probe); err != nil {
continue
}
if probe.Type == "ready" {
continue
}
var ev Event
if err := json.Unmarshal(data, &ev); err != nil {
continue
}
if ev.Seq > *since {
*since = ev.Seq
}
if c.OnEvent != nil {
if err := c.OnEvent(ctx, ev); err != nil && c.Log != nil {
c.Log.Warn("tasks ws handler", "error", err, "type", ev.Type)
}
}
}
}
func backoff(attempt int) time.Duration {
if attempt < 1 {
attempt = 1
}
if attempt > 6 {
attempt = 6
}
base := 200 * time.Millisecond
d := base * time.Duration(1<<uint(attempt-1))
if d > 5*time.Second {
d = 5 * time.Second
}
jitter := time.Duration(rand.Int63n(int64(d/2) + 1))
return d/2 + jitter
}
// MentionsSelf reports whether ev.mentions includes agentID.
func MentionsSelf(ev Event, agentID string) bool {
for _, m := range ev.Mentions {
if m.Type == "agent" && m.ID == agentID {
return true
}
}
return false
}
// ErrAuth is returned conceptually for logging; dial errors wrap status.
func FormatDial(url, agent string) string {
return fmt.Sprintf("tasksws agent=%s url=%s", agent, url)
}

48
profiles/kaya.yaml Normal file
View File

@ -0,0 +1,48 @@
version: 1
agent:
id: kaya
identity:
name: Kaya
aliases:
ru: Кая
role: BoardAssistant
self_awareness: ai_agent
public_profile:
bio: "Friendly helper on the Tasks board — answers @kaya mentions in comments."
status: Ready
personality:
traits:
- friendly
- helpful
- clear
- concise
- pragmatic
- mildly_warm
style:
tone: friendly_helpful
verbosity: low_to_medium
formality: informal_professional
avoid:
- corporate_language
- fake_certainty
- excessive_disclaimers
- long_unsolicited_essays
- repeating_the_whole_thread
behavior:
language:
match_conversation: true
russian_display_name: Кая
english_display_name: Kaya
board_comments:
reply_to_mentions_only: true
keep_replies_short: true
llm:
response:
model: deepseek-v4-flash
tools:
enabled: false

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,243 @@
syntax = "proto3";
package ai;
option go_package = "git.be.kaien.ru/l-ai/l-ai-core-proto/proto";
// AIRequest содержит запрос к AI модели
message AIRequest {
string text = 1; // Текст запроса
string bot_username = 2; // Username бота для упоминаний
string model_name = 3; // Название модели (по умолчанию "deepseek-v4-flash")
bool enable_reasoning = 4; // Включить режим рассуждений (по умолчанию false)
int64 global_user_id = 5; // Global user ID для получения доп. данных (профиль, воспоминания)
AnswerGenerationOptions options = 6; // Дополнительные опции генерации ответа
int64 telegram_user_id = 7; // Telegram user ID для применения пользовательских настроек (стиль ответа)
int64 steam_user_id = 8; // SteamID64 отправителя для живых Steam-диалогов (l-ai-steam) - тот же "сигнал живого чата", что telegram_user_id, для второй платформы
bool supports_deferred = 9; // Клиент умеет обрабатывать status=PENDING + GetAnswerJob (async тяжёлые тулзы). Старые клиенты не ставят — тяжёлые тулзы исполняются синхронно, как раньше.
}
// AnswerGenerationOptions содержит дополнительные опции для генерации ответа
message AnswerGenerationOptions {
bool include_user_memories = 1; // Включить воспоминания о пользователе в промпт
}
// AIResponse содержит ответ от AI модели
message AIResponse {
string content = 1; // Текст ответа (при status=PENDING — interim-сообщение)
string error = 2; // Текст ошибки, если произошла
double duration_ms = 3; // Время выполнения в миллисекундах
string reasoning_content = 4; // Текст рассуждений (если включён режим reasoning)
AnswerStatus status = 5; // COMPLETE (или UNSPECIFIED для старых клиентов) — content финальный;
// PENDING — запущена долгая job, content это interim, финал тянуть через GetAnswerJob(job_id)
string job_id = 6; // Идентификатор отложенной job (только при status=PENDING)
}
// AnswerStatus описывает, готов ли ответ синхронно или отложен в job.
enum AnswerStatus {
ANSWER_STATUS_UNSPECIFIED = 0; // трактуется как COMPLETE (обратная совместимость)
ANSWER_STATUS_COMPLETE = 1; // content — финальный ответ
ANSWER_STATUS_PENDING = 2; // запущена долгая job; поллить GetAnswerJob(job_id)
}
// GetAnswerJobRequest запрашивает статус отложенной генерации ответа.
message GetAnswerJobRequest {
string job_id = 1;
}
// AnswerJobState — состояние отложенной генерации ответа.
// Значения 0-4 исторические (GenerateAnswer deferred). Значения 5-7 добавлены для
// durable RunAgent jobs; старые клиенты их не создают и потому не увидят. RUNNING
// для старого клиента семантически эквивалентен PENDING (job ещё не терминальна).
enum AnswerJobState {
ANSWER_JOB_STATE_UNSPECIFIED = 0;
ANSWER_JOB_STATE_PENDING = 1; // ещё не взята в работу / выполняется (evolver работает, модель дописывает)
ANSWER_JOB_STATE_DONE = 2; // готово, content заполнен
ANSWER_JOB_STATE_FAILED = 3; // ошибка, error заполнен
ANSWER_JOB_STATE_NOT_FOUND = 4; // неизвестный/протухший job_id (напр. после рестарта core)
ANSWER_JOB_STATE_RUNNING = 5; // взята воркером (lease активен), tool loop исполняется
ANSWER_JOB_STATE_CANCELLED = 6; // отменена через CancelAnswerJob
ANSWER_JOB_STATE_EXPIRED = 7; // истёк expiration до достижения терминального состояния
}
// AnswerJobResponse — результат опроса отложенной генерации.
message AnswerJobResponse {
AnswerJobState state = 1;
string content = 2; // финальный ответ, когда DONE
string error = 3; // причина, когда FAILED
double duration_ms = 4; // суммарное время генерации, когда DONE
// Authoritative usage завершённой deferred RunAgent job — тот же счётчик, что
// возвращает synchronous RunAgent (реальные iterations/tool calls core, не
// self-reported из ответа модели). Для legacy GenerateAnswer jobs не заполняется.
AgentUsage agent_usage = 5;
}
// CreateSessionRequest для создания новой AI сессии
message CreateSessionRequest {
string user_id = 1; // Идентификатор пользователя
string bot_username = 2; // Username бота для упоминаний
string model_name = 3; // Название модели (по умолчанию "deepseek-v4-flash")
int64 ttl_seconds = 4; // Время жизни сессии в секундах (по умолчанию 3600 = 1 час)
}
// CreateSessionResponse содержит ID созданной сессии
message CreateSessionResponse {
string session_id = 1; // ID сессии
string status = 2; // Статус сессии
string error = 3; // Текст ошибки, если произошла
}
// AskInSessionRequest для запроса в рамках сессии
message AskInSessionRequest {
string session_id = 1; // ID сессии
string text = 2; // Текст запроса
bool enable_reasoning = 3; // Включить режим рассуждений (по умолчанию false)
int64 global_user_id = 4; // Global user ID для получения воспоминаний
AnswerGenerationOptions options = 5; // Дополнительные опции генерации
}
// SessionStatusResponse содержит статус сессии и ответ AI
message SessionStatusResponse {
string session_id = 1; // ID сессии
string status = 2; // Статус сессии (active, expired, closed)
string content = 3; // Текст ответа от AI
string error = 4; // Текст ошибки, если произошла
double duration_ms = 5; // Время выполнения в миллисекундах
string reasoning_content = 6; // Текст рассуждений (если включён режим reasoning)
}
// StatelessMessage одно сообщение для stateless запроса (role + content)
message StatelessMessage {
string role = 1; // "system", "user", "assistant"
string content = 2; // Текст сообщения
}
// StatelessRequest для stateless генерации с полным контролем над messages
message StatelessRequest {
repeated StatelessMessage messages = 1; // Массив сообщений (system + history + user)
string model_name = 2; // Название модели (по умолчанию "deepseek-v4-flash")
}
// HealthCheckRequest для проверки здоровья сервиса
message HealthCheckRequest {}
// HealthCheckResponse для ответа на проверку здоровья
message HealthCheckResponse {
bool serving = 1;
string version = 2;
}
// ---------------------------------------------------------------------------
// RunAgent: надёжный агентный вызов (server-side tool filtering, deferred jobs).
// В отличие от GenerateAnswer, НЕ подмешивает Telegram/Steam-персонализацию и не
// требует telegram_user_id/steam_user_id. Вызывающая сторона полностью владеет
// массивом messages (включая system). Требует service-to-service авторизации
// (Bearer service-token в gRPC metadata) — agent_id/tool_profile сами по себе не
// являются авторизацией.
// ---------------------------------------------------------------------------
// AgentToolLoopOptions задаёт лимиты tool loop. Значения клиента могут только
// сузить серверные потолки (max_tool_iterations/max_total_tool_calls ≤ server max).
message AgentToolLoopOptions {
bool tools_enabled = 1; // false — модель отвечает без tools
uint32 max_tool_iterations = 2; // потолок round-trip'ов модель↔tools (0 = серверный default)
uint32 max_total_tool_calls = 3; // потолок РЕАЛЬНЫХ tool call'ов, не итераций (0 = серверный default)
uint32 tool_timeout_seconds = 4; // таймаут одного tool call (0 = серверный default)
uint32 total_timeout_seconds = 5; // wall-clock таймаут всего вызова (0 = серверный default), независим от budget
}
// AgentOutputFormat — требуемый формат финального ответа.
enum AgentOutputFormat {
AGENT_OUTPUT_FORMAT_UNSPECIFIED = 0; // трактуется как TEXT
AGENT_OUTPUT_FORMAT_TEXT = 1; // произвольный текст
AGENT_OUTPUT_FORMAT_JSON = 2; // должен быть валидный JSON (без schema-проверки)
AGENT_OUTPUT_FORMAT_JSON_SCHEMA = 3; // валидный JSON, проходящий json_schema
}
// AgentResponseContract описывает требования к структурированному ответу.
message AgentResponseContract {
AgentOutputFormat format = 1;
string json_schema = 2; // JSON Schema (draft-07), обязателен при format=JSON_SCHEMA
bool repair_invalid_output = 3; // при провале валидации — один repair-запрос модели (без новых tools)
}
// AgentRequest — запрос агентного вызова.
message AgentRequest {
repeated StatelessMessage messages = 1; // полный контекст, включая system
string model_name = 2; // модель (по умолчанию deepseek-v4-flash)
bool enable_reasoning = 3; // reasoning не попадает в обычный content
string agent_id = 4; // идентификатор логического агента (для логов/метрик, НЕ авторизация)
string tool_profile = 5; // серверный профиль tools (НЕ авторизация; проверяется против credential)
repeated string requested_tools = 6; // сужение профиля: effective = profile.allowed ∩ requested; пусто = все allowed
AgentToolLoopOptions tool_loop = 7;
AgentResponseContract response_contract = 8;
string request_id = 9; // сквозной идентификатор для трассировки
string idempotency_key = 10; // повторный RunAgent с тем же ключом не создаёт дубликат job
bool supports_deferred = 11; // клиент умеет status=PENDING + GetAnswerJob для тяжёлых операций
map<string, string> metadata = 12; // произвольные метки (не логируются как секреты)
}
// AgentUsage — счётчики использования за вызов.
message AgentUsage {
uint32 tool_iterations = 1; // сколько round-trip'ов модель↔tools произошло
uint32 total_tool_calls = 2; // сколько РЕАЛЬНЫХ tool call'ов исполнено (3 calls в одной итерации = 3)
repeated string tools_used = 3;
uint64 input_tokens = 4;
uint64 output_tokens = 5;
}
// AgentResponse — результат агентного вызова.
message AgentResponse {
AnswerStatus status = 1; // COMPLETE — content финальный; PENDING — тянуть через GetAnswerJob(job_id)
string content = 2;
string error = 3;
double duration_ms = 4;
string job_id = 5; // при status=PENDING
AgentUsage usage = 6;
}
// CancelAnswerJobRequest — запрос отмены отложенной job (идемпотентный).
message CancelAnswerJobRequest {
string job_id = 1;
string reason = 2; // причина отмены (сохраняется), опционально
}
// CancelAnswerJobResponse — результат отмены.
message CancelAnswerJobResponse {
AnswerJobState state = 1; // состояние job после запроса (CANCELLED, либо терминальное, если уже завершилась)
bool cancelled = 2; // true — отмена применена этим вызовом; false — job уже была терминальна/отменена
string error = 3;
}
// AIService - основной сервис для работы с AI
service AIService {
// RunAgent — надёжный агентный вызов с серверной фильтрацией tools и deferred
// jobs. Требует service-to-service авторизации.
rpc RunAgent(AgentRequest) returns (AgentResponse);
// CancelAnswerJob отменяет отложенную job (идемпотентно). Требует авторизации.
rpc CancelAnswerJob(CancelAnswerJobRequest) returns (CancelAnswerJobResponse);
// GenerateAnswer генерирует ответ на запрос
rpc GenerateAnswer(AIRequest) returns (AIResponse);
// GenerateStateless генерирует ответ из массива messages (system+history+user)
rpc GenerateStateless(StatelessRequest) returns (AIResponse);
// HealthCheck проверяет здоровье сервиса
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
// CreateSession созда новую AI сессию
rpc CreateSession(CreateSessionRequest) returns (CreateSessionResponse);
// AskInSession отправляет запрос в рамках существующей сессии
rpc AskInSession(AskInSessionRequest) returns (SessionStatusResponse);
// GetAnswerJob возвращает статус отложенной генерации ответа (когда GenerateAnswer
// вернул status=PENDING). Клиент поллит его до DONE/FAILED и постит финал.
rpc GetAnswerJob(GetAnswerJobRequest) returns (AnswerJobResponse);
}

View File

@ -0,0 +1,411 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v7.34.0
// source: ai.proto
package proto
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
AIService_RunAgent_FullMethodName = "/ai.AIService/RunAgent"
AIService_CancelAnswerJob_FullMethodName = "/ai.AIService/CancelAnswerJob"
AIService_GenerateAnswer_FullMethodName = "/ai.AIService/GenerateAnswer"
AIService_GenerateStateless_FullMethodName = "/ai.AIService/GenerateStateless"
AIService_HealthCheck_FullMethodName = "/ai.AIService/HealthCheck"
AIService_CreateSession_FullMethodName = "/ai.AIService/CreateSession"
AIService_AskInSession_FullMethodName = "/ai.AIService/AskInSession"
AIService_GetAnswerJob_FullMethodName = "/ai.AIService/GetAnswerJob"
)
// AIServiceClient is the client API for AIService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// AIService - основной сервис для работы с AI
type AIServiceClient interface {
// RunAgent — надёжный агентный вызов с серверной фильтрацией tools и deferred
// jobs. Требует service-to-service авторизации.
RunAgent(ctx context.Context, in *AgentRequest, opts ...grpc.CallOption) (*AgentResponse, error)
// CancelAnswerJob отменяет отложенную job (идемпотентно). Требует авторизации.
CancelAnswerJob(ctx context.Context, in *CancelAnswerJobRequest, opts ...grpc.CallOption) (*CancelAnswerJobResponse, error)
// GenerateAnswer генерирует ответ на запрос
GenerateAnswer(ctx context.Context, in *AIRequest, opts ...grpc.CallOption) (*AIResponse, error)
// GenerateStateless генерирует ответ из массива messages (system+history+user)
GenerateStateless(ctx context.Context, in *StatelessRequest, opts ...grpc.CallOption) (*AIResponse, error)
// HealthCheck проверяет здоровье сервиса
HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error)
// CreateSession созда новую AI сессию
CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*CreateSessionResponse, error)
// AskInSession отправляет запрос в рамках существующей сессии
AskInSession(ctx context.Context, in *AskInSessionRequest, opts ...grpc.CallOption) (*SessionStatusResponse, error)
// GetAnswerJob возвращает статус отложенной генерации ответа (когда GenerateAnswer
// вернул status=PENDING). Клиент поллит его до DONE/FAILED и постит финал.
GetAnswerJob(ctx context.Context, in *GetAnswerJobRequest, opts ...grpc.CallOption) (*AnswerJobResponse, error)
}
type aIServiceClient struct {
cc grpc.ClientConnInterface
}
func NewAIServiceClient(cc grpc.ClientConnInterface) AIServiceClient {
return &aIServiceClient{cc}
}
func (c *aIServiceClient) RunAgent(ctx context.Context, in *AgentRequest, opts ...grpc.CallOption) (*AgentResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AgentResponse)
err := c.cc.Invoke(ctx, AIService_RunAgent_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aIServiceClient) CancelAnswerJob(ctx context.Context, in *CancelAnswerJobRequest, opts ...grpc.CallOption) (*CancelAnswerJobResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CancelAnswerJobResponse)
err := c.cc.Invoke(ctx, AIService_CancelAnswerJob_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aIServiceClient) GenerateAnswer(ctx context.Context, in *AIRequest, opts ...grpc.CallOption) (*AIResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AIResponse)
err := c.cc.Invoke(ctx, AIService_GenerateAnswer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aIServiceClient) GenerateStateless(ctx context.Context, in *StatelessRequest, opts ...grpc.CallOption) (*AIResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AIResponse)
err := c.cc.Invoke(ctx, AIService_GenerateStateless_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aIServiceClient) HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(HealthCheckResponse)
err := c.cc.Invoke(ctx, AIService_HealthCheck_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aIServiceClient) CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*CreateSessionResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateSessionResponse)
err := c.cc.Invoke(ctx, AIService_CreateSession_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aIServiceClient) AskInSession(ctx context.Context, in *AskInSessionRequest, opts ...grpc.CallOption) (*SessionStatusResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SessionStatusResponse)
err := c.cc.Invoke(ctx, AIService_AskInSession_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *aIServiceClient) GetAnswerJob(ctx context.Context, in *GetAnswerJobRequest, opts ...grpc.CallOption) (*AnswerJobResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AnswerJobResponse)
err := c.cc.Invoke(ctx, AIService_GetAnswerJob_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AIServiceServer is the server API for AIService service.
// All implementations must embed UnimplementedAIServiceServer
// for forward compatibility.
//
// AIService - основной сервис для работы с AI
type AIServiceServer interface {
// RunAgent — надёжный агентный вызов с серверной фильтрацией tools и deferred
// jobs. Требует service-to-service авторизации.
RunAgent(context.Context, *AgentRequest) (*AgentResponse, error)
// CancelAnswerJob отменяет отложенную job (идемпотентно). Требует авторизации.
CancelAnswerJob(context.Context, *CancelAnswerJobRequest) (*CancelAnswerJobResponse, error)
// GenerateAnswer генерирует ответ на запрос
GenerateAnswer(context.Context, *AIRequest) (*AIResponse, error)
// GenerateStateless генерирует ответ из массива messages (system+history+user)
GenerateStateless(context.Context, *StatelessRequest) (*AIResponse, error)
// HealthCheck проверяет здоровье сервиса
HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
// CreateSession созда новую AI сессию
CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error)
// AskInSession отправляет запрос в рамках существующей сессии
AskInSession(context.Context, *AskInSessionRequest) (*SessionStatusResponse, error)
// GetAnswerJob возвращает статус отложенной генерации ответа (когда GenerateAnswer
// вернул status=PENDING). Клиент поллит его до DONE/FAILED и постит финал.
GetAnswerJob(context.Context, *GetAnswerJobRequest) (*AnswerJobResponse, error)
mustEmbedUnimplementedAIServiceServer()
}
// UnimplementedAIServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAIServiceServer struct{}
func (UnimplementedAIServiceServer) RunAgent(context.Context, *AgentRequest) (*AgentResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RunAgent not implemented")
}
func (UnimplementedAIServiceServer) CancelAnswerJob(context.Context, *CancelAnswerJobRequest) (*CancelAnswerJobResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CancelAnswerJob not implemented")
}
func (UnimplementedAIServiceServer) GenerateAnswer(context.Context, *AIRequest) (*AIResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GenerateAnswer not implemented")
}
func (UnimplementedAIServiceServer) GenerateStateless(context.Context, *StatelessRequest) (*AIResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GenerateStateless not implemented")
}
func (UnimplementedAIServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {
return nil, status.Error(codes.Unimplemented, "method HealthCheck not implemented")
}
func (UnimplementedAIServiceServer) CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateSession not implemented")
}
func (UnimplementedAIServiceServer) AskInSession(context.Context, *AskInSessionRequest) (*SessionStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AskInSession not implemented")
}
func (UnimplementedAIServiceServer) GetAnswerJob(context.Context, *GetAnswerJobRequest) (*AnswerJobResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAnswerJob not implemented")
}
func (UnimplementedAIServiceServer) mustEmbedUnimplementedAIServiceServer() {}
func (UnimplementedAIServiceServer) testEmbeddedByValue() {}
// UnsafeAIServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AIServiceServer will
// result in compilation errors.
type UnsafeAIServiceServer interface {
mustEmbedUnimplementedAIServiceServer()
}
func RegisterAIServiceServer(s grpc.ServiceRegistrar, srv AIServiceServer) {
// If the following call panics, it indicates UnimplementedAIServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&AIService_ServiceDesc, srv)
}
func _AIService_RunAgent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AgentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AIServiceServer).RunAgent(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AIService_RunAgent_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AIServiceServer).RunAgent(ctx, req.(*AgentRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AIService_CancelAnswerJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CancelAnswerJobRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AIServiceServer).CancelAnswerJob(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AIService_CancelAnswerJob_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AIServiceServer).CancelAnswerJob(ctx, req.(*CancelAnswerJobRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AIService_GenerateAnswer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AIRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AIServiceServer).GenerateAnswer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AIService_GenerateAnswer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AIServiceServer).GenerateAnswer(ctx, req.(*AIRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AIService_GenerateStateless_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StatelessRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AIServiceServer).GenerateStateless(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AIService_GenerateStateless_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AIServiceServer).GenerateStateless(ctx, req.(*StatelessRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AIService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HealthCheckRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AIServiceServer).HealthCheck(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AIService_HealthCheck_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AIServiceServer).HealthCheck(ctx, req.(*HealthCheckRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AIService_CreateSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateSessionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AIServiceServer).CreateSession(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AIService_CreateSession_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AIServiceServer).CreateSession(ctx, req.(*CreateSessionRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AIService_AskInSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AskInSessionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AIServiceServer).AskInSession(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AIService_AskInSession_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AIServiceServer).AskInSession(ctx, req.(*AskInSessionRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AIService_GetAnswerJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAnswerJobRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AIServiceServer).GetAnswerJob(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AIService_GetAnswerJob_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AIServiceServer).GetAnswerJob(ctx, req.(*GetAnswerJobRequest))
}
return interceptor(ctx, in, info, handler)
}
// AIService_ServiceDesc is the grpc.ServiceDesc for AIService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var AIService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "ai.AIService",
HandlerType: (*AIServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "RunAgent",
Handler: _AIService_RunAgent_Handler,
},
{
MethodName: "CancelAnswerJob",
Handler: _AIService_CancelAnswerJob_Handler,
},
{
MethodName: "GenerateAnswer",
Handler: _AIService_GenerateAnswer_Handler,
},
{
MethodName: "GenerateStateless",
Handler: _AIService_GenerateStateless_Handler,
},
{
MethodName: "HealthCheck",
Handler: _AIService_HealthCheck_Handler,
},
{
MethodName: "CreateSession",
Handler: _AIService_CreateSession_Handler,
},
{
MethodName: "AskInSession",
Handler: _AIService_AskInSession_Handler,
},
{
MethodName: "GetAnswerJob",
Handler: _AIService_GetAnswerJob_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "ai.proto",
}