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