Spaces:
Build error
Build error
Deployment commited on
Commit ·
4b1daed
1
Parent(s): 6b18e31
Automated deployment update
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +38 -0
- .env.example +118 -0
- .github/workflows/ci.yml +117 -0
- .gitignore +70 -0
- .golangci.yml +76 -0
- Dockerfile +51 -0
- Makefile +118 -0
- README.md +464 -7
- agent.pb.go +1837 -0
- agent_grpc.pb.go +291 -0
- cmd/agent-server/main.go +270 -0
- cmd/generator-server/main.go +290 -0
- cmd/retriever-server/main.go +317 -0
- cmd/worker/main.go +327 -0
- config.example.yaml +67 -0
- deploy.md +149 -0
- docs/API_GATEWAY.md +211 -0
- gateway.example.yaml +96 -0
- generator.pb.go +1414 -0
- generator_grpc.pb.go +291 -0
- go.mod +118 -0
- go.sum +497 -0
- go.work +10 -0
- internal/agent/interceptors.go +60 -0
- internal/agent/orchestrator.go +565 -0
- internal/agent/server.go +311 -0
- internal/cache/cache.go +310 -0
- internal/cache/cache_test.go +310 -0
- internal/gateway/cache/cache.go +100 -0
- internal/gateway/cmd/main.go +65 -0
- internal/gateway/config.go +204 -0
- internal/gateway/gateway.go +457 -0
- internal/gateway/gwtypes/types.go +196 -0
- internal/gateway/handlers/admin.go +93 -0
- internal/gateway/handlers/agent.go +270 -0
- internal/gateway/handlers/auth.go +115 -0
- internal/gateway/handlers/graphql.go +620 -0
- internal/gateway/handlers/handlers_test.go +292 -0
- internal/gateway/handlers/memory.go +152 -0
- internal/gateway/handlers/query.go +224 -0
- internal/gateway/handlers/voice_files.go +232 -0
- internal/gateway/handlers/websocket.go +289 -0
- internal/gateway/middleware/audit.go +133 -0
- internal/gateway/middleware/auth.go +311 -0
- internal/gateway/middleware/common.go +51 -0
- internal/gateway/middleware/cors.go +143 -0
- internal/gateway/middleware/middleware_test.go +388 -0
- internal/gateway/middleware/ratelimit.go +253 -0
- internal/gateway/middleware/tracing.go +96 -0
- internal/gateway/observability/metrics.go +104 -0
.dockerignore
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Version Control
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
.github
|
| 5 |
+
|
| 6 |
+
# Dependencies
|
| 7 |
+
**/node_modules
|
| 8 |
+
**/vendor
|
| 9 |
+
|
| 10 |
+
# Build Artifacts
|
| 11 |
+
**/bin
|
| 12 |
+
**/dist
|
| 13 |
+
**/.next
|
| 14 |
+
**/out
|
| 15 |
+
*.exe
|
| 16 |
+
*.test
|
| 17 |
+
*.prof
|
| 18 |
+
|
| 19 |
+
# IDEs
|
| 20 |
+
.vscode
|
| 21 |
+
.idea
|
| 22 |
+
*.swp
|
| 23 |
+
*.swo
|
| 24 |
+
|
| 25 |
+
# Environment Variables
|
| 26 |
+
.env
|
| 27 |
+
.env.*
|
| 28 |
+
!.env.example
|
| 29 |
+
|
| 30 |
+
# OS Files
|
| 31 |
+
.DS_Store
|
| 32 |
+
Thumbs.db
|
| 33 |
+
|
| 34 |
+
# Large Documentation/Assets
|
| 35 |
+
docs/
|
| 36 |
+
tests/integration
|
| 37 |
+
frontend/packages/*/dist
|
| 38 |
+
frontend/apps/*/dist
|
.env.example
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AmaniQuery Consolidated Environment Variables Example
|
| 2 |
+
# Copy this file to .env and adjust the values as needed.
|
| 3 |
+
|
| 4 |
+
# =============================================================================
|
| 5 |
+
# General Configuration
|
| 6 |
+
# =============================================================================
|
| 7 |
+
ENV=development
|
| 8 |
+
LOG_LEVEL=info
|
| 9 |
+
VERSION=1.0.0
|
| 10 |
+
|
| 11 |
+
# =============================================================================
|
| 12 |
+
# Server Configuration
|
| 13 |
+
# =============================================================================
|
| 14 |
+
# Main Service
|
| 15 |
+
AMANI_SERVER_GRPC_PORT=9090
|
| 16 |
+
AMANI_SERVER_HTTP_PORT=8080
|
| 17 |
+
AMANI_SERVER_GRACEFUL_TIMEOUT=30s
|
| 18 |
+
AMANI_SERVER_MAX_CONNECTIONS=1000
|
| 19 |
+
|
| 20 |
+
# Notification Service
|
| 21 |
+
PORT=:8080
|
| 22 |
+
NOTIF_SERVER_READ_TIMEOUT=30s
|
| 23 |
+
NOTIF_SERVER_WRITE_TIMEOUT=30s
|
| 24 |
+
NOTIF_SERVER_SHUTDOWN_TIMEOUT=30s
|
| 25 |
+
|
| 26 |
+
# =============================================================================
|
| 27 |
+
# LLM Providers (Main Service)
|
| 28 |
+
# =============================================================================
|
| 29 |
+
# Fallback order: Gemini -> Moonshot -> Ollama -> OpenAI -> Anthropic
|
| 30 |
+
GEMINI_API_KEY=your-gemini-api-key
|
| 31 |
+
GOOGLE_API_KEY=your-gemini-api-key # Alias for GEMINI_API_KEY
|
| 32 |
+
|
| 33 |
+
MOONSHOT_API_KEY=your-moonshot-api-key
|
| 34 |
+
|
| 35 |
+
OLLAMA_BASE_URL=http://localhost:11434
|
| 36 |
+
|
| 37 |
+
OPENAI_API_KEY=your-openai-api-key
|
| 38 |
+
|
| 39 |
+
ANTHROPIC_API_KEY=your-anthropic-api-key
|
| 40 |
+
|
| 41 |
+
# LLM Settings
|
| 42 |
+
AMANI_LLM_DEFAULT_MODEL=gemini-2.5-flash
|
| 43 |
+
AMANI_LLM_MAX_TOKENS=4096
|
| 44 |
+
AMANI_LLM_TEMPERATURE=0.7
|
| 45 |
+
AMANI_LLM_TIMEOUT=60s
|
| 46 |
+
AMANI_LLM_MAX_RETRIES=3
|
| 47 |
+
AMANI_LLM_ENABLE_FALLBACK=true
|
| 48 |
+
|
| 49 |
+
# =============================================================================
|
| 50 |
+
# Vector Store (Qdrant)
|
| 51 |
+
# =============================================================================
|
| 52 |
+
QDRANT_URL=localhost:6334
|
| 53 |
+
QDRANT_API_KEY=your_qdrant_api_key_here
|
| 54 |
+
AMANI_VECTOR_STORE_COLLECTION=amaniquery
|
| 55 |
+
AMANI_VECTOR_STORE_DIMENSION=1536
|
| 56 |
+
AMANI_VECTOR_STORE_DISTANCE=Cosine
|
| 57 |
+
|
| 58 |
+
# =============================================================================
|
| 59 |
+
# Caching & Queue (Redis)
|
| 60 |
+
# =============================================================================
|
| 61 |
+
# Used by both Main and Notification services
|
| 62 |
+
REDIS_URL=redis://localhost:6379
|
| 63 |
+
REDIS_ADDR=localhost:6379
|
| 64 |
+
REDIS_PASSWORD=
|
| 65 |
+
REDIS_DB=0
|
| 66 |
+
|
| 67 |
+
# =============================================================================
|
| 68 |
+
# Database (MongoDB)
|
| 69 |
+
# =============================================================================
|
| 70 |
+
# Primarily used by Notification Service and Memory Service
|
| 71 |
+
MONGO_URI=mongodb://localhost:27017
|
| 72 |
+
MONGO_DATABASE=amaniquery_notifications
|
| 73 |
+
|
| 74 |
+
# =============================================================================
|
| 75 |
+
# Security (JWT & Auth)
|
| 76 |
+
# =============================================================================
|
| 77 |
+
JWT_SECRET=your-secure-jwt-secret-key-at-least-32-characters
|
| 78 |
+
JWT_ISSUER=amaniquery
|
| 79 |
+
JWT_AUDIENCE=amaniquery-notifications
|
| 80 |
+
|
| 81 |
+
# =============================================================================
|
| 82 |
+
# Notification Service Providers
|
| 83 |
+
# =============================================================================
|
| 84 |
+
# Mailtrap (Email)
|
| 85 |
+
MAILTRAP_API_KEY=your-mailtrap-api-key
|
| 86 |
+
MAILTRAP_ACCOUNT_ID=your-mailtrap-account-id
|
| 87 |
+
MAILTRAP_SENDER_NAME=AmaniQuery
|
| 88 |
+
MAILTRAP_SENDER_EMAIL=noreply@yourdomain.com
|
| 89 |
+
|
| 90 |
+
# Africa's Talking (SMS)
|
| 91 |
+
AFRICASTALKING_USERNAME=sandbox
|
| 92 |
+
AFRICASTALKING_API_KEY=your-africastalking-api-key
|
| 93 |
+
AFRICASTALKING_SENDER_ID=AmaniQuery
|
| 94 |
+
AFRICASTALKING_SANDBOX=true
|
| 95 |
+
|
| 96 |
+
# =============================================================================
|
| 97 |
+
# Observability & Monitoring
|
| 98 |
+
# =============================================================================
|
| 99 |
+
JAEGER_ENDPOINT=localhost:4317
|
| 100 |
+
AMANI_OBSERVABILITY_TRACING_ENABLED=true
|
| 101 |
+
AMANI_OBSERVABILITY_METRICS_ENABLED=true
|
| 102 |
+
AMANI_OBSERVABILITY_METRICS_PORT=9091
|
| 103 |
+
|
| 104 |
+
# Prometheus (Notifications)
|
| 105 |
+
METRICS_ENABLED=true
|
| 106 |
+
METRICS_PORT=:9090
|
| 107 |
+
METRICS_PATH=/metrics
|
| 108 |
+
|
| 109 |
+
# Grafana
|
| 110 |
+
GRAFANA_PASSWORD=your-grafana-admin-password
|
| 111 |
+
|
| 112 |
+
# =============================================================================
|
| 113 |
+
# Memory Service Configuration
|
| 114 |
+
# =============================================================================
|
| 115 |
+
AMANI_MEMORY_RUST_SERVICE_HOST=localhost
|
| 116 |
+
AMANI_MEMORY_RUST_SERVICE_PORT=9091
|
| 117 |
+
AMANI_MEMORY_ENABLE_RUST_SERVICE=false
|
| 118 |
+
AMANI_MEMORY_ENABLE_GDPR=true
|
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [master, develop]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [master]
|
| 8 |
+
|
| 9 |
+
env:
|
| 10 |
+
GO_VERSION: "1.21"
|
| 11 |
+
|
| 12 |
+
jobs:
|
| 13 |
+
lint:
|
| 14 |
+
name: Lint
|
| 15 |
+
runs-on: ubuntu-latest
|
| 16 |
+
steps:
|
| 17 |
+
- uses: actions/checkout@v4
|
| 18 |
+
|
| 19 |
+
- name: Set up Go
|
| 20 |
+
uses: actions/setup-go@v5
|
| 21 |
+
with:
|
| 22 |
+
go-version: ${{ env.GO_VERSION }}
|
| 23 |
+
cache: true
|
| 24 |
+
|
| 25 |
+
- name: golangci-lint
|
| 26 |
+
uses: golangci/golangci-lint-action@v3
|
| 27 |
+
with:
|
| 28 |
+
version: v1.55.2
|
| 29 |
+
args: --timeout=5m
|
| 30 |
+
|
| 31 |
+
test:
|
| 32 |
+
name: Test
|
| 33 |
+
runs-on: ubuntu-latest
|
| 34 |
+
steps:
|
| 35 |
+
- uses: actions/checkout@v4
|
| 36 |
+
|
| 37 |
+
- name: Set up Go
|
| 38 |
+
uses: actions/setup-go@v5
|
| 39 |
+
with:
|
| 40 |
+
go-version: ${{ env.GO_VERSION }}
|
| 41 |
+
cache: true
|
| 42 |
+
|
| 43 |
+
- name: Run tests
|
| 44 |
+
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
|
| 45 |
+
|
| 46 |
+
- name: Upload coverage
|
| 47 |
+
uses: codecov/codecov-action@v3
|
| 48 |
+
with:
|
| 49 |
+
file: ./coverage.out
|
| 50 |
+
fail_ci_if_error: false
|
| 51 |
+
|
| 52 |
+
build:
|
| 53 |
+
name: Build
|
| 54 |
+
runs-on: ubuntu-latest
|
| 55 |
+
needs: [lint, test]
|
| 56 |
+
steps:
|
| 57 |
+
- uses: actions/checkout@v4
|
| 58 |
+
|
| 59 |
+
- name: Set up Go
|
| 60 |
+
uses: actions/setup-go@v5
|
| 61 |
+
with:
|
| 62 |
+
go-version: ${{ env.GO_VERSION }}
|
| 63 |
+
cache: true
|
| 64 |
+
|
| 65 |
+
- name: Build agent-server
|
| 66 |
+
run: go build -ldflags="-s -w" -o bin/agent-server ./cmd/agent-server
|
| 67 |
+
|
| 68 |
+
docker:
|
| 69 |
+
name: Docker Build
|
| 70 |
+
runs-on: ubuntu-latest
|
| 71 |
+
needs: [build]
|
| 72 |
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
| 73 |
+
steps:
|
| 74 |
+
- uses: actions/checkout@v4
|
| 75 |
+
|
| 76 |
+
- name: Set up Docker Buildx
|
| 77 |
+
uses: docker/setup-buildx-action@v3
|
| 78 |
+
|
| 79 |
+
- name: Login to GitHub Container Registry
|
| 80 |
+
uses: docker/login-action@v3
|
| 81 |
+
with:
|
| 82 |
+
registry: ghcr.io
|
| 83 |
+
username: ${{ github.actor }}
|
| 84 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 85 |
+
|
| 86 |
+
- name: Build and push agent-server
|
| 87 |
+
uses: docker/build-push-action@v5
|
| 88 |
+
with:
|
| 89 |
+
context: .
|
| 90 |
+
file: deployments/docker/Dockerfile.agent
|
| 91 |
+
push: true
|
| 92 |
+
tags: |
|
| 93 |
+
ghcr.io/${{ github.repository }}/agent-server:latest
|
| 94 |
+
ghcr.io/${{ github.repository }}/agent-server:${{ github.sha }}
|
| 95 |
+
cache-from: type=gha
|
| 96 |
+
cache-to: type=gha,mode=max
|
| 97 |
+
|
| 98 |
+
security-scan:
|
| 99 |
+
name: Security Scan
|
| 100 |
+
runs-on: ubuntu-latest
|
| 101 |
+
steps:
|
| 102 |
+
- uses: actions/checkout@v4
|
| 103 |
+
|
| 104 |
+
- name: Run Trivy vulnerability scanner
|
| 105 |
+
uses: aquasecurity/trivy-action@master
|
| 106 |
+
with:
|
| 107 |
+
scan-type: "fs"
|
| 108 |
+
scan-ref: "."
|
| 109 |
+
ignore-unfixed: true
|
| 110 |
+
format: "sarif"
|
| 111 |
+
output: "trivy-results.sarif"
|
| 112 |
+
|
| 113 |
+
- name: Upload Trivy scan results to GitHub Security tab
|
| 114 |
+
uses: github/codeql-action/upload-sarif@v2
|
| 115 |
+
if: always()
|
| 116 |
+
with:
|
| 117 |
+
sarif_file: "trivy-results.sarif"
|
.gitignore
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Binaries
|
| 2 |
+
bin/
|
| 3 |
+
*.exe
|
| 4 |
+
*.exe~
|
| 5 |
+
*.dll
|
| 6 |
+
*.so
|
| 7 |
+
*.dylib
|
| 8 |
+
|
| 9 |
+
# Test binary, built with `go test -c`
|
| 10 |
+
*.test
|
| 11 |
+
|
| 12 |
+
# Output of the go coverage tool
|
| 13 |
+
*.out
|
| 14 |
+
|
| 15 |
+
# Dependency directories
|
| 16 |
+
vendor/
|
| 17 |
+
|
| 18 |
+
# Go workspace sum file (keep go.work for local development)
|
| 19 |
+
go.work.sum
|
| 20 |
+
|
| 21 |
+
# IDE
|
| 22 |
+
.idea/
|
| 23 |
+
.vscode/
|
| 24 |
+
*.swp
|
| 25 |
+
*.swo
|
| 26 |
+
*~
|
| 27 |
+
|
| 28 |
+
# OS
|
| 29 |
+
.DS_Store
|
| 30 |
+
Thumbs.db
|
| 31 |
+
|
| 32 |
+
# Logs
|
| 33 |
+
*.log
|
| 34 |
+
logs/
|
| 35 |
+
|
| 36 |
+
# Environment files
|
| 37 |
+
.env
|
| 38 |
+
.env.local
|
| 39 |
+
.env.*.local
|
| 40 |
+
config.yaml
|
| 41 |
+
!config.example.yaml
|
| 42 |
+
|
| 43 |
+
# Temporary files
|
| 44 |
+
tmp/
|
| 45 |
+
temp/
|
| 46 |
+
|
| 47 |
+
# Build artifacts
|
| 48 |
+
dist/
|
| 49 |
+
|
| 50 |
+
# Docker
|
| 51 |
+
.docker/
|
| 52 |
+
|
| 53 |
+
# Kubernetes secrets (never commit)
|
| 54 |
+
*-secret.yaml
|
| 55 |
+
*-secrets.yaml
|
| 56 |
+
|
| 57 |
+
# Coverage reports
|
| 58 |
+
coverage/
|
| 59 |
+
coverage.html
|
| 60 |
+
coverage.txt
|
| 61 |
+
|
| 62 |
+
# Bleve index files
|
| 63 |
+
*.bleve/
|
| 64 |
+
amaniquery_index/
|
| 65 |
+
|
| 66 |
+
# Qdrant data (for local development)
|
| 67 |
+
qdrant_storage/
|
| 68 |
+
|
| 69 |
+
# Proto generated files (regenerate from source)
|
| 70 |
+
pkg/proto/gen/
|
.golangci.yml
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
run:
|
| 2 |
+
timeout: 5m
|
| 3 |
+
tests: true
|
| 4 |
+
|
| 5 |
+
linters:
|
| 6 |
+
enable:
|
| 7 |
+
- errcheck
|
| 8 |
+
- gosimple
|
| 9 |
+
- govet
|
| 10 |
+
- ineffassign
|
| 11 |
+
- staticcheck
|
| 12 |
+
- unused
|
| 13 |
+
- gofmt
|
| 14 |
+
- goimports
|
| 15 |
+
- revive
|
| 16 |
+
- gosec
|
| 17 |
+
- prealloc
|
| 18 |
+
- unconvert
|
| 19 |
+
- misspell
|
| 20 |
+
- goconst
|
| 21 |
+
- bodyclose
|
| 22 |
+
- noctx
|
| 23 |
+
- dupl
|
| 24 |
+
- nestif
|
| 25 |
+
|
| 26 |
+
linters-settings:
|
| 27 |
+
gofmt:
|
| 28 |
+
simplify: true
|
| 29 |
+
goimports:
|
| 30 |
+
local-prefixes: github.com/AmaniQuery/amaniquery
|
| 31 |
+
revive:
|
| 32 |
+
severity: warning
|
| 33 |
+
rules:
|
| 34 |
+
- name: blank-imports
|
| 35 |
+
- name: context-as-argument
|
| 36 |
+
- name: context-keys-type
|
| 37 |
+
- name: error-return
|
| 38 |
+
- name: error-strings
|
| 39 |
+
- name: error-naming
|
| 40 |
+
- name: exported
|
| 41 |
+
- name: increment-decrement
|
| 42 |
+
- name: var-naming
|
| 43 |
+
- name: package-comments
|
| 44 |
+
- name: range
|
| 45 |
+
- name: receiver-naming
|
| 46 |
+
- name: time-naming
|
| 47 |
+
- name: unexported-return
|
| 48 |
+
- name: indent-error-flow
|
| 49 |
+
gosec:
|
| 50 |
+
excludes:
|
| 51 |
+
- G104 # Unhandled error - too noisy for grpc
|
| 52 |
+
- G107 # Potential HTTP request made with variable url
|
| 53 |
+
nestif:
|
| 54 |
+
min-complexity: 5
|
| 55 |
+
goconst:
|
| 56 |
+
min-len: 3
|
| 57 |
+
min-occurrences: 3
|
| 58 |
+
dupl:
|
| 59 |
+
threshold: 150
|
| 60 |
+
|
| 61 |
+
issues:
|
| 62 |
+
exclude-use-default: false
|
| 63 |
+
max-issues-per-linter: 50
|
| 64 |
+
max-same-issues: 10
|
| 65 |
+
exclude-rules:
|
| 66 |
+
- path: _test\.go
|
| 67 |
+
linters:
|
| 68 |
+
- dupl
|
| 69 |
+
- gosec
|
| 70 |
+
- goconst
|
| 71 |
+
- path: cmd/
|
| 72 |
+
linters:
|
| 73 |
+
- gochecknoinits
|
| 74 |
+
- linters:
|
| 75 |
+
- staticcheck
|
| 76 |
+
text: "SA1019:" # Deprecated
|
Dockerfile
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Go Build Stage
|
| 2 |
+
FROM golang:1.24-alpine AS go-builder
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
RUN apk add --no-cache git ca-certificates
|
| 5 |
+
COPY go.mod go.sum ./
|
| 6 |
+
COPY go.work go.work.sum ./
|
| 7 |
+
RUN go mod download
|
| 8 |
+
COPY . .
|
| 9 |
+
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /agent-server ./cmd/agent-server
|
| 10 |
+
|
| 11 |
+
# Rust Build Stage
|
| 12 |
+
FROM rust:1.76-alpine AS rust-builder
|
| 13 |
+
WORKDIR /app
|
| 14 |
+
RUN apk add --no-cache musl-dev
|
| 15 |
+
COPY rust-memory-service/Cargo.toml rust-memory-service/Cargo.lock ./
|
| 16 |
+
# Dummy build to cache deps
|
| 17 |
+
RUN mkdir src && echo "fn main() {}" > src/main.rs
|
| 18 |
+
RUN cargo build --release
|
| 19 |
+
RUN rm -rf src
|
| 20 |
+
COPY rust-memory-service/ .
|
| 21 |
+
RUN cargo build --release
|
| 22 |
+
RUN cp target/release/memory-server /memory-server
|
| 23 |
+
|
| 24 |
+
# Runtime Stage
|
| 25 |
+
FROM alpine:3.19
|
| 26 |
+
RUN apk add --no-cache ca-certificates tzdata
|
| 27 |
+
RUN adduser -D -g '' appuser
|
| 28 |
+
WORKDIR /app
|
| 29 |
+
|
| 30 |
+
# Copy binaries
|
| 31 |
+
COPY --from=go-builder /agent-server .
|
| 32 |
+
COPY --from=rust-builder /memory-server .
|
| 33 |
+
|
| 34 |
+
# Copy scripts and configs
|
| 35 |
+
COPY deployments/huggingface/start.sh .
|
| 36 |
+
COPY config.example.yaml ./config.yaml
|
| 37 |
+
COPY rust-memory-service/.env.example .env
|
| 38 |
+
|
| 39 |
+
RUN chmod +x start.sh && chown -R appuser:appuser /app
|
| 40 |
+
|
| 41 |
+
USER appuser
|
| 42 |
+
EXPOSE 7860
|
| 43 |
+
|
| 44 |
+
# Environment variables
|
| 45 |
+
ENV PORT=7860
|
| 46 |
+
ENV MEMORY_BIND_ADDR=127.0.0.1:9091
|
| 47 |
+
# Configure Go agent to use local memory service (needs env var mapping in config/env)
|
| 48 |
+
ENV MEMORY_SERVICE_HOST=127.0.0.1
|
| 49 |
+
ENV MEMORY_SERVICE_PORT=9091
|
| 50 |
+
|
| 51 |
+
CMD ["./start.sh"]
|
Makefile
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: all build clean test proto run-agent run-retriever docker infra-up infra-down lint
|
| 2 |
+
|
| 3 |
+
# Variables
|
| 4 |
+
PROTO_DIR := pkg/proto
|
| 5 |
+
GO_OUT := pkg/proto/gen
|
| 6 |
+
DOCKER_COMPOSE := docker-compose -f deployments/docker/docker-compose.yml
|
| 7 |
+
|
| 8 |
+
# Default target
|
| 9 |
+
all: proto build
|
| 10 |
+
|
| 11 |
+
# Generate Go code from protobuf definitions
|
| 12 |
+
proto:
|
| 13 |
+
@echo "Generating protobuf code..."
|
| 14 |
+
@mkdir -p $(GO_OUT)
|
| 15 |
+
protoc --go_out=. --go_opt=paths=source_relative \
|
| 16 |
+
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
| 17 |
+
$(PROTO_DIR)/agent.proto $(PROTO_DIR)/generator.proto $(PROTO_DIR)/retriever.proto
|
| 18 |
+
protoc --go_out=. --go_opt=paths=source_relative \
|
| 19 |
+
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
| 20 |
+
$(PROTO_DIR)/clara.proto
|
| 21 |
+
|
| 22 |
+
# Build all services
|
| 23 |
+
build:
|
| 24 |
+
@echo "Building services..."
|
| 25 |
+
go build -o bin/agent-server ./cmd/agent-server
|
| 26 |
+
go build -o bin/retriever-server ./cmd/retriever-server
|
| 27 |
+
go build -o bin/generator-server ./cmd/generator-server
|
| 28 |
+
|
| 29 |
+
# Clean build artifacts
|
| 30 |
+
clean:
|
| 31 |
+
@echo "Cleaning..."
|
| 32 |
+
rm -rf bin/
|
| 33 |
+
rm -rf $(GO_OUT)
|
| 34 |
+
|
| 35 |
+
# Run tests
|
| 36 |
+
test:
|
| 37 |
+
@echo "Running tests..."
|
| 38 |
+
go test -v -race -cover ./...
|
| 39 |
+
|
| 40 |
+
# Run integration tests
|
| 41 |
+
test-integration:
|
| 42 |
+
@echo "Running integration tests..."
|
| 43 |
+
go test -v -tags=integration ./tests/...
|
| 44 |
+
|
| 45 |
+
# Run agent server
|
| 46 |
+
run-agent:
|
| 47 |
+
@echo "Starting agent server..."
|
| 48 |
+
go run ./cmd/agent-server
|
| 49 |
+
|
| 50 |
+
# Run retriever server
|
| 51 |
+
run-retriever:
|
| 52 |
+
@echo "Starting retriever server..."
|
| 53 |
+
go run ./cmd/retriever-server
|
| 54 |
+
|
| 55 |
+
# Run generator server
|
| 56 |
+
run-generator:
|
| 57 |
+
@echo "Starting generator server..."
|
| 58 |
+
go run ./cmd/generator-server
|
| 59 |
+
|
| 60 |
+
# Build Docker images
|
| 61 |
+
docker:
|
| 62 |
+
@echo "Building Docker images..."
|
| 63 |
+
docker build -t amaniquery/agent-server:latest -f deployments/docker/Dockerfile.agent .
|
| 64 |
+
docker build -t amaniquery/retriever-server:latest -f deployments/docker/Dockerfile.retriever .
|
| 65 |
+
docker build -t amaniquery/generator-server:latest -f deployments/docker/Dockerfile.generator .
|
| 66 |
+
|
| 67 |
+
# Start infrastructure (Qdrant, Redis, etc.)
|
| 68 |
+
infra-up:
|
| 69 |
+
@echo "Starting infrastructure..."
|
| 70 |
+
$(DOCKER_COMPOSE) up -d qdrant redis
|
| 71 |
+
|
| 72 |
+
# Stop infrastructure
|
| 73 |
+
infra-down:
|
| 74 |
+
@echo "Stopping infrastructure..."
|
| 75 |
+
$(DOCKER_COMPOSE) down
|
| 76 |
+
|
| 77 |
+
# Start all services with Docker Compose
|
| 78 |
+
up:
|
| 79 |
+
@echo "Starting all services..."
|
| 80 |
+
$(DOCKER_COMPOSE) up -d
|
| 81 |
+
|
| 82 |
+
# Stop all services
|
| 83 |
+
down:
|
| 84 |
+
@echo "Stopping all services..."
|
| 85 |
+
$(DOCKER_COMPOSE) down
|
| 86 |
+
|
| 87 |
+
# View logs
|
| 88 |
+
logs:
|
| 89 |
+
$(DOCKER_COMPOSE) logs -f
|
| 90 |
+
|
| 91 |
+
# Lint code
|
| 92 |
+
lint:
|
| 93 |
+
@echo "Linting..."
|
| 94 |
+
golangci-lint run ./...
|
| 95 |
+
|
| 96 |
+
# Format code
|
| 97 |
+
fmt:
|
| 98 |
+
@echo "Formatting..."
|
| 99 |
+
go fmt ./...
|
| 100 |
+
goimports -w .
|
| 101 |
+
|
| 102 |
+
# Download dependencies
|
| 103 |
+
deps:
|
| 104 |
+
@echo "Downloading dependencies..."
|
| 105 |
+
go mod download
|
| 106 |
+
go mod tidy
|
| 107 |
+
|
| 108 |
+
# Generate mocks for testing
|
| 109 |
+
mocks:
|
| 110 |
+
@echo "Generating mocks..."
|
| 111 |
+
mockgen -source=internal/agent/orchestrator.go -destination=internal/agent/mocks/orchestrator_mock.go
|
| 112 |
+
mockgen -source=internal/retriever/retriever.go -destination=internal/retriever/mocks/retriever_mock.go
|
| 113 |
+
|
| 114 |
+
# Security scan
|
| 115 |
+
security:
|
| 116 |
+
@echo "Running security scan..."
|
| 117 |
+
gosec ./...
|
| 118 |
+
trivy fs .
|
README.md
CHANGED
|
@@ -1,10 +1,467 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
# AmaniQuery
|
| 2 |
+
|
| 3 |
+
**AI-Powered Legal & News Intelligence Platform for Kenya**
|
| 4 |
+
|
| 5 |
+
[](https://go.dev/)
|
| 6 |
+
[](LICENSE)
|
| 7 |
+
[](https://github.com/amaniquery/amaniquery)
|
| 8 |
+
|
| 9 |
+
AmaniQuery is an AI agent framework designed to democratize access to legal information and civil education in Kenya. Built with a production-ready, Go-based RAG (Retrieval-Augmented Generation) architecture, it provides accurate, contextual answers about Kenya Law and current affairs.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## 🎯 Vision
|
| 14 |
+
|
| 15 |
+
**Democratizing Access to Information and Civil Education**
|
| 16 |
+
|
| 17 |
+
AmaniQuery aims to make legal knowledge accessible to every Kenyan citizen by:
|
| 18 |
+
- Providing accurate answers to legal questions in plain language
|
| 19 |
+
- Aggregating and contextualizing news relevant to civic matters
|
| 20 |
+
- Offering a reliable, fast, and secure platform for information retrieval
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## 📐 Architecture Overview
|
| 25 |
+
|
| 26 |
+
### High-Level System Architecture
|
| 27 |
+
|
| 28 |
+
```mermaid
|
| 29 |
+
graph TB
|
| 30 |
+
subgraph "Client Layer"
|
| 31 |
+
UI[Web UI/React]
|
| 32 |
+
API[REST/gRPC API]
|
| 33 |
+
CLI[Command Line]
|
| 34 |
+
end
|
| 35 |
+
|
| 36 |
+
subgraph "Control Plane"
|
| 37 |
+
AGENT[Agent Orchestrator<br/>Go-based Coordinator]
|
| 38 |
+
ROUTER[Query Router<br/>Semantic Classifier]
|
| 39 |
+
EVAL[Evaluation Engine<br/>Quality Scorer]
|
| 40 |
+
GUARDRAILS[NeMo Guardrails<br/>Safety Filter]
|
| 41 |
+
end
|
| 42 |
+
|
| 43 |
+
subgraph "Data Plane"
|
| 44 |
+
EMBED[Embedding Service<br/>GPU-Accelerated]
|
| 45 |
+
RETRIEVER[Retriever Service<br/>Hybrid Search]
|
| 46 |
+
RERANK[Reranker Service<br/>Cross-Encoder]
|
| 47 |
+
GENERATOR[Generator Service<br/>LLM Gateway]
|
| 48 |
+
end
|
| 49 |
+
|
| 50 |
+
subgraph "Storage Layer"
|
| 51 |
+
VDB[(Vector DB<br/>Qdrant)]
|
| 52 |
+
CACHE[(Redis Cache)]
|
| 53 |
+
OBJ[(Object Store<br/>MinIO/S3)]
|
| 54 |
+
GRAPH[(Graph DB<br/>Neo4j)]
|
| 55 |
+
end
|
| 56 |
+
|
| 57 |
+
subgraph "Infrastructure"
|
| 58 |
+
MONITOR[Monitoring<br/>Prometheus/Grafana]
|
| 59 |
+
TRACING[Tracing<br/>Jaeger]
|
| 60 |
+
VAULT[Secrets Vault<br/>HashiCorp Vault]
|
| 61 |
+
end
|
| 62 |
+
|
| 63 |
+
UI --> API
|
| 64 |
+
CLI --> API
|
| 65 |
+
API --> AGENT
|
| 66 |
+
AGENT --> ROUTER
|
| 67 |
+
ROUTER --> GUARDRAILS
|
| 68 |
+
GUARDRAILS --> EMBED
|
| 69 |
+
GUARDRAILS --> RETRIEVER
|
| 70 |
+
RETRIEVER --> VDB
|
| 71 |
+
RETRIEVER --> GRAPH
|
| 72 |
+
EMBED --> VDB
|
| 73 |
+
RETRIEVER --> RERANK
|
| 74 |
+
RERANK --> GENERATOR
|
| 75 |
+
GENERATOR --> EVAL
|
| 76 |
+
EVAL --> CACHE
|
| 77 |
+
CACHE --> API
|
| 78 |
+
AGENT --> MONITOR
|
| 79 |
+
AGENT --> TRACING
|
| 80 |
+
VAULT --> AGENT
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Query Router & Semantic Classification
|
| 84 |
+
|
| 85 |
+
```mermaid
|
| 86 |
+
graph LR
|
| 87 |
+
Q[User Query] --> PREPROCESS[Preprocessor<br/>Cleaning/Normalization]
|
| 88 |
+
PREPROCESS --> CLASSIFIER[Classifier<br/>Intent Detection]
|
| 89 |
+
CLASSIFIER --> C1{Query Type}
|
| 90 |
+
C1 -->|Factual| VSS[Vector Search]
|
| 91 |
+
C1 -->|Keyword| BM25[BM25 Search]
|
| 92 |
+
C1 -->|Relational| GRAPH[GraphRAG]
|
| 93 |
+
C1 -->|Multi-hop| AGENT[Agentic Search]
|
| 94 |
+
|
| 95 |
+
VSS --> HYBRID[Hybrid Results]
|
| 96 |
+
BM25 --> HYBRID
|
| 97 |
+
GRAPH --> HYBRID
|
| 98 |
+
AGENT --> HYBRID
|
| 99 |
+
HYBRID --> FUSION[Rank Fusion<br/>Reciprocal Rank]
|
| 100 |
+
FUSION --> OUTPUT[Ranked Chunks]
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
### Security Architecture
|
| 104 |
+
|
| 105 |
+
```mermaid
|
| 106 |
+
graph TB
|
| 107 |
+
subgraph "Perimeter Security"
|
| 108 |
+
WAF[Web App Firewall]
|
| 109 |
+
API_GATEWAY[API Gateway<br/>Kong/Envoy]
|
| 110 |
+
RATE_LIMIT[Rate Limiter<br/>Token Bucket]
|
| 111 |
+
end
|
| 112 |
+
|
| 113 |
+
subgraph "Authentication & Authorization"
|
| 114 |
+
OIDC[OIDC Provider<br/>Keycloak]
|
| 115 |
+
JWT[JWT Validator<br/>RS256]
|
| 116 |
+
OPA[OPA Policy Agent]
|
| 117 |
+
POLICY[Rego Policies]
|
| 118 |
+
end
|
| 119 |
+
|
| 120 |
+
subgraph "Data Security"
|
| 121 |
+
TLS[TLS 1.3<br/>mTLS Internal]
|
| 122 |
+
ENCRYPT[AES-256-GCM]
|
| 123 |
+
VAULT[(HashiCorp Vault)]
|
| 124 |
+
end
|
| 125 |
+
|
| 126 |
+
subgraph "Compliance"
|
| 127 |
+
AUDIT[Audit Logger]
|
| 128 |
+
GUARDRAILS2[Content Filter]
|
| 129 |
+
PII[PII Detector]
|
| 130 |
+
end
|
| 131 |
+
|
| 132 |
+
CLIENT[Client] --> WAF
|
| 133 |
+
WAF --> RATE_LIMIT
|
| 134 |
+
RATE_LIMIT --> API_GATEWAY
|
| 135 |
+
API_GATEWAY --> OIDC
|
| 136 |
+
OIDC --> JWT
|
| 137 |
+
JWT --> OPA
|
| 138 |
+
OPA --> POLICY
|
| 139 |
+
POLICY --> SERVICE[Core Services]
|
| 140 |
+
SERVICE --> TLS
|
| 141 |
+
SERVICE --> VAULT
|
| 142 |
+
SERVICE --> AUDIT
|
| 143 |
+
SERVICE --> GUARDRAILS2
|
| 144 |
+
SERVICE --> PII
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
### Synchronous Query Flow
|
| 148 |
+
|
| 149 |
+
```mermaid
|
| 150 |
+
sequenceDiagram
|
| 151 |
+
participant Client
|
| 152 |
+
participant API_Gateway
|
| 153 |
+
participant Agent
|
| 154 |
+
participant Router
|
| 155 |
+
participant Guardrails
|
| 156 |
+
participant Retriever
|
| 157 |
+
participant Generator
|
| 158 |
+
participant Cache
|
| 159 |
+
|
| 160 |
+
Client->>API_Gateway: POST /query
|
| 161 |
+
API_Gateway->>Agent: Forward query
|
| 162 |
+
|
| 163 |
+
Agent->>Cache: Check cache
|
| 164 |
+
alt Cache Hit
|
| 165 |
+
Cache-->>Agent: Cached result
|
| 166 |
+
Agent-->>Client: Response 200ms
|
| 167 |
+
else Cache Miss
|
| 168 |
+
Agent->>Router: RouteQuery()
|
| 169 |
+
Router-->>Agent: RoutingDecision
|
| 170 |
+
|
| 171 |
+
Agent->>Guardrails: ValidateQuery()
|
| 172 |
+
Guardrails-->>Agent: Safe/Unsafe
|
| 173 |
+
|
| 174 |
+
Agent->>Retriever: HybridSearch()
|
| 175 |
+
Retriever-->>Agent: Top-K chunks
|
| 176 |
+
|
| 177 |
+
Agent->>Generator: GenerateResponse()
|
| 178 |
+
Generator-->>Agent: Answer
|
| 179 |
+
|
| 180 |
+
Agent->>Cache: Store result
|
| 181 |
+
Agent-->>Client: Response 2-3s
|
| 182 |
+
end
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
### Agentic Multi-Step Flow
|
| 186 |
+
|
| 187 |
+
```mermaid
|
| 188 |
+
sequenceDiagram
|
| 189 |
+
participant Client
|
| 190 |
+
participant Agent
|
| 191 |
+
participant Planner
|
| 192 |
+
participant Tools
|
| 193 |
+
participant Evaluator
|
| 194 |
+
|
| 195 |
+
Client->>Agent: Complex query
|
| 196 |
+
Agent->>Planner: Create plan
|
| 197 |
+
Planner-->>Agent: Multi-step plan
|
| 198 |
+
|
| 199 |
+
loop For each step
|
| 200 |
+
Agent->>Tools: Execute tool
|
| 201 |
+
Tools-->>Agent: Result
|
| 202 |
+
Agent->>Planner: Update plan
|
| 203 |
+
end
|
| 204 |
+
|
| 205 |
+
Agent->>Evaluator: Validate answer
|
| 206 |
+
Evaluator-->>Agent: Score
|
| 207 |
+
|
| 208 |
+
alt Score >= threshold
|
| 209 |
+
Agent-->>Client: Final answer
|
| 210 |
+
else Score < threshold
|
| 211 |
+
Agent->>Planner: Revise plan
|
| 212 |
+
end
|
| 213 |
+
```
|
| 214 |
+
|
| 215 |
+
### Data Ingestion Pipeline
|
| 216 |
+
|
| 217 |
+
```mermaid
|
| 218 |
+
graph TB
|
| 219 |
+
SRC[Data Sources<br/>PDF/DB/API] --> INGEST[Ingestion API]
|
| 220 |
+
|
| 221 |
+
INGEST --> QUEUE1[Message Queue<br/>Kafka/RabbitMQ]
|
| 222 |
+
|
| 223 |
+
QUEUE1 --> PREPROC[Preprocessor<br/>Go Workers]
|
| 224 |
+
|
| 225 |
+
PREPROC --> CHUNK[Chunking Engine<br/>Semantic Splitter]
|
| 226 |
+
|
| 227 |
+
CHUNK --> ENRICH[Enrichment<br/>Metadata/NER]
|
| 228 |
+
|
| 229 |
+
ENRICH --> EMBED2[Embedding Worker<br/>Batched GPU]
|
| 230 |
+
|
| 231 |
+
EMBED2 --> INDEX[Indexing Worker]
|
| 232 |
+
|
| 233 |
+
INDEX --> VDB2[(Vector DB)]
|
| 234 |
+
INDEX --> GRAPH2[(Graph DB)]
|
| 235 |
+
INDEX --> CACHE3[(Cache)]
|
| 236 |
+
|
| 237 |
+
style PREPROC fill:#4A90D9
|
| 238 |
+
style CHUNK fill:#50C878
|
| 239 |
+
style EMBED2 fill:#FF6B6B
|
| 240 |
+
style INDEX fill:#DA70D6
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
### Multi-Tier Caching Strategy
|
| 244 |
+
|
| 245 |
+
```mermaid
|
| 246 |
+
graph LR
|
| 247 |
+
CLIENT[Query] --> CACHE1[L1 Cache<br/>CDN/Edge]
|
| 248 |
+
|
| 249 |
+
CACHE1 -->|Miss| CACHE2[L2 Cache<br/>Redis Cluster]
|
| 250 |
+
|
| 251 |
+
CACHE2 -->|Miss| CACHE3[L3 Compute Cache]
|
| 252 |
+
|
| 253 |
+
CACHE3 -->|Miss| EMBED[Embedding Cache<br/>Local LRU]
|
| 254 |
+
|
| 255 |
+
EMBED -->|Miss| MODEL[Embedding Model]
|
| 256 |
+
|
| 257 |
+
style CACHE1 fill:#FF69B4
|
| 258 |
+
style CACHE2 fill:#6495ED
|
| 259 |
+
style CACHE3 fill:#90EE90
|
| 260 |
+
style EMBED fill:#FFB6C1
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
---
|
| 264 |
+
|
| 265 |
+
## 🗂️ Project Structure
|
| 266 |
+
|
| 267 |
+
```
|
| 268 |
+
AmaniQuery/
|
| 269 |
+
├── cmd/ # Application entry points
|
| 270 |
+
│ ├── agent-server/ # Agent Orchestrator service
|
| 271 |
+
│ ├── retriever-server/ # Retrieval service
|
| 272 |
+
│ └── generator-server/ # LLM Gateway service
|
| 273 |
+
├── internal/ # Private application code
|
| 274 |
+
│ ├── agent/ # Agent orchestration logic
|
| 275 |
+
│ ├── retriever/ # Hybrid search implementation
|
| 276 |
+
│ │ ├── vector/ # Vector store clients
|
| 277 |
+
│ │ ├── keyword/ # BM25/Bleve implementation
|
| 278 |
+
│ │ └── graph/ # Neo4j GraphRAG
|
| 279 |
+
│ ├── generator/ # LLM client and prompting
|
| 280 |
+
│ ├── router/ # Query classification
|
| 281 |
+
│ ├── guardrails/ # Content safety filters
|
| 282 |
+
│ ├── cache/ # Multi-tier caching
|
| 283 |
+
│ └── security/ # Auth, encryption, policies
|
| 284 |
+
├── pkg/ # Public shared libraries
|
| 285 |
+
│ ├── proto/ # gRPC/protobuf definitions
|
| 286 |
+
│ ├── config/ # Configuration management
|
| 287 |
+
│ ├── observability/ # Metrics, tracing, logging
|
| 288 |
+
│ └── errors/ # Custom error types
|
| 289 |
+
├── api/ # REST API layer
|
| 290 |
+
├── deployments/ # Kubernetes/Helm configs
|
| 291 |
+
│ ├── docker/ # Dockerfiles
|
| 292 |
+
│ └── k8s/ # Kubernetes manifests
|
| 293 |
+
├── scripts/ # Build and deployment scripts
|
| 294 |
+
├── docs/ # Documentation
|
| 295 |
+
└── tests/ # Integration tests
|
| 296 |
+
```
|
| 297 |
+
|
| 298 |
+
---
|
| 299 |
+
|
| 300 |
+
## 🚀 Quick Start
|
| 301 |
+
|
| 302 |
+
### Prerequisites
|
| 303 |
+
|
| 304 |
+
- Go 1.21+
|
| 305 |
+
- Docker & Docker Compose
|
| 306 |
+
- Make
|
| 307 |
+
|
| 308 |
+
### Local Development
|
| 309 |
+
|
| 310 |
+
```bash
|
| 311 |
+
# Clone the repository
|
| 312 |
+
git clone https://github.com/amaniquery/amaniquery.git
|
| 313 |
+
cd amaniquery
|
| 314 |
+
|
| 315 |
+
# Install dependencies
|
| 316 |
+
go mod download
|
| 317 |
+
|
| 318 |
+
# Generate protobuf code
|
| 319 |
+
make proto
|
| 320 |
+
|
| 321 |
+
# Start infrastructure (Qdrant, Redis)
|
| 322 |
+
make infra-up
|
| 323 |
+
|
| 324 |
+
# Run the agent server
|
| 325 |
+
make run-agent
|
| 326 |
+
|
| 327 |
+
# Run tests
|
| 328 |
+
make test
|
| 329 |
+
```
|
| 330 |
+
|
| 331 |
+
### Docker Compose
|
| 332 |
+
|
| 333 |
+
```bash
|
| 334 |
+
# Start all services
|
| 335 |
+
docker-compose up -d
|
| 336 |
+
|
| 337 |
+
# View logs
|
| 338 |
+
docker-compose logs -f agent-server
|
| 339 |
+
|
| 340 |
+
# Stop services
|
| 341 |
+
docker-compose down
|
| 342 |
+
```
|
| 343 |
+
|
| 344 |
---
|
| 345 |
+
|
| 346 |
+
## 📡 API Usage
|
| 347 |
+
|
| 348 |
+
### gRPC
|
| 349 |
+
|
| 350 |
+
```bash
|
| 351 |
+
# List available services
|
| 352 |
+
grpcurl -plaintext localhost:9090 list
|
| 353 |
+
|
| 354 |
+
# Process a query
|
| 355 |
+
grpcurl -plaintext -d '{
|
| 356 |
+
"query": "What does the Kenya Constitution say about land rights?",
|
| 357 |
+
"session_id": "user-123"
|
| 358 |
+
}' localhost:9090 rag.v1.AgentService/ProcessQuery
|
| 359 |
+
```
|
| 360 |
+
|
| 361 |
+
### REST API
|
| 362 |
+
|
| 363 |
+
```bash
|
| 364 |
+
# Health check
|
| 365 |
+
curl http://localhost:8080/health
|
| 366 |
+
|
| 367 |
+
# Process query
|
| 368 |
+
curl -X POST http://localhost:8080/api/v1/query \
|
| 369 |
+
-H "Content-Type: application/json" \
|
| 370 |
+
-d '{"query": "Explain the Bill of Rights in Kenya"}'
|
| 371 |
+
```
|
| 372 |
+
|
| 373 |
+
---
|
| 374 |
+
|
| 375 |
+
## ⚙️ Configuration
|
| 376 |
+
|
| 377 |
+
Configuration is managed through environment variables and YAML files:
|
| 378 |
+
|
| 379 |
+
```yaml
|
| 380 |
+
# config.yaml
|
| 381 |
+
server:
|
| 382 |
+
grpc_port: 9090
|
| 383 |
+
http_port: 8080
|
| 384 |
+
|
| 385 |
+
vector_store:
|
| 386 |
+
type: qdrant
|
| 387 |
+
host: localhost
|
| 388 |
+
port: 6333
|
| 389 |
+
collection: amaniquery
|
| 390 |
+
|
| 391 |
+
llm:
|
| 392 |
+
provider: openai # openai, anthropic, google
|
| 393 |
+
model: gpt-4o
|
| 394 |
+
max_tokens: 4096
|
| 395 |
+
temperature: 0.7
|
| 396 |
+
|
| 397 |
+
cache:
|
| 398 |
+
redis_url: redis://localhost:6379
|
| 399 |
+
local_size: 10000
|
| 400 |
+
ttl: 3600
|
| 401 |
+
|
| 402 |
+
security:
|
| 403 |
+
jwt_secret: ${JWT_SECRET}
|
| 404 |
+
vault_addr: ${VAULT_ADDR}
|
| 405 |
+
```
|
| 406 |
+
|
| 407 |
+
---
|
| 408 |
+
|
| 409 |
+
## 📊 Performance Targets
|
| 410 |
+
|
| 411 |
+
| Metric | Target | Description |
|
| 412 |
+
|--------|--------|-------------|
|
| 413 |
+
| P95 Latency | < 3s | 95th percentile query latency |
|
| 414 |
+
| P99 Latency | < 5s | 99th percentile query latency |
|
| 415 |
+
| Cache Hit Rate | > 85% | Query cache effectiveness |
|
| 416 |
+
| Throughput | 10k docs/min | Document ingestion rate |
|
| 417 |
+
| Concurrent Users | 10,000+ | Simultaneous connections |
|
| 418 |
+
| Availability | 99.95% | Uptime SLA |
|
| 419 |
+
|
| 420 |
+
---
|
| 421 |
+
|
| 422 |
+
## 🔒 Security
|
| 423 |
+
|
| 424 |
+
AmaniQuery implements enterprise-grade security:
|
| 425 |
+
|
| 426 |
+
- **mTLS**: Service-to-service encryption
|
| 427 |
+
- **JWT + OIDC**: Token-based authentication
|
| 428 |
+
- **OPA/Rego**: Fine-grained authorization policies
|
| 429 |
+
- **HashiCorp Vault**: Secrets management
|
| 430 |
+
- **PII Detection**: Automatic sensitive data handling
|
| 431 |
+
- **Audit Logging**: Immutable activity logs
|
| 432 |
+
- **Rate Limiting**: DDoS protection
|
| 433 |
+
|
| 434 |
+
---
|
| 435 |
+
|
| 436 |
+
## 📈 Observability
|
| 437 |
+
|
| 438 |
+
- **Metrics**: Prometheus + Grafana dashboards
|
| 439 |
+
- **Tracing**: Jaeger distributed tracing
|
| 440 |
+
- **Logging**: Structured JSON logs with correlation IDs
|
| 441 |
+
- **Alerting**: PagerDuty/Slack integration
|
| 442 |
+
|
| 443 |
+
---
|
| 444 |
+
|
| 445 |
+
## 🤝 Contributing
|
| 446 |
+
|
| 447 |
+
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
|
| 448 |
+
|
| 449 |
+
---
|
| 450 |
+
|
| 451 |
+
## 📄 License
|
| 452 |
+
|
| 453 |
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
| 454 |
+
|
| 455 |
+
---
|
| 456 |
+
|
| 457 |
+
## 🙏 Acknowledgments
|
| 458 |
+
|
| 459 |
+
- Built with inspiration from NVIDIA's RAG Blueprint
|
| 460 |
+
- Kenya Law Reports for legal data access
|
| 461 |
+
- The Go community for excellent tooling
|
| 462 |
+
|
| 463 |
---
|
| 464 |
|
| 465 |
+
<p align="center">
|
| 466 |
+
<b>AmaniQuery</b> - Democratizing Access to Legal Knowledge
|
| 467 |
+
</p>
|
agent.pb.go
ADDED
|
@@ -0,0 +1,1837 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Code generated by protoc-gen-go. DO NOT EDIT.
|
| 2 |
+
// versions:
|
| 3 |
+
// protoc-gen-go v1.36.11
|
| 4 |
+
// protoc v6.33.2
|
| 5 |
+
// source: agent.proto
|
| 6 |
+
|
| 7 |
+
package ragv1
|
| 8 |
+
|
| 9 |
+
import (
|
| 10 |
+
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
| 11 |
+
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
| 12 |
+
reflect "reflect"
|
| 13 |
+
sync "sync"
|
| 14 |
+
unsafe "unsafe"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
const (
|
| 18 |
+
// Verify that this generated code is sufficiently up-to-date.
|
| 19 |
+
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
| 20 |
+
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
| 21 |
+
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
// ChunkType defines the type of streaming chunk
|
| 25 |
+
type ChunkType int32
|
| 26 |
+
|
| 27 |
+
const (
|
| 28 |
+
ChunkType_CHUNK_TYPE_UNSPECIFIED ChunkType = 0
|
| 29 |
+
ChunkType_CHUNK_TYPE_THINKING ChunkType = 1
|
| 30 |
+
ChunkType_CHUNK_TYPE_RETRIEVAL ChunkType = 2
|
| 31 |
+
ChunkType_CHUNK_TYPE_GENERATION ChunkType = 3
|
| 32 |
+
ChunkType_CHUNK_TYPE_COMPLETE ChunkType = 4
|
| 33 |
+
ChunkType_CHUNK_TYPE_ERROR ChunkType = 5
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
// Enum value maps for ChunkType.
|
| 37 |
+
var (
|
| 38 |
+
ChunkType_name = map[int32]string{
|
| 39 |
+
0: "CHUNK_TYPE_UNSPECIFIED",
|
| 40 |
+
1: "CHUNK_TYPE_THINKING",
|
| 41 |
+
2: "CHUNK_TYPE_RETRIEVAL",
|
| 42 |
+
3: "CHUNK_TYPE_GENERATION",
|
| 43 |
+
4: "CHUNK_TYPE_COMPLETE",
|
| 44 |
+
5: "CHUNK_TYPE_ERROR",
|
| 45 |
+
}
|
| 46 |
+
ChunkType_value = map[string]int32{
|
| 47 |
+
"CHUNK_TYPE_UNSPECIFIED": 0,
|
| 48 |
+
"CHUNK_TYPE_THINKING": 1,
|
| 49 |
+
"CHUNK_TYPE_RETRIEVAL": 2,
|
| 50 |
+
"CHUNK_TYPE_GENERATION": 3,
|
| 51 |
+
"CHUNK_TYPE_COMPLETE": 4,
|
| 52 |
+
"CHUNK_TYPE_ERROR": 5,
|
| 53 |
+
}
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
func (x ChunkType) Enum() *ChunkType {
|
| 57 |
+
p := new(ChunkType)
|
| 58 |
+
*p = x
|
| 59 |
+
return p
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
func (x ChunkType) String() string {
|
| 63 |
+
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
func (ChunkType) Descriptor() protoreflect.EnumDescriptor {
|
| 67 |
+
return file_agent_proto_enumTypes[0].Descriptor()
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
func (ChunkType) Type() protoreflect.EnumType {
|
| 71 |
+
return &file_agent_proto_enumTypes[0]
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
func (x ChunkType) Number() protoreflect.EnumNumber {
|
| 75 |
+
return protoreflect.EnumNumber(x)
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// Deprecated: Use ChunkType.Descriptor instead.
|
| 79 |
+
func (ChunkType) EnumDescriptor() ([]byte, []int) {
|
| 80 |
+
return file_agent_proto_rawDescGZIP(), []int{0}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// MessageRole defines who sent the message
|
| 84 |
+
type MessageRole int32
|
| 85 |
+
|
| 86 |
+
const (
|
| 87 |
+
MessageRole_MESSAGE_ROLE_UNSPECIFIED MessageRole = 0
|
| 88 |
+
MessageRole_MESSAGE_ROLE_USER MessageRole = 1
|
| 89 |
+
MessageRole_MESSAGE_ROLE_ASSISTANT MessageRole = 2
|
| 90 |
+
MessageRole_MESSAGE_ROLE_SYSTEM MessageRole = 3
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
// Enum value maps for MessageRole.
|
| 94 |
+
var (
|
| 95 |
+
MessageRole_name = map[int32]string{
|
| 96 |
+
0: "MESSAGE_ROLE_UNSPECIFIED",
|
| 97 |
+
1: "MESSAGE_ROLE_USER",
|
| 98 |
+
2: "MESSAGE_ROLE_ASSISTANT",
|
| 99 |
+
3: "MESSAGE_ROLE_SYSTEM",
|
| 100 |
+
}
|
| 101 |
+
MessageRole_value = map[string]int32{
|
| 102 |
+
"MESSAGE_ROLE_UNSPECIFIED": 0,
|
| 103 |
+
"MESSAGE_ROLE_USER": 1,
|
| 104 |
+
"MESSAGE_ROLE_ASSISTANT": 2,
|
| 105 |
+
"MESSAGE_ROLE_SYSTEM": 3,
|
| 106 |
+
}
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
func (x MessageRole) Enum() *MessageRole {
|
| 110 |
+
p := new(MessageRole)
|
| 111 |
+
*p = x
|
| 112 |
+
return p
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
func (x MessageRole) String() string {
|
| 116 |
+
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
func (MessageRole) Descriptor() protoreflect.EnumDescriptor {
|
| 120 |
+
return file_agent_proto_enumTypes[1].Descriptor()
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
func (MessageRole) Type() protoreflect.EnumType {
|
| 124 |
+
return &file_agent_proto_enumTypes[1]
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
func (x MessageRole) Number() protoreflect.EnumNumber {
|
| 128 |
+
return protoreflect.EnumNumber(x)
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Deprecated: Use MessageRole.Descriptor instead.
|
| 132 |
+
func (MessageRole) EnumDescriptor() ([]byte, []int) {
|
| 133 |
+
return file_agent_proto_rawDescGZIP(), []int{1}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// StepType defines execution step types
|
| 137 |
+
type StepType int32
|
| 138 |
+
|
| 139 |
+
const (
|
| 140 |
+
StepType_STEP_TYPE_UNSPECIFIED StepType = 0
|
| 141 |
+
StepType_STEP_TYPE_SEARCH StepType = 1
|
| 142 |
+
StepType_STEP_TYPE_ANALYZE StepType = 2
|
| 143 |
+
StepType_STEP_TYPE_SYNTHESIZE StepType = 3
|
| 144 |
+
StepType_STEP_TYPE_VALIDATE StepType = 4
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
// Enum value maps for StepType.
|
| 148 |
+
var (
|
| 149 |
+
StepType_name = map[int32]string{
|
| 150 |
+
0: "STEP_TYPE_UNSPECIFIED",
|
| 151 |
+
1: "STEP_TYPE_SEARCH",
|
| 152 |
+
2: "STEP_TYPE_ANALYZE",
|
| 153 |
+
3: "STEP_TYPE_SYNTHESIZE",
|
| 154 |
+
4: "STEP_TYPE_VALIDATE",
|
| 155 |
+
}
|
| 156 |
+
StepType_value = map[string]int32{
|
| 157 |
+
"STEP_TYPE_UNSPECIFIED": 0,
|
| 158 |
+
"STEP_TYPE_SEARCH": 1,
|
| 159 |
+
"STEP_TYPE_ANALYZE": 2,
|
| 160 |
+
"STEP_TYPE_SYNTHESIZE": 3,
|
| 161 |
+
"STEP_TYPE_VALIDATE": 4,
|
| 162 |
+
}
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
func (x StepType) Enum() *StepType {
|
| 166 |
+
p := new(StepType)
|
| 167 |
+
*p = x
|
| 168 |
+
return p
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
func (x StepType) String() string {
|
| 172 |
+
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
func (StepType) Descriptor() protoreflect.EnumDescriptor {
|
| 176 |
+
return file_agent_proto_enumTypes[2].Descriptor()
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
func (StepType) Type() protoreflect.EnumType {
|
| 180 |
+
return &file_agent_proto_enumTypes[2]
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
func (x StepType) Number() protoreflect.EnumNumber {
|
| 184 |
+
return protoreflect.EnumNumber(x)
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// Deprecated: Use StepType.Descriptor instead.
|
| 188 |
+
func (StepType) EnumDescriptor() ([]byte, []int) {
|
| 189 |
+
return file_agent_proto_rawDescGZIP(), []int{2}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// ExecutionStatus for plan execution
|
| 193 |
+
type ExecutionStatus int32
|
| 194 |
+
|
| 195 |
+
const (
|
| 196 |
+
ExecutionStatus_EXECUTION_STATUS_UNSPECIFIED ExecutionStatus = 0
|
| 197 |
+
ExecutionStatus_EXECUTION_STATUS_PENDING ExecutionStatus = 1
|
| 198 |
+
ExecutionStatus_EXECUTION_STATUS_RUNNING ExecutionStatus = 2
|
| 199 |
+
ExecutionStatus_EXECUTION_STATUS_COMPLETED ExecutionStatus = 3
|
| 200 |
+
ExecutionStatus_EXECUTION_STATUS_FAILED ExecutionStatus = 4
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
// Enum value maps for ExecutionStatus.
|
| 204 |
+
var (
|
| 205 |
+
ExecutionStatus_name = map[int32]string{
|
| 206 |
+
0: "EXECUTION_STATUS_UNSPECIFIED",
|
| 207 |
+
1: "EXECUTION_STATUS_PENDING",
|
| 208 |
+
2: "EXECUTION_STATUS_RUNNING",
|
| 209 |
+
3: "EXECUTION_STATUS_COMPLETED",
|
| 210 |
+
4: "EXECUTION_STATUS_FAILED",
|
| 211 |
+
}
|
| 212 |
+
ExecutionStatus_value = map[string]int32{
|
| 213 |
+
"EXECUTION_STATUS_UNSPECIFIED": 0,
|
| 214 |
+
"EXECUTION_STATUS_PENDING": 1,
|
| 215 |
+
"EXECUTION_STATUS_RUNNING": 2,
|
| 216 |
+
"EXECUTION_STATUS_COMPLETED": 3,
|
| 217 |
+
"EXECUTION_STATUS_FAILED": 4,
|
| 218 |
+
}
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
func (x ExecutionStatus) Enum() *ExecutionStatus {
|
| 222 |
+
p := new(ExecutionStatus)
|
| 223 |
+
*p = x
|
| 224 |
+
return p
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
func (x ExecutionStatus) String() string {
|
| 228 |
+
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
func (ExecutionStatus) Descriptor() protoreflect.EnumDescriptor {
|
| 232 |
+
return file_agent_proto_enumTypes[3].Descriptor()
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
func (ExecutionStatus) Type() protoreflect.EnumType {
|
| 236 |
+
return &file_agent_proto_enumTypes[3]
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
func (x ExecutionStatus) Number() protoreflect.EnumNumber {
|
| 240 |
+
return protoreflect.EnumNumber(x)
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
// Deprecated: Use ExecutionStatus.Descriptor instead.
|
| 244 |
+
func (ExecutionStatus) EnumDescriptor() ([]byte, []int) {
|
| 245 |
+
return file_agent_proto_rawDescGZIP(), []int{3}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
// QueryRequest represents a user query
|
| 249 |
+
type QueryRequest struct {
|
| 250 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 251 |
+
// The user's query text
|
| 252 |
+
Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"`
|
| 253 |
+
// Session ID for conversation continuity
|
| 254 |
+
SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
|
| 255 |
+
// User ID for personalization and audit
|
| 256 |
+
UserId string `protobuf:"bytes,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
| 257 |
+
// Conversation history for context
|
| 258 |
+
ConversationHistory []*Message `protobuf:"bytes,4,rep,name=conversation_history,json=conversationHistory,proto3" json:"conversation_history,omitempty"`
|
| 259 |
+
// Additional metadata
|
| 260 |
+
Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
| 261 |
+
// Query configuration options
|
| 262 |
+
Options *QueryOptions `protobuf:"bytes,6,opt,name=options,proto3" json:"options,omitempty"`
|
| 263 |
+
unknownFields protoimpl.UnknownFields
|
| 264 |
+
sizeCache protoimpl.SizeCache
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
func (x *QueryRequest) Reset() {
|
| 268 |
+
*x = QueryRequest{}
|
| 269 |
+
mi := &file_agent_proto_msgTypes[0]
|
| 270 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 271 |
+
ms.StoreMessageInfo(mi)
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
func (x *QueryRequest) String() string {
|
| 275 |
+
return protoimpl.X.MessageStringOf(x)
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
func (*QueryRequest) ProtoMessage() {}
|
| 279 |
+
|
| 280 |
+
func (x *QueryRequest) ProtoReflect() protoreflect.Message {
|
| 281 |
+
mi := &file_agent_proto_msgTypes[0]
|
| 282 |
+
if x != nil {
|
| 283 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 284 |
+
if ms.LoadMessageInfo() == nil {
|
| 285 |
+
ms.StoreMessageInfo(mi)
|
| 286 |
+
}
|
| 287 |
+
return ms
|
| 288 |
+
}
|
| 289 |
+
return mi.MessageOf(x)
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead.
|
| 293 |
+
func (*QueryRequest) Descriptor() ([]byte, []int) {
|
| 294 |
+
return file_agent_proto_rawDescGZIP(), []int{0}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
func (x *QueryRequest) GetQuery() string {
|
| 298 |
+
if x != nil {
|
| 299 |
+
return x.Query
|
| 300 |
+
}
|
| 301 |
+
return ""
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
func (x *QueryRequest) GetSessionId() string {
|
| 305 |
+
if x != nil {
|
| 306 |
+
return x.SessionId
|
| 307 |
+
}
|
| 308 |
+
return ""
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
func (x *QueryRequest) GetUserId() string {
|
| 312 |
+
if x != nil {
|
| 313 |
+
return x.UserId
|
| 314 |
+
}
|
| 315 |
+
return ""
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
func (x *QueryRequest) GetConversationHistory() []*Message {
|
| 319 |
+
if x != nil {
|
| 320 |
+
return x.ConversationHistory
|
| 321 |
+
}
|
| 322 |
+
return nil
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
func (x *QueryRequest) GetMetadata() map[string]string {
|
| 326 |
+
if x != nil {
|
| 327 |
+
return x.Metadata
|
| 328 |
+
}
|
| 329 |
+
return nil
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
func (x *QueryRequest) GetOptions() *QueryOptions {
|
| 333 |
+
if x != nil {
|
| 334 |
+
return x.Options
|
| 335 |
+
}
|
| 336 |
+
return nil
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
// QueryOptions configures query processing behavior
|
| 340 |
+
type QueryOptions struct {
|
| 341 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 342 |
+
// Maximum number of sources to retrieve
|
| 343 |
+
MaxSources int32 `protobuf:"varint,1,opt,name=max_sources,json=maxSources,proto3" json:"max_sources,omitempty"`
|
| 344 |
+
// Enable/disable caching
|
| 345 |
+
UseCache bool `protobuf:"varint,2,opt,name=use_cache,json=useCache,proto3" json:"use_cache,omitempty"`
|
| 346 |
+
// Enable agentic multi-step reasoning
|
| 347 |
+
EnableAgentic bool `protobuf:"varint,3,opt,name=enable_agentic,json=enableAgentic,proto3" json:"enable_agentic,omitempty"`
|
| 348 |
+
// Specific knowledge bases to search
|
| 349 |
+
KnowledgeBases []string `protobuf:"bytes,4,rep,name=knowledge_bases,json=knowledgeBases,proto3" json:"knowledge_bases,omitempty"`
|
| 350 |
+
// Temperature for LLM generation (0.0 - 1.0)
|
| 351 |
+
Temperature float32 `protobuf:"fixed32,5,opt,name=temperature,proto3" json:"temperature,omitempty"`
|
| 352 |
+
// Maximum tokens for response
|
| 353 |
+
MaxTokens int32 `protobuf:"varint,6,opt,name=max_tokens,json=maxTokens,proto3" json:"max_tokens,omitempty"`
|
| 354 |
+
unknownFields protoimpl.UnknownFields
|
| 355 |
+
sizeCache protoimpl.SizeCache
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
func (x *QueryOptions) Reset() {
|
| 359 |
+
*x = QueryOptions{}
|
| 360 |
+
mi := &file_agent_proto_msgTypes[1]
|
| 361 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 362 |
+
ms.StoreMessageInfo(mi)
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
func (x *QueryOptions) String() string {
|
| 366 |
+
return protoimpl.X.MessageStringOf(x)
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
func (*QueryOptions) ProtoMessage() {}
|
| 370 |
+
|
| 371 |
+
func (x *QueryOptions) ProtoReflect() protoreflect.Message {
|
| 372 |
+
mi := &file_agent_proto_msgTypes[1]
|
| 373 |
+
if x != nil {
|
| 374 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 375 |
+
if ms.LoadMessageInfo() == nil {
|
| 376 |
+
ms.StoreMessageInfo(mi)
|
| 377 |
+
}
|
| 378 |
+
return ms
|
| 379 |
+
}
|
| 380 |
+
return mi.MessageOf(x)
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// Deprecated: Use QueryOptions.ProtoReflect.Descriptor instead.
|
| 384 |
+
func (*QueryOptions) Descriptor() ([]byte, []int) {
|
| 385 |
+
return file_agent_proto_rawDescGZIP(), []int{1}
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
func (x *QueryOptions) GetMaxSources() int32 {
|
| 389 |
+
if x != nil {
|
| 390 |
+
return x.MaxSources
|
| 391 |
+
}
|
| 392 |
+
return 0
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
func (x *QueryOptions) GetUseCache() bool {
|
| 396 |
+
if x != nil {
|
| 397 |
+
return x.UseCache
|
| 398 |
+
}
|
| 399 |
+
return false
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
func (x *QueryOptions) GetEnableAgentic() bool {
|
| 403 |
+
if x != nil {
|
| 404 |
+
return x.EnableAgentic
|
| 405 |
+
}
|
| 406 |
+
return false
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
func (x *QueryOptions) GetKnowledgeBases() []string {
|
| 410 |
+
if x != nil {
|
| 411 |
+
return x.KnowledgeBases
|
| 412 |
+
}
|
| 413 |
+
return nil
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
func (x *QueryOptions) GetTemperature() float32 {
|
| 417 |
+
if x != nil {
|
| 418 |
+
return x.Temperature
|
| 419 |
+
}
|
| 420 |
+
return 0
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
func (x *QueryOptions) GetMaxTokens() int32 {
|
| 424 |
+
if x != nil {
|
| 425 |
+
return x.MaxTokens
|
| 426 |
+
}
|
| 427 |
+
return 0
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
// QueryResponse contains the complete answer
|
| 431 |
+
type QueryResponse struct {
|
| 432 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 433 |
+
// The generated answer text
|
| 434 |
+
Answer string `protobuf:"bytes,1,opt,name=answer,proto3" json:"answer,omitempty"`
|
| 435 |
+
// Sources used to generate the answer
|
| 436 |
+
Sources []*Source `protobuf:"bytes,2,rep,name=sources,proto3" json:"sources,omitempty"`
|
| 437 |
+
// Confidence score (0.0 - 1.0)
|
| 438 |
+
Confidence float32 `protobuf:"fixed32,3,opt,name=confidence,proto3" json:"confidence,omitempty"`
|
| 439 |
+
// Query processing metadata
|
| 440 |
+
Metadata *QueryMetadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
| 441 |
+
// Suggested follow-up questions
|
| 442 |
+
FollowUpQuestions []string `protobuf:"bytes,5,rep,name=follow_up_questions,json=followUpQuestions,proto3" json:"follow_up_questions,omitempty"`
|
| 443 |
+
unknownFields protoimpl.UnknownFields
|
| 444 |
+
sizeCache protoimpl.SizeCache
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
func (x *QueryResponse) Reset() {
|
| 448 |
+
*x = QueryResponse{}
|
| 449 |
+
mi := &file_agent_proto_msgTypes[2]
|
| 450 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 451 |
+
ms.StoreMessageInfo(mi)
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
func (x *QueryResponse) String() string {
|
| 455 |
+
return protoimpl.X.MessageStringOf(x)
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
func (*QueryResponse) ProtoMessage() {}
|
| 459 |
+
|
| 460 |
+
func (x *QueryResponse) ProtoReflect() protoreflect.Message {
|
| 461 |
+
mi := &file_agent_proto_msgTypes[2]
|
| 462 |
+
if x != nil {
|
| 463 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 464 |
+
if ms.LoadMessageInfo() == nil {
|
| 465 |
+
ms.StoreMessageInfo(mi)
|
| 466 |
+
}
|
| 467 |
+
return ms
|
| 468 |
+
}
|
| 469 |
+
return mi.MessageOf(x)
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
// Deprecated: Use QueryResponse.ProtoReflect.Descriptor instead.
|
| 473 |
+
func (*QueryResponse) Descriptor() ([]byte, []int) {
|
| 474 |
+
return file_agent_proto_rawDescGZIP(), []int{2}
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
func (x *QueryResponse) GetAnswer() string {
|
| 478 |
+
if x != nil {
|
| 479 |
+
return x.Answer
|
| 480 |
+
}
|
| 481 |
+
return ""
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
func (x *QueryResponse) GetSources() []*Source {
|
| 485 |
+
if x != nil {
|
| 486 |
+
return x.Sources
|
| 487 |
+
}
|
| 488 |
+
return nil
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
func (x *QueryResponse) GetConfidence() float32 {
|
| 492 |
+
if x != nil {
|
| 493 |
+
return x.Confidence
|
| 494 |
+
}
|
| 495 |
+
return 0
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
func (x *QueryResponse) GetMetadata() *QueryMetadata {
|
| 499 |
+
if x != nil {
|
| 500 |
+
return x.Metadata
|
| 501 |
+
}
|
| 502 |
+
return nil
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
func (x *QueryResponse) GetFollowUpQuestions() []string {
|
| 506 |
+
if x != nil {
|
| 507 |
+
return x.FollowUpQuestions
|
| 508 |
+
}
|
| 509 |
+
return nil
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
// QueryResponseChunk for streaming responses
|
| 513 |
+
type QueryResponseChunk struct {
|
| 514 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 515 |
+
// Chunk type: THINKING, RETRIEVAL, GENERATION, COMPLETE
|
| 516 |
+
Type ChunkType `protobuf:"varint,1,opt,name=type,proto3,enum=rag.v1.ChunkType" json:"type,omitempty"`
|
| 517 |
+
// Text content of the chunk
|
| 518 |
+
Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
|
| 519 |
+
// Sources (populated in RETRIEVAL chunks)
|
| 520 |
+
Sources []*Source `protobuf:"bytes,3,rep,name=sources,proto3" json:"sources,omitempty"`
|
| 521 |
+
// Is this the final chunk?
|
| 522 |
+
IsFinal bool `protobuf:"varint,4,opt,name=is_final,json=isFinal,proto3" json:"is_final,omitempty"`
|
| 523 |
+
unknownFields protoimpl.UnknownFields
|
| 524 |
+
sizeCache protoimpl.SizeCache
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
func (x *QueryResponseChunk) Reset() {
|
| 528 |
+
*x = QueryResponseChunk{}
|
| 529 |
+
mi := &file_agent_proto_msgTypes[3]
|
| 530 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 531 |
+
ms.StoreMessageInfo(mi)
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
func (x *QueryResponseChunk) String() string {
|
| 535 |
+
return protoimpl.X.MessageStringOf(x)
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
func (*QueryResponseChunk) ProtoMessage() {}
|
| 539 |
+
|
| 540 |
+
func (x *QueryResponseChunk) ProtoReflect() protoreflect.Message {
|
| 541 |
+
mi := &file_agent_proto_msgTypes[3]
|
| 542 |
+
if x != nil {
|
| 543 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 544 |
+
if ms.LoadMessageInfo() == nil {
|
| 545 |
+
ms.StoreMessageInfo(mi)
|
| 546 |
+
}
|
| 547 |
+
return ms
|
| 548 |
+
}
|
| 549 |
+
return mi.MessageOf(x)
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
// Deprecated: Use QueryResponseChunk.ProtoReflect.Descriptor instead.
|
| 553 |
+
func (*QueryResponseChunk) Descriptor() ([]byte, []int) {
|
| 554 |
+
return file_agent_proto_rawDescGZIP(), []int{3}
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
func (x *QueryResponseChunk) GetType() ChunkType {
|
| 558 |
+
if x != nil {
|
| 559 |
+
return x.Type
|
| 560 |
+
}
|
| 561 |
+
return ChunkType_CHUNK_TYPE_UNSPECIFIED
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
func (x *QueryResponseChunk) GetContent() string {
|
| 565 |
+
if x != nil {
|
| 566 |
+
return x.Content
|
| 567 |
+
}
|
| 568 |
+
return ""
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
func (x *QueryResponseChunk) GetSources() []*Source {
|
| 572 |
+
if x != nil {
|
| 573 |
+
return x.Sources
|
| 574 |
+
}
|
| 575 |
+
return nil
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
func (x *QueryResponseChunk) GetIsFinal() bool {
|
| 579 |
+
if x != nil {
|
| 580 |
+
return x.IsFinal
|
| 581 |
+
}
|
| 582 |
+
return false
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
// Source represents a retrieved document chunk
|
| 586 |
+
type Source struct {
|
| 587 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 588 |
+
// Unique identifier for the source
|
| 589 |
+
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
| 590 |
+
// Document title
|
| 591 |
+
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
| 592 |
+
// Relevant text content
|
| 593 |
+
Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"`
|
| 594 |
+
// Source URL or path
|
| 595 |
+
Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"`
|
| 596 |
+
// Relevance score (0.0 - 1.0)
|
| 597 |
+
Score float32 `protobuf:"fixed32,5,opt,name=score,proto3" json:"score,omitempty"`
|
| 598 |
+
// Source metadata
|
| 599 |
+
Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
| 600 |
+
// Page number or section reference
|
| 601 |
+
Location string `protobuf:"bytes,7,opt,name=location,proto3" json:"location,omitempty"`
|
| 602 |
+
unknownFields protoimpl.UnknownFields
|
| 603 |
+
sizeCache protoimpl.SizeCache
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
func (x *Source) Reset() {
|
| 607 |
+
*x = Source{}
|
| 608 |
+
mi := &file_agent_proto_msgTypes[4]
|
| 609 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 610 |
+
ms.StoreMessageInfo(mi)
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
func (x *Source) String() string {
|
| 614 |
+
return protoimpl.X.MessageStringOf(x)
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
func (*Source) ProtoMessage() {}
|
| 618 |
+
|
| 619 |
+
func (x *Source) ProtoReflect() protoreflect.Message {
|
| 620 |
+
mi := &file_agent_proto_msgTypes[4]
|
| 621 |
+
if x != nil {
|
| 622 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 623 |
+
if ms.LoadMessageInfo() == nil {
|
| 624 |
+
ms.StoreMessageInfo(mi)
|
| 625 |
+
}
|
| 626 |
+
return ms
|
| 627 |
+
}
|
| 628 |
+
return mi.MessageOf(x)
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
// Deprecated: Use Source.ProtoReflect.Descriptor instead.
|
| 632 |
+
func (*Source) Descriptor() ([]byte, []int) {
|
| 633 |
+
return file_agent_proto_rawDescGZIP(), []int{4}
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
func (x *Source) GetId() string {
|
| 637 |
+
if x != nil {
|
| 638 |
+
return x.Id
|
| 639 |
+
}
|
| 640 |
+
return ""
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
func (x *Source) GetTitle() string {
|
| 644 |
+
if x != nil {
|
| 645 |
+
return x.Title
|
| 646 |
+
}
|
| 647 |
+
return ""
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
func (x *Source) GetContent() string {
|
| 651 |
+
if x != nil {
|
| 652 |
+
return x.Content
|
| 653 |
+
}
|
| 654 |
+
return ""
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
func (x *Source) GetUrl() string {
|
| 658 |
+
if x != nil {
|
| 659 |
+
return x.Url
|
| 660 |
+
}
|
| 661 |
+
return ""
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
func (x *Source) GetScore() float32 {
|
| 665 |
+
if x != nil {
|
| 666 |
+
return x.Score
|
| 667 |
+
}
|
| 668 |
+
return 0
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
func (x *Source) GetMetadata() map[string]string {
|
| 672 |
+
if x != nil {
|
| 673 |
+
return x.Metadata
|
| 674 |
+
}
|
| 675 |
+
return nil
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
func (x *Source) GetLocation() string {
|
| 679 |
+
if x != nil {
|
| 680 |
+
return x.Location
|
| 681 |
+
}
|
| 682 |
+
return ""
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
// Message represents a conversation turn
|
| 686 |
+
type Message struct {
|
| 687 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 688 |
+
// Message role: USER, ASSISTANT, SYSTEM
|
| 689 |
+
Role MessageRole `protobuf:"varint,1,opt,name=role,proto3,enum=rag.v1.MessageRole" json:"role,omitempty"`
|
| 690 |
+
// Message content
|
| 691 |
+
Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
|
| 692 |
+
// Timestamp
|
| 693 |
+
Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
| 694 |
+
unknownFields protoimpl.UnknownFields
|
| 695 |
+
sizeCache protoimpl.SizeCache
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
func (x *Message) Reset() {
|
| 699 |
+
*x = Message{}
|
| 700 |
+
mi := &file_agent_proto_msgTypes[5]
|
| 701 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 702 |
+
ms.StoreMessageInfo(mi)
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
func (x *Message) String() string {
|
| 706 |
+
return protoimpl.X.MessageStringOf(x)
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
func (*Message) ProtoMessage() {}
|
| 710 |
+
|
| 711 |
+
func (x *Message) ProtoReflect() protoreflect.Message {
|
| 712 |
+
mi := &file_agent_proto_msgTypes[5]
|
| 713 |
+
if x != nil {
|
| 714 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 715 |
+
if ms.LoadMessageInfo() == nil {
|
| 716 |
+
ms.StoreMessageInfo(mi)
|
| 717 |
+
}
|
| 718 |
+
return ms
|
| 719 |
+
}
|
| 720 |
+
return mi.MessageOf(x)
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
// Deprecated: Use Message.ProtoReflect.Descriptor instead.
|
| 724 |
+
func (*Message) Descriptor() ([]byte, []int) {
|
| 725 |
+
return file_agent_proto_rawDescGZIP(), []int{5}
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
func (x *Message) GetRole() MessageRole {
|
| 729 |
+
if x != nil {
|
| 730 |
+
return x.Role
|
| 731 |
+
}
|
| 732 |
+
return MessageRole_MESSAGE_ROLE_UNSPECIFIED
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
func (x *Message) GetContent() string {
|
| 736 |
+
if x != nil {
|
| 737 |
+
return x.Content
|
| 738 |
+
}
|
| 739 |
+
return ""
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
func (x *Message) GetTimestamp() int64 {
|
| 743 |
+
if x != nil {
|
| 744 |
+
return x.Timestamp
|
| 745 |
+
}
|
| 746 |
+
return 0
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
// QueryMetadata contains processing information
|
| 750 |
+
type QueryMetadata struct {
|
| 751 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 752 |
+
// Total processing time in milliseconds
|
| 753 |
+
ProcessingTimeMs int64 `protobuf:"varint,1,opt,name=processing_time_ms,json=processingTimeMs,proto3" json:"processing_time_ms,omitempty"`
|
| 754 |
+
// Number of chunks retrieved
|
| 755 |
+
ChunksRetrieved int32 `protobuf:"varint,2,opt,name=chunks_retrieved,json=chunksRetrieved,proto3" json:"chunks_retrieved,omitempty"`
|
| 756 |
+
// Tokens used for generation
|
| 757 |
+
TokensUsed int32 `protobuf:"varint,3,opt,name=tokens_used,json=tokensUsed,proto3" json:"tokens_used,omitempty"`
|
| 758 |
+
// Whether result was cached
|
| 759 |
+
CacheHit bool `protobuf:"varint,4,opt,name=cache_hit,json=cacheHit,proto3" json:"cache_hit,omitempty"`
|
| 760 |
+
// Query routing decision
|
| 761 |
+
RoutingStrategy string `protobuf:"bytes,5,opt,name=routing_strategy,json=routingStrategy,proto3" json:"routing_strategy,omitempty"`
|
| 762 |
+
// Trace ID for debugging
|
| 763 |
+
TraceId string `protobuf:"bytes,6,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"`
|
| 764 |
+
unknownFields protoimpl.UnknownFields
|
| 765 |
+
sizeCache protoimpl.SizeCache
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
func (x *QueryMetadata) Reset() {
|
| 769 |
+
*x = QueryMetadata{}
|
| 770 |
+
mi := &file_agent_proto_msgTypes[6]
|
| 771 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 772 |
+
ms.StoreMessageInfo(mi)
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
func (x *QueryMetadata) String() string {
|
| 776 |
+
return protoimpl.X.MessageStringOf(x)
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
func (*QueryMetadata) ProtoMessage() {}
|
| 780 |
+
|
| 781 |
+
func (x *QueryMetadata) ProtoReflect() protoreflect.Message {
|
| 782 |
+
mi := &file_agent_proto_msgTypes[6]
|
| 783 |
+
if x != nil {
|
| 784 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 785 |
+
if ms.LoadMessageInfo() == nil {
|
| 786 |
+
ms.StoreMessageInfo(mi)
|
| 787 |
+
}
|
| 788 |
+
return ms
|
| 789 |
+
}
|
| 790 |
+
return mi.MessageOf(x)
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
// Deprecated: Use QueryMetadata.ProtoReflect.Descriptor instead.
|
| 794 |
+
func (*QueryMetadata) Descriptor() ([]byte, []int) {
|
| 795 |
+
return file_agent_proto_rawDescGZIP(), []int{6}
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
func (x *QueryMetadata) GetProcessingTimeMs() int64 {
|
| 799 |
+
if x != nil {
|
| 800 |
+
return x.ProcessingTimeMs
|
| 801 |
+
}
|
| 802 |
+
return 0
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
func (x *QueryMetadata) GetChunksRetrieved() int32 {
|
| 806 |
+
if x != nil {
|
| 807 |
+
return x.ChunksRetrieved
|
| 808 |
+
}
|
| 809 |
+
return 0
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
func (x *QueryMetadata) GetTokensUsed() int32 {
|
| 813 |
+
if x != nil {
|
| 814 |
+
return x.TokensUsed
|
| 815 |
+
}
|
| 816 |
+
return 0
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
func (x *QueryMetadata) GetCacheHit() bool {
|
| 820 |
+
if x != nil {
|
| 821 |
+
return x.CacheHit
|
| 822 |
+
}
|
| 823 |
+
return false
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
func (x *QueryMetadata) GetRoutingStrategy() string {
|
| 827 |
+
if x != nil {
|
| 828 |
+
return x.RoutingStrategy
|
| 829 |
+
}
|
| 830 |
+
return ""
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
func (x *QueryMetadata) GetTraceId() string {
|
| 834 |
+
if x != nil {
|
| 835 |
+
return x.TraceId
|
| 836 |
+
}
|
| 837 |
+
return ""
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
// CreateAgentRequest for creating specialized agents
|
| 841 |
+
type CreateAgentRequest struct {
|
| 842 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 843 |
+
// Agent name
|
| 844 |
+
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
| 845 |
+
// Agent description
|
| 846 |
+
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
|
| 847 |
+
// System prompt for the agent
|
| 848 |
+
SystemPrompt string `protobuf:"bytes,3,opt,name=system_prompt,json=systemPrompt,proto3" json:"system_prompt,omitempty"`
|
| 849 |
+
// Tools available to the agent
|
| 850 |
+
Tools []string `protobuf:"bytes,4,rep,name=tools,proto3" json:"tools,omitempty"`
|
| 851 |
+
// Agent configuration
|
| 852 |
+
Config *AgentConfig `protobuf:"bytes,5,opt,name=config,proto3" json:"config,omitempty"`
|
| 853 |
+
unknownFields protoimpl.UnknownFields
|
| 854 |
+
sizeCache protoimpl.SizeCache
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
func (x *CreateAgentRequest) Reset() {
|
| 858 |
+
*x = CreateAgentRequest{}
|
| 859 |
+
mi := &file_agent_proto_msgTypes[7]
|
| 860 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 861 |
+
ms.StoreMessageInfo(mi)
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
func (x *CreateAgentRequest) String() string {
|
| 865 |
+
return protoimpl.X.MessageStringOf(x)
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
func (*CreateAgentRequest) ProtoMessage() {}
|
| 869 |
+
|
| 870 |
+
func (x *CreateAgentRequest) ProtoReflect() protoreflect.Message {
|
| 871 |
+
mi := &file_agent_proto_msgTypes[7]
|
| 872 |
+
if x != nil {
|
| 873 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 874 |
+
if ms.LoadMessageInfo() == nil {
|
| 875 |
+
ms.StoreMessageInfo(mi)
|
| 876 |
+
}
|
| 877 |
+
return ms
|
| 878 |
+
}
|
| 879 |
+
return mi.MessageOf(x)
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
// Deprecated: Use CreateAgentRequest.ProtoReflect.Descriptor instead.
|
| 883 |
+
func (*CreateAgentRequest) Descriptor() ([]byte, []int) {
|
| 884 |
+
return file_agent_proto_rawDescGZIP(), []int{7}
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
func (x *CreateAgentRequest) GetName() string {
|
| 888 |
+
if x != nil {
|
| 889 |
+
return x.Name
|
| 890 |
+
}
|
| 891 |
+
return ""
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
func (x *CreateAgentRequest) GetDescription() string {
|
| 895 |
+
if x != nil {
|
| 896 |
+
return x.Description
|
| 897 |
+
}
|
| 898 |
+
return ""
|
| 899 |
+
}
|
| 900 |
+
|
| 901 |
+
func (x *CreateAgentRequest) GetSystemPrompt() string {
|
| 902 |
+
if x != nil {
|
| 903 |
+
return x.SystemPrompt
|
| 904 |
+
}
|
| 905 |
+
return ""
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
func (x *CreateAgentRequest) GetTools() []string {
|
| 909 |
+
if x != nil {
|
| 910 |
+
return x.Tools
|
| 911 |
+
}
|
| 912 |
+
return nil
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
func (x *CreateAgentRequest) GetConfig() *AgentConfig {
|
| 916 |
+
if x != nil {
|
| 917 |
+
return x.Config
|
| 918 |
+
}
|
| 919 |
+
return nil
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
// Agent represents a configured AI agent
|
| 923 |
+
type Agent struct {
|
| 924 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 925 |
+
// Unique agent ID
|
| 926 |
+
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
| 927 |
+
// Agent name
|
| 928 |
+
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
| 929 |
+
// Agent description
|
| 930 |
+
Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
|
| 931 |
+
// Creation timestamp
|
| 932 |
+
CreatedAt int64 `protobuf:"varint,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
| 933 |
+
// Agent configuration
|
| 934 |
+
Config *AgentConfig `protobuf:"bytes,5,opt,name=config,proto3" json:"config,omitempty"`
|
| 935 |
+
unknownFields protoimpl.UnknownFields
|
| 936 |
+
sizeCache protoimpl.SizeCache
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
func (x *Agent) Reset() {
|
| 940 |
+
*x = Agent{}
|
| 941 |
+
mi := &file_agent_proto_msgTypes[8]
|
| 942 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 943 |
+
ms.StoreMessageInfo(mi)
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
func (x *Agent) String() string {
|
| 947 |
+
return protoimpl.X.MessageStringOf(x)
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
func (*Agent) ProtoMessage() {}
|
| 951 |
+
|
| 952 |
+
func (x *Agent) ProtoReflect() protoreflect.Message {
|
| 953 |
+
mi := &file_agent_proto_msgTypes[8]
|
| 954 |
+
if x != nil {
|
| 955 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 956 |
+
if ms.LoadMessageInfo() == nil {
|
| 957 |
+
ms.StoreMessageInfo(mi)
|
| 958 |
+
}
|
| 959 |
+
return ms
|
| 960 |
+
}
|
| 961 |
+
return mi.MessageOf(x)
|
| 962 |
+
}
|
| 963 |
+
|
| 964 |
+
// Deprecated: Use Agent.ProtoReflect.Descriptor instead.
|
| 965 |
+
func (*Agent) Descriptor() ([]byte, []int) {
|
| 966 |
+
return file_agent_proto_rawDescGZIP(), []int{8}
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
func (x *Agent) GetId() string {
|
| 970 |
+
if x != nil {
|
| 971 |
+
return x.Id
|
| 972 |
+
}
|
| 973 |
+
return ""
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
func (x *Agent) GetName() string {
|
| 977 |
+
if x != nil {
|
| 978 |
+
return x.Name
|
| 979 |
+
}
|
| 980 |
+
return ""
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
func (x *Agent) GetDescription() string {
|
| 984 |
+
if x != nil {
|
| 985 |
+
return x.Description
|
| 986 |
+
}
|
| 987 |
+
return ""
|
| 988 |
+
}
|
| 989 |
+
|
| 990 |
+
func (x *Agent) GetCreatedAt() int64 {
|
| 991 |
+
if x != nil {
|
| 992 |
+
return x.CreatedAt
|
| 993 |
+
}
|
| 994 |
+
return 0
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
func (x *Agent) GetConfig() *AgentConfig {
|
| 998 |
+
if x != nil {
|
| 999 |
+
return x.Config
|
| 1000 |
+
}
|
| 1001 |
+
return nil
|
| 1002 |
+
}
|
| 1003 |
+
|
| 1004 |
+
// AgentConfig contains agent settings
|
| 1005 |
+
type AgentConfig struct {
|
| 1006 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1007 |
+
// LLM model to use
|
| 1008 |
+
Model string `protobuf:"bytes,1,opt,name=model,proto3" json:"model,omitempty"`
|
| 1009 |
+
// Temperature setting
|
| 1010 |
+
Temperature float32 `protobuf:"fixed32,2,opt,name=temperature,proto3" json:"temperature,omitempty"`
|
| 1011 |
+
// Maximum iterations for agentic loops
|
| 1012 |
+
MaxIterations int32 `protobuf:"varint,3,opt,name=max_iterations,json=maxIterations,proto3" json:"max_iterations,omitempty"`
|
| 1013 |
+
// Timeout in seconds
|
| 1014 |
+
TimeoutSeconds int32 `protobuf:"varint,4,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"`
|
| 1015 |
+
unknownFields protoimpl.UnknownFields
|
| 1016 |
+
sizeCache protoimpl.SizeCache
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
func (x *AgentConfig) Reset() {
|
| 1020 |
+
*x = AgentConfig{}
|
| 1021 |
+
mi := &file_agent_proto_msgTypes[9]
|
| 1022 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1023 |
+
ms.StoreMessageInfo(mi)
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
func (x *AgentConfig) String() string {
|
| 1027 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
func (*AgentConfig) ProtoMessage() {}
|
| 1031 |
+
|
| 1032 |
+
func (x *AgentConfig) ProtoReflect() protoreflect.Message {
|
| 1033 |
+
mi := &file_agent_proto_msgTypes[9]
|
| 1034 |
+
if x != nil {
|
| 1035 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1036 |
+
if ms.LoadMessageInfo() == nil {
|
| 1037 |
+
ms.StoreMessageInfo(mi)
|
| 1038 |
+
}
|
| 1039 |
+
return ms
|
| 1040 |
+
}
|
| 1041 |
+
return mi.MessageOf(x)
|
| 1042 |
+
}
|
| 1043 |
+
|
| 1044 |
+
// Deprecated: Use AgentConfig.ProtoReflect.Descriptor instead.
|
| 1045 |
+
func (*AgentConfig) Descriptor() ([]byte, []int) {
|
| 1046 |
+
return file_agent_proto_rawDescGZIP(), []int{9}
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
func (x *AgentConfig) GetModel() string {
|
| 1050 |
+
if x != nil {
|
| 1051 |
+
return x.Model
|
| 1052 |
+
}
|
| 1053 |
+
return ""
|
| 1054 |
+
}
|
| 1055 |
+
|
| 1056 |
+
func (x *AgentConfig) GetTemperature() float32 {
|
| 1057 |
+
if x != nil {
|
| 1058 |
+
return x.Temperature
|
| 1059 |
+
}
|
| 1060 |
+
return 0
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
func (x *AgentConfig) GetMaxIterations() int32 {
|
| 1064 |
+
if x != nil {
|
| 1065 |
+
return x.MaxIterations
|
| 1066 |
+
}
|
| 1067 |
+
return 0
|
| 1068 |
+
}
|
| 1069 |
+
|
| 1070 |
+
func (x *AgentConfig) GetTimeoutSeconds() int32 {
|
| 1071 |
+
if x != nil {
|
| 1072 |
+
return x.TimeoutSeconds
|
| 1073 |
+
}
|
| 1074 |
+
return 0
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
// ExecutionPlan for multi-step queries
|
| 1078 |
+
type ExecutionPlan struct {
|
| 1079 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1080 |
+
// Plan ID
|
| 1081 |
+
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
| 1082 |
+
// Original query
|
| 1083 |
+
Query string `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"`
|
| 1084 |
+
// Execution steps
|
| 1085 |
+
Steps []*ExecutionStep `protobuf:"bytes,3,rep,name=steps,proto3" json:"steps,omitempty"`
|
| 1086 |
+
unknownFields protoimpl.UnknownFields
|
| 1087 |
+
sizeCache protoimpl.SizeCache
|
| 1088 |
+
}
|
| 1089 |
+
|
| 1090 |
+
func (x *ExecutionPlan) Reset() {
|
| 1091 |
+
*x = ExecutionPlan{}
|
| 1092 |
+
mi := &file_agent_proto_msgTypes[10]
|
| 1093 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1094 |
+
ms.StoreMessageInfo(mi)
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
func (x *ExecutionPlan) String() string {
|
| 1098 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1099 |
+
}
|
| 1100 |
+
|
| 1101 |
+
func (*ExecutionPlan) ProtoMessage() {}
|
| 1102 |
+
|
| 1103 |
+
func (x *ExecutionPlan) ProtoReflect() protoreflect.Message {
|
| 1104 |
+
mi := &file_agent_proto_msgTypes[10]
|
| 1105 |
+
if x != nil {
|
| 1106 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1107 |
+
if ms.LoadMessageInfo() == nil {
|
| 1108 |
+
ms.StoreMessageInfo(mi)
|
| 1109 |
+
}
|
| 1110 |
+
return ms
|
| 1111 |
+
}
|
| 1112 |
+
return mi.MessageOf(x)
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
// Deprecated: Use ExecutionPlan.ProtoReflect.Descriptor instead.
|
| 1116 |
+
func (*ExecutionPlan) Descriptor() ([]byte, []int) {
|
| 1117 |
+
return file_agent_proto_rawDescGZIP(), []int{10}
|
| 1118 |
+
}
|
| 1119 |
+
|
| 1120 |
+
func (x *ExecutionPlan) GetId() string {
|
| 1121 |
+
if x != nil {
|
| 1122 |
+
return x.Id
|
| 1123 |
+
}
|
| 1124 |
+
return ""
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
func (x *ExecutionPlan) GetQuery() string {
|
| 1128 |
+
if x != nil {
|
| 1129 |
+
return x.Query
|
| 1130 |
+
}
|
| 1131 |
+
return ""
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
func (x *ExecutionPlan) GetSteps() []*ExecutionStep {
|
| 1135 |
+
if x != nil {
|
| 1136 |
+
return x.Steps
|
| 1137 |
+
}
|
| 1138 |
+
return nil
|
| 1139 |
+
}
|
| 1140 |
+
|
| 1141 |
+
// ExecutionStep represents a single step in the plan
|
| 1142 |
+
type ExecutionStep struct {
|
| 1143 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1144 |
+
// Step ID
|
| 1145 |
+
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
| 1146 |
+
// Step type: SEARCH, ANALYZE, SYNTHESIZE
|
| 1147 |
+
Type StepType `protobuf:"varint,2,opt,name=type,proto3,enum=rag.v1.StepType" json:"type,omitempty"`
|
| 1148 |
+
// Step description
|
| 1149 |
+
Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
|
| 1150 |
+
// Tool to use
|
| 1151 |
+
Tool string `protobuf:"bytes,4,opt,name=tool,proto3" json:"tool,omitempty"`
|
| 1152 |
+
// Input for the step
|
| 1153 |
+
Input string `protobuf:"bytes,5,opt,name=input,proto3" json:"input,omitempty"`
|
| 1154 |
+
// Dependencies (IDs of steps that must complete first)
|
| 1155 |
+
Dependencies []string `protobuf:"bytes,6,rep,name=dependencies,proto3" json:"dependencies,omitempty"`
|
| 1156 |
+
unknownFields protoimpl.UnknownFields
|
| 1157 |
+
sizeCache protoimpl.SizeCache
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
func (x *ExecutionStep) Reset() {
|
| 1161 |
+
*x = ExecutionStep{}
|
| 1162 |
+
mi := &file_agent_proto_msgTypes[11]
|
| 1163 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1164 |
+
ms.StoreMessageInfo(mi)
|
| 1165 |
+
}
|
| 1166 |
+
|
| 1167 |
+
func (x *ExecutionStep) String() string {
|
| 1168 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
func (*ExecutionStep) ProtoMessage() {}
|
| 1172 |
+
|
| 1173 |
+
func (x *ExecutionStep) ProtoReflect() protoreflect.Message {
|
| 1174 |
+
mi := &file_agent_proto_msgTypes[11]
|
| 1175 |
+
if x != nil {
|
| 1176 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1177 |
+
if ms.LoadMessageInfo() == nil {
|
| 1178 |
+
ms.StoreMessageInfo(mi)
|
| 1179 |
+
}
|
| 1180 |
+
return ms
|
| 1181 |
+
}
|
| 1182 |
+
return mi.MessageOf(x)
|
| 1183 |
+
}
|
| 1184 |
+
|
| 1185 |
+
// Deprecated: Use ExecutionStep.ProtoReflect.Descriptor instead.
|
| 1186 |
+
func (*ExecutionStep) Descriptor() ([]byte, []int) {
|
| 1187 |
+
return file_agent_proto_rawDescGZIP(), []int{11}
|
| 1188 |
+
}
|
| 1189 |
+
|
| 1190 |
+
func (x *ExecutionStep) GetId() string {
|
| 1191 |
+
if x != nil {
|
| 1192 |
+
return x.Id
|
| 1193 |
+
}
|
| 1194 |
+
return ""
|
| 1195 |
+
}
|
| 1196 |
+
|
| 1197 |
+
func (x *ExecutionStep) GetType() StepType {
|
| 1198 |
+
if x != nil {
|
| 1199 |
+
return x.Type
|
| 1200 |
+
}
|
| 1201 |
+
return StepType_STEP_TYPE_UNSPECIFIED
|
| 1202 |
+
}
|
| 1203 |
+
|
| 1204 |
+
func (x *ExecutionStep) GetDescription() string {
|
| 1205 |
+
if x != nil {
|
| 1206 |
+
return x.Description
|
| 1207 |
+
}
|
| 1208 |
+
return ""
|
| 1209 |
+
}
|
| 1210 |
+
|
| 1211 |
+
func (x *ExecutionStep) GetTool() string {
|
| 1212 |
+
if x != nil {
|
| 1213 |
+
return x.Tool
|
| 1214 |
+
}
|
| 1215 |
+
return ""
|
| 1216 |
+
}
|
| 1217 |
+
|
| 1218 |
+
func (x *ExecutionStep) GetInput() string {
|
| 1219 |
+
if x != nil {
|
| 1220 |
+
return x.Input
|
| 1221 |
+
}
|
| 1222 |
+
return ""
|
| 1223 |
+
}
|
| 1224 |
+
|
| 1225 |
+
func (x *ExecutionStep) GetDependencies() []string {
|
| 1226 |
+
if x != nil {
|
| 1227 |
+
return x.Dependencies
|
| 1228 |
+
}
|
| 1229 |
+
return nil
|
| 1230 |
+
}
|
| 1231 |
+
|
| 1232 |
+
// PlanResult contains execution results
|
| 1233 |
+
type PlanResult struct {
|
| 1234 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1235 |
+
// Plan ID
|
| 1236 |
+
PlanId string `protobuf:"bytes,1,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"`
|
| 1237 |
+
// Execution status
|
| 1238 |
+
Status ExecutionStatus `protobuf:"varint,2,opt,name=status,proto3,enum=rag.v1.ExecutionStatus" json:"status,omitempty"`
|
| 1239 |
+
// Step results
|
| 1240 |
+
StepResults []*StepResult `protobuf:"bytes,3,rep,name=step_results,json=stepResults,proto3" json:"step_results,omitempty"`
|
| 1241 |
+
// Final answer
|
| 1242 |
+
FinalAnswer string `protobuf:"bytes,4,opt,name=final_answer,json=finalAnswer,proto3" json:"final_answer,omitempty"`
|
| 1243 |
+
// Total execution time
|
| 1244 |
+
ExecutionTimeMs int64 `protobuf:"varint,5,opt,name=execution_time_ms,json=executionTimeMs,proto3" json:"execution_time_ms,omitempty"`
|
| 1245 |
+
unknownFields protoimpl.UnknownFields
|
| 1246 |
+
sizeCache protoimpl.SizeCache
|
| 1247 |
+
}
|
| 1248 |
+
|
| 1249 |
+
func (x *PlanResult) Reset() {
|
| 1250 |
+
*x = PlanResult{}
|
| 1251 |
+
mi := &file_agent_proto_msgTypes[12]
|
| 1252 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1253 |
+
ms.StoreMessageInfo(mi)
|
| 1254 |
+
}
|
| 1255 |
+
|
| 1256 |
+
func (x *PlanResult) String() string {
|
| 1257 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1258 |
+
}
|
| 1259 |
+
|
| 1260 |
+
func (*PlanResult) ProtoMessage() {}
|
| 1261 |
+
|
| 1262 |
+
func (x *PlanResult) ProtoReflect() protoreflect.Message {
|
| 1263 |
+
mi := &file_agent_proto_msgTypes[12]
|
| 1264 |
+
if x != nil {
|
| 1265 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1266 |
+
if ms.LoadMessageInfo() == nil {
|
| 1267 |
+
ms.StoreMessageInfo(mi)
|
| 1268 |
+
}
|
| 1269 |
+
return ms
|
| 1270 |
+
}
|
| 1271 |
+
return mi.MessageOf(x)
|
| 1272 |
+
}
|
| 1273 |
+
|
| 1274 |
+
// Deprecated: Use PlanResult.ProtoReflect.Descriptor instead.
|
| 1275 |
+
func (*PlanResult) Descriptor() ([]byte, []int) {
|
| 1276 |
+
return file_agent_proto_rawDescGZIP(), []int{12}
|
| 1277 |
+
}
|
| 1278 |
+
|
| 1279 |
+
func (x *PlanResult) GetPlanId() string {
|
| 1280 |
+
if x != nil {
|
| 1281 |
+
return x.PlanId
|
| 1282 |
+
}
|
| 1283 |
+
return ""
|
| 1284 |
+
}
|
| 1285 |
+
|
| 1286 |
+
func (x *PlanResult) GetStatus() ExecutionStatus {
|
| 1287 |
+
if x != nil {
|
| 1288 |
+
return x.Status
|
| 1289 |
+
}
|
| 1290 |
+
return ExecutionStatus_EXECUTION_STATUS_UNSPECIFIED
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
func (x *PlanResult) GetStepResults() []*StepResult {
|
| 1294 |
+
if x != nil {
|
| 1295 |
+
return x.StepResults
|
| 1296 |
+
}
|
| 1297 |
+
return nil
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
func (x *PlanResult) GetFinalAnswer() string {
|
| 1301 |
+
if x != nil {
|
| 1302 |
+
return x.FinalAnswer
|
| 1303 |
+
}
|
| 1304 |
+
return ""
|
| 1305 |
+
}
|
| 1306 |
+
|
| 1307 |
+
func (x *PlanResult) GetExecutionTimeMs() int64 {
|
| 1308 |
+
if x != nil {
|
| 1309 |
+
return x.ExecutionTimeMs
|
| 1310 |
+
}
|
| 1311 |
+
return 0
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
// StepResult contains individual step results
|
| 1315 |
+
type StepResult struct {
|
| 1316 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1317 |
+
// Step ID
|
| 1318 |
+
StepId string `protobuf:"bytes,1,opt,name=step_id,json=stepId,proto3" json:"step_id,omitempty"`
|
| 1319 |
+
// Step status
|
| 1320 |
+
Status ExecutionStatus `protobuf:"varint,2,opt,name=status,proto3,enum=rag.v1.ExecutionStatus" json:"status,omitempty"`
|
| 1321 |
+
// Step output
|
| 1322 |
+
Output string `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"`
|
| 1323 |
+
// Error message if failed
|
| 1324 |
+
Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"`
|
| 1325 |
+
// Execution time
|
| 1326 |
+
ExecutionTimeMs int64 `protobuf:"varint,5,opt,name=execution_time_ms,json=executionTimeMs,proto3" json:"execution_time_ms,omitempty"`
|
| 1327 |
+
unknownFields protoimpl.UnknownFields
|
| 1328 |
+
sizeCache protoimpl.SizeCache
|
| 1329 |
+
}
|
| 1330 |
+
|
| 1331 |
+
func (x *StepResult) Reset() {
|
| 1332 |
+
*x = StepResult{}
|
| 1333 |
+
mi := &file_agent_proto_msgTypes[13]
|
| 1334 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1335 |
+
ms.StoreMessageInfo(mi)
|
| 1336 |
+
}
|
| 1337 |
+
|
| 1338 |
+
func (x *StepResult) String() string {
|
| 1339 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1340 |
+
}
|
| 1341 |
+
|
| 1342 |
+
func (*StepResult) ProtoMessage() {}
|
| 1343 |
+
|
| 1344 |
+
func (x *StepResult) ProtoReflect() protoreflect.Message {
|
| 1345 |
+
mi := &file_agent_proto_msgTypes[13]
|
| 1346 |
+
if x != nil {
|
| 1347 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1348 |
+
if ms.LoadMessageInfo() == nil {
|
| 1349 |
+
ms.StoreMessageInfo(mi)
|
| 1350 |
+
}
|
| 1351 |
+
return ms
|
| 1352 |
+
}
|
| 1353 |
+
return mi.MessageOf(x)
|
| 1354 |
+
}
|
| 1355 |
+
|
| 1356 |
+
// Deprecated: Use StepResult.ProtoReflect.Descriptor instead.
|
| 1357 |
+
func (*StepResult) Descriptor() ([]byte, []int) {
|
| 1358 |
+
return file_agent_proto_rawDescGZIP(), []int{13}
|
| 1359 |
+
}
|
| 1360 |
+
|
| 1361 |
+
func (x *StepResult) GetStepId() string {
|
| 1362 |
+
if x != nil {
|
| 1363 |
+
return x.StepId
|
| 1364 |
+
}
|
| 1365 |
+
return ""
|
| 1366 |
+
}
|
| 1367 |
+
|
| 1368 |
+
func (x *StepResult) GetStatus() ExecutionStatus {
|
| 1369 |
+
if x != nil {
|
| 1370 |
+
return x.Status
|
| 1371 |
+
}
|
| 1372 |
+
return ExecutionStatus_EXECUTION_STATUS_UNSPECIFIED
|
| 1373 |
+
}
|
| 1374 |
+
|
| 1375 |
+
func (x *StepResult) GetOutput() string {
|
| 1376 |
+
if x != nil {
|
| 1377 |
+
return x.Output
|
| 1378 |
+
}
|
| 1379 |
+
return ""
|
| 1380 |
+
}
|
| 1381 |
+
|
| 1382 |
+
func (x *StepResult) GetError() string {
|
| 1383 |
+
if x != nil {
|
| 1384 |
+
return x.Error
|
| 1385 |
+
}
|
| 1386 |
+
return ""
|
| 1387 |
+
}
|
| 1388 |
+
|
| 1389 |
+
func (x *StepResult) GetExecutionTimeMs() int64 {
|
| 1390 |
+
if x != nil {
|
| 1391 |
+
return x.ExecutionTimeMs
|
| 1392 |
+
}
|
| 1393 |
+
return 0
|
| 1394 |
+
}
|
| 1395 |
+
|
| 1396 |
+
// QueryStatusRequest to check query progress
|
| 1397 |
+
type QueryStatusRequest struct {
|
| 1398 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1399 |
+
// Query ID
|
| 1400 |
+
QueryId string `protobuf:"bytes,1,opt,name=query_id,json=queryId,proto3" json:"query_id,omitempty"`
|
| 1401 |
+
unknownFields protoimpl.UnknownFields
|
| 1402 |
+
sizeCache protoimpl.SizeCache
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
func (x *QueryStatusRequest) Reset() {
|
| 1406 |
+
*x = QueryStatusRequest{}
|
| 1407 |
+
mi := &file_agent_proto_msgTypes[14]
|
| 1408 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1409 |
+
ms.StoreMessageInfo(mi)
|
| 1410 |
+
}
|
| 1411 |
+
|
| 1412 |
+
func (x *QueryStatusRequest) String() string {
|
| 1413 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1414 |
+
}
|
| 1415 |
+
|
| 1416 |
+
func (*QueryStatusRequest) ProtoMessage() {}
|
| 1417 |
+
|
| 1418 |
+
func (x *QueryStatusRequest) ProtoReflect() protoreflect.Message {
|
| 1419 |
+
mi := &file_agent_proto_msgTypes[14]
|
| 1420 |
+
if x != nil {
|
| 1421 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1422 |
+
if ms.LoadMessageInfo() == nil {
|
| 1423 |
+
ms.StoreMessageInfo(mi)
|
| 1424 |
+
}
|
| 1425 |
+
return ms
|
| 1426 |
+
}
|
| 1427 |
+
return mi.MessageOf(x)
|
| 1428 |
+
}
|
| 1429 |
+
|
| 1430 |
+
// Deprecated: Use QueryStatusRequest.ProtoReflect.Descriptor instead.
|
| 1431 |
+
func (*QueryStatusRequest) Descriptor() ([]byte, []int) {
|
| 1432 |
+
return file_agent_proto_rawDescGZIP(), []int{14}
|
| 1433 |
+
}
|
| 1434 |
+
|
| 1435 |
+
func (x *QueryStatusRequest) GetQueryId() string {
|
| 1436 |
+
if x != nil {
|
| 1437 |
+
return x.QueryId
|
| 1438 |
+
}
|
| 1439 |
+
return ""
|
| 1440 |
+
}
|
| 1441 |
+
|
| 1442 |
+
// QueryStatus represents current query state
|
| 1443 |
+
type QueryStatus struct {
|
| 1444 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1445 |
+
// Query ID
|
| 1446 |
+
QueryId string `protobuf:"bytes,1,opt,name=query_id,json=queryId,proto3" json:"query_id,omitempty"`
|
| 1447 |
+
// Current status
|
| 1448 |
+
Status ExecutionStatus `protobuf:"varint,2,opt,name=status,proto3,enum=rag.v1.ExecutionStatus" json:"status,omitempty"`
|
| 1449 |
+
// Progress percentage (0-100)
|
| 1450 |
+
Progress int32 `protobuf:"varint,3,opt,name=progress,proto3" json:"progress,omitempty"`
|
| 1451 |
+
// Current step description
|
| 1452 |
+
CurrentStep string `protobuf:"bytes,4,opt,name=current_step,json=currentStep,proto3" json:"current_step,omitempty"`
|
| 1453 |
+
unknownFields protoimpl.UnknownFields
|
| 1454 |
+
sizeCache protoimpl.SizeCache
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
func (x *QueryStatus) Reset() {
|
| 1458 |
+
*x = QueryStatus{}
|
| 1459 |
+
mi := &file_agent_proto_msgTypes[15]
|
| 1460 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1461 |
+
ms.StoreMessageInfo(mi)
|
| 1462 |
+
}
|
| 1463 |
+
|
| 1464 |
+
func (x *QueryStatus) String() string {
|
| 1465 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1466 |
+
}
|
| 1467 |
+
|
| 1468 |
+
func (*QueryStatus) ProtoMessage() {}
|
| 1469 |
+
|
| 1470 |
+
func (x *QueryStatus) ProtoReflect() protoreflect.Message {
|
| 1471 |
+
mi := &file_agent_proto_msgTypes[15]
|
| 1472 |
+
if x != nil {
|
| 1473 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1474 |
+
if ms.LoadMessageInfo() == nil {
|
| 1475 |
+
ms.StoreMessageInfo(mi)
|
| 1476 |
+
}
|
| 1477 |
+
return ms
|
| 1478 |
+
}
|
| 1479 |
+
return mi.MessageOf(x)
|
| 1480 |
+
}
|
| 1481 |
+
|
| 1482 |
+
// Deprecated: Use QueryStatus.ProtoReflect.Descriptor instead.
|
| 1483 |
+
func (*QueryStatus) Descriptor() ([]byte, []int) {
|
| 1484 |
+
return file_agent_proto_rawDescGZIP(), []int{15}
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
func (x *QueryStatus) GetQueryId() string {
|
| 1488 |
+
if x != nil {
|
| 1489 |
+
return x.QueryId
|
| 1490 |
+
}
|
| 1491 |
+
return ""
|
| 1492 |
+
}
|
| 1493 |
+
|
| 1494 |
+
func (x *QueryStatus) GetStatus() ExecutionStatus {
|
| 1495 |
+
if x != nil {
|
| 1496 |
+
return x.Status
|
| 1497 |
+
}
|
| 1498 |
+
return ExecutionStatus_EXECUTION_STATUS_UNSPECIFIED
|
| 1499 |
+
}
|
| 1500 |
+
|
| 1501 |
+
func (x *QueryStatus) GetProgress() int32 {
|
| 1502 |
+
if x != nil {
|
| 1503 |
+
return x.Progress
|
| 1504 |
+
}
|
| 1505 |
+
return 0
|
| 1506 |
+
}
|
| 1507 |
+
|
| 1508 |
+
func (x *QueryStatus) GetCurrentStep() string {
|
| 1509 |
+
if x != nil {
|
| 1510 |
+
return x.CurrentStep
|
| 1511 |
+
}
|
| 1512 |
+
return ""
|
| 1513 |
+
}
|
| 1514 |
+
|
| 1515 |
+
// SecurityContext for request authentication/authorization
|
| 1516 |
+
type SecurityContext struct {
|
| 1517 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1518 |
+
// User ID
|
| 1519 |
+
UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
| 1520 |
+
// User roles
|
| 1521 |
+
Roles []string `protobuf:"bytes,2,rep,name=roles,proto3" json:"roles,omitempty"`
|
| 1522 |
+
// Tenant ID for multi-tenancy
|
| 1523 |
+
TenantId string `protobuf:"bytes,3,opt,name=tenant_id,json=tenantId,proto3" json:"tenant_id,omitempty"`
|
| 1524 |
+
// Additional claims
|
| 1525 |
+
Claims map[string]string `protobuf:"bytes,4,rep,name=claims,proto3" json:"claims,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
| 1526 |
+
unknownFields protoimpl.UnknownFields
|
| 1527 |
+
sizeCache protoimpl.SizeCache
|
| 1528 |
+
}
|
| 1529 |
+
|
| 1530 |
+
func (x *SecurityContext) Reset() {
|
| 1531 |
+
*x = SecurityContext{}
|
| 1532 |
+
mi := &file_agent_proto_msgTypes[16]
|
| 1533 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1534 |
+
ms.StoreMessageInfo(mi)
|
| 1535 |
+
}
|
| 1536 |
+
|
| 1537 |
+
func (x *SecurityContext) String() string {
|
| 1538 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1539 |
+
}
|
| 1540 |
+
|
| 1541 |
+
func (*SecurityContext) ProtoMessage() {}
|
| 1542 |
+
|
| 1543 |
+
func (x *SecurityContext) ProtoReflect() protoreflect.Message {
|
| 1544 |
+
mi := &file_agent_proto_msgTypes[16]
|
| 1545 |
+
if x != nil {
|
| 1546 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1547 |
+
if ms.LoadMessageInfo() == nil {
|
| 1548 |
+
ms.StoreMessageInfo(mi)
|
| 1549 |
+
}
|
| 1550 |
+
return ms
|
| 1551 |
+
}
|
| 1552 |
+
return mi.MessageOf(x)
|
| 1553 |
+
}
|
| 1554 |
+
|
| 1555 |
+
// Deprecated: Use SecurityContext.ProtoReflect.Descriptor instead.
|
| 1556 |
+
func (*SecurityContext) Descriptor() ([]byte, []int) {
|
| 1557 |
+
return file_agent_proto_rawDescGZIP(), []int{16}
|
| 1558 |
+
}
|
| 1559 |
+
|
| 1560 |
+
func (x *SecurityContext) GetUserId() string {
|
| 1561 |
+
if x != nil {
|
| 1562 |
+
return x.UserId
|
| 1563 |
+
}
|
| 1564 |
+
return ""
|
| 1565 |
+
}
|
| 1566 |
+
|
| 1567 |
+
func (x *SecurityContext) GetRoles() []string {
|
| 1568 |
+
if x != nil {
|
| 1569 |
+
return x.Roles
|
| 1570 |
+
}
|
| 1571 |
+
return nil
|
| 1572 |
+
}
|
| 1573 |
+
|
| 1574 |
+
func (x *SecurityContext) GetTenantId() string {
|
| 1575 |
+
if x != nil {
|
| 1576 |
+
return x.TenantId
|
| 1577 |
+
}
|
| 1578 |
+
return ""
|
| 1579 |
+
}
|
| 1580 |
+
|
| 1581 |
+
func (x *SecurityContext) GetClaims() map[string]string {
|
| 1582 |
+
if x != nil {
|
| 1583 |
+
return x.Claims
|
| 1584 |
+
}
|
| 1585 |
+
return nil
|
| 1586 |
+
}
|
| 1587 |
+
|
| 1588 |
+
var File_agent_proto protoreflect.FileDescriptor
|
| 1589 |
+
|
| 1590 |
+
const file_agent_proto_rawDesc = "" +
|
| 1591 |
+
"\n" +
|
| 1592 |
+
"\vagent.proto\x12\x06rag.v1\"\xcd\x02\n" +
|
| 1593 |
+
"\fQueryRequest\x12\x14\n" +
|
| 1594 |
+
"\x05query\x18\x01 \x01(\tR\x05query\x12\x1d\n" +
|
| 1595 |
+
"\n" +
|
| 1596 |
+
"session_id\x18\x02 \x01(\tR\tsessionId\x12\x17\n" +
|
| 1597 |
+
"\auser_id\x18\x03 \x01(\tR\x06userId\x12B\n" +
|
| 1598 |
+
"\x14conversation_history\x18\x04 \x03(\v2\x0f.rag.v1.MessageR\x13conversationHistory\x12>\n" +
|
| 1599 |
+
"\bmetadata\x18\x05 \x03(\v2\".rag.v1.QueryRequest.MetadataEntryR\bmetadata\x12.\n" +
|
| 1600 |
+
"\aoptions\x18\x06 \x01(\v2\x14.rag.v1.QueryOptionsR\aoptions\x1a;\n" +
|
| 1601 |
+
"\rMetadataEntry\x12\x10\n" +
|
| 1602 |
+
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
| 1603 |
+
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xdd\x01\n" +
|
| 1604 |
+
"\fQueryOptions\x12\x1f\n" +
|
| 1605 |
+
"\vmax_sources\x18\x01 \x01(\x05R\n" +
|
| 1606 |
+
"maxSources\x12\x1b\n" +
|
| 1607 |
+
"\tuse_cache\x18\x02 \x01(\bR\buseCache\x12%\n" +
|
| 1608 |
+
"\x0eenable_agentic\x18\x03 \x01(\bR\renableAgentic\x12'\n" +
|
| 1609 |
+
"\x0fknowledge_bases\x18\x04 \x03(\tR\x0eknowledgeBases\x12 \n" +
|
| 1610 |
+
"\vtemperature\x18\x05 \x01(\x02R\vtemperature\x12\x1d\n" +
|
| 1611 |
+
"\n" +
|
| 1612 |
+
"max_tokens\x18\x06 \x01(\x05R\tmaxTokens\"\xd4\x01\n" +
|
| 1613 |
+
"\rQueryResponse\x12\x16\n" +
|
| 1614 |
+
"\x06answer\x18\x01 \x01(\tR\x06answer\x12(\n" +
|
| 1615 |
+
"\asources\x18\x02 \x03(\v2\x0e.rag.v1.SourceR\asources\x12\x1e\n" +
|
| 1616 |
+
"\n" +
|
| 1617 |
+
"confidence\x18\x03 \x01(\x02R\n" +
|
| 1618 |
+
"confidence\x121\n" +
|
| 1619 |
+
"\bmetadata\x18\x04 \x01(\v2\x15.rag.v1.QueryMetadataR\bmetadata\x12.\n" +
|
| 1620 |
+
"\x13follow_up_questions\x18\x05 \x03(\tR\x11followUpQuestions\"\x9a\x01\n" +
|
| 1621 |
+
"\x12QueryResponseChunk\x12%\n" +
|
| 1622 |
+
"\x04type\x18\x01 \x01(\x0e2\x11.rag.v1.ChunkTypeR\x04type\x12\x18\n" +
|
| 1623 |
+
"\acontent\x18\x02 \x01(\tR\acontent\x12(\n" +
|
| 1624 |
+
"\asources\x18\x03 \x03(\v2\x0e.rag.v1.SourceR\asources\x12\x19\n" +
|
| 1625 |
+
"\bis_final\x18\x04 \x01(\bR\aisFinal\"\x83\x02\n" +
|
| 1626 |
+
"\x06Source\x12\x0e\n" +
|
| 1627 |
+
"\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
|
| 1628 |
+
"\x05title\x18\x02 \x01(\tR\x05title\x12\x18\n" +
|
| 1629 |
+
"\acontent\x18\x03 \x01(\tR\acontent\x12\x10\n" +
|
| 1630 |
+
"\x03url\x18\x04 \x01(\tR\x03url\x12\x14\n" +
|
| 1631 |
+
"\x05score\x18\x05 \x01(\x02R\x05score\x128\n" +
|
| 1632 |
+
"\bmetadata\x18\x06 \x03(\v2\x1c.rag.v1.Source.MetadataEntryR\bmetadata\x12\x1a\n" +
|
| 1633 |
+
"\blocation\x18\a \x01(\tR\blocation\x1a;\n" +
|
| 1634 |
+
"\rMetadataEntry\x12\x10\n" +
|
| 1635 |
+
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
| 1636 |
+
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"j\n" +
|
| 1637 |
+
"\aMessage\x12'\n" +
|
| 1638 |
+
"\x04role\x18\x01 \x01(\x0e2\x13.rag.v1.MessageRoleR\x04role\x12\x18\n" +
|
| 1639 |
+
"\acontent\x18\x02 \x01(\tR\acontent\x12\x1c\n" +
|
| 1640 |
+
"\ttimestamp\x18\x03 \x01(\x03R\ttimestamp\"\xec\x01\n" +
|
| 1641 |
+
"\rQueryMetadata\x12,\n" +
|
| 1642 |
+
"\x12processing_time_ms\x18\x01 \x01(\x03R\x10processingTimeMs\x12)\n" +
|
| 1643 |
+
"\x10chunks_retrieved\x18\x02 \x01(\x05R\x0fchunksRetrieved\x12\x1f\n" +
|
| 1644 |
+
"\vtokens_used\x18\x03 \x01(\x05R\n" +
|
| 1645 |
+
"tokensUsed\x12\x1b\n" +
|
| 1646 |
+
"\tcache_hit\x18\x04 \x01(\bR\bcacheHit\x12)\n" +
|
| 1647 |
+
"\x10routing_strategy\x18\x05 \x01(\tR\x0froutingStrategy\x12\x19\n" +
|
| 1648 |
+
"\btrace_id\x18\x06 \x01(\tR\atraceId\"\xb2\x01\n" +
|
| 1649 |
+
"\x12CreateAgentRequest\x12\x12\n" +
|
| 1650 |
+
"\x04name\x18\x01 \x01(\tR\x04name\x12 \n" +
|
| 1651 |
+
"\vdescription\x18\x02 \x01(\tR\vdescription\x12#\n" +
|
| 1652 |
+
"\rsystem_prompt\x18\x03 \x01(\tR\fsystemPrompt\x12\x14\n" +
|
| 1653 |
+
"\x05tools\x18\x04 \x03(\tR\x05tools\x12+\n" +
|
| 1654 |
+
"\x06config\x18\x05 \x01(\v2\x13.rag.v1.AgentConfigR\x06config\"\x99\x01\n" +
|
| 1655 |
+
"\x05Agent\x12\x0e\n" +
|
| 1656 |
+
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
|
| 1657 |
+
"\x04name\x18\x02 \x01(\tR\x04name\x12 \n" +
|
| 1658 |
+
"\vdescription\x18\x03 \x01(\tR\vdescription\x12\x1d\n" +
|
| 1659 |
+
"\n" +
|
| 1660 |
+
"created_at\x18\x04 \x01(\x03R\tcreatedAt\x12+\n" +
|
| 1661 |
+
"\x06config\x18\x05 \x01(\v2\x13.rag.v1.AgentConfigR\x06config\"\x95\x01\n" +
|
| 1662 |
+
"\vAgentConfig\x12\x14\n" +
|
| 1663 |
+
"\x05model\x18\x01 \x01(\tR\x05model\x12 \n" +
|
| 1664 |
+
"\vtemperature\x18\x02 \x01(\x02R\vtemperature\x12%\n" +
|
| 1665 |
+
"\x0emax_iterations\x18\x03 \x01(\x05R\rmaxIterations\x12'\n" +
|
| 1666 |
+
"\x0ftimeout_seconds\x18\x04 \x01(\x05R\x0etimeoutSeconds\"b\n" +
|
| 1667 |
+
"\rExecutionPlan\x12\x0e\n" +
|
| 1668 |
+
"\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
|
| 1669 |
+
"\x05query\x18\x02 \x01(\tR\x05query\x12+\n" +
|
| 1670 |
+
"\x05steps\x18\x03 \x03(\v2\x15.rag.v1.ExecutionStepR\x05steps\"\xb5\x01\n" +
|
| 1671 |
+
"\rExecutionStep\x12\x0e\n" +
|
| 1672 |
+
"\x02id\x18\x01 \x01(\tR\x02id\x12$\n" +
|
| 1673 |
+
"\x04type\x18\x02 \x01(\x0e2\x10.rag.v1.StepTypeR\x04type\x12 \n" +
|
| 1674 |
+
"\vdescription\x18\x03 \x01(\tR\vdescription\x12\x12\n" +
|
| 1675 |
+
"\x04tool\x18\x04 \x01(\tR\x04tool\x12\x14\n" +
|
| 1676 |
+
"\x05input\x18\x05 \x01(\tR\x05input\x12\"\n" +
|
| 1677 |
+
"\fdependencies\x18\x06 \x03(\tR\fdependencies\"\xdc\x01\n" +
|
| 1678 |
+
"\n" +
|
| 1679 |
+
"PlanResult\x12\x17\n" +
|
| 1680 |
+
"\aplan_id\x18\x01 \x01(\tR\x06planId\x12/\n" +
|
| 1681 |
+
"\x06status\x18\x02 \x01(\x0e2\x17.rag.v1.ExecutionStatusR\x06status\x125\n" +
|
| 1682 |
+
"\fstep_results\x18\x03 \x03(\v2\x12.rag.v1.StepResultR\vstepResults\x12!\n" +
|
| 1683 |
+
"\ffinal_answer\x18\x04 \x01(\tR\vfinalAnswer\x12*\n" +
|
| 1684 |
+
"\x11execution_time_ms\x18\x05 \x01(\x03R\x0fexecutionTimeMs\"\xb0\x01\n" +
|
| 1685 |
+
"\n" +
|
| 1686 |
+
"StepResult\x12\x17\n" +
|
| 1687 |
+
"\astep_id\x18\x01 \x01(\tR\x06stepId\x12/\n" +
|
| 1688 |
+
"\x06status\x18\x02 \x01(\x0e2\x17.rag.v1.ExecutionStatusR\x06status\x12\x16\n" +
|
| 1689 |
+
"\x06output\x18\x03 \x01(\tR\x06output\x12\x14\n" +
|
| 1690 |
+
"\x05error\x18\x04 \x01(\tR\x05error\x12*\n" +
|
| 1691 |
+
"\x11execution_time_ms\x18\x05 \x01(\x03R\x0fexecutionTimeMs\"/\n" +
|
| 1692 |
+
"\x12QueryStatusRequest\x12\x19\n" +
|
| 1693 |
+
"\bquery_id\x18\x01 \x01(\tR\aqueryId\"\x98\x01\n" +
|
| 1694 |
+
"\vQueryStatus\x12\x19\n" +
|
| 1695 |
+
"\bquery_id\x18\x01 \x01(\tR\aqueryId\x12/\n" +
|
| 1696 |
+
"\x06status\x18\x02 \x01(\x0e2\x17.rag.v1.ExecutionStatusR\x06status\x12\x1a\n" +
|
| 1697 |
+
"\bprogress\x18\x03 \x01(\x05R\bprogress\x12!\n" +
|
| 1698 |
+
"\fcurrent_step\x18\x04 \x01(\tR\vcurrentStep\"\xd5\x01\n" +
|
| 1699 |
+
"\x0fSecurityContext\x12\x17\n" +
|
| 1700 |
+
"\auser_id\x18\x01 \x01(\tR\x06userId\x12\x14\n" +
|
| 1701 |
+
"\x05roles\x18\x02 \x03(\tR\x05roles\x12\x1b\n" +
|
| 1702 |
+
"\ttenant_id\x18\x03 \x01(\tR\btenantId\x12;\n" +
|
| 1703 |
+
"\x06claims\x18\x04 \x03(\v2#.rag.v1.SecurityContext.ClaimsEntryR\x06claims\x1a9\n" +
|
| 1704 |
+
"\vClaimsEntry\x12\x10\n" +
|
| 1705 |
+
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
| 1706 |
+
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*\xa4\x01\n" +
|
| 1707 |
+
"\tChunkType\x12\x1a\n" +
|
| 1708 |
+
"\x16CHUNK_TYPE_UNSPECIFIED\x10\x00\x12\x17\n" +
|
| 1709 |
+
"\x13CHUNK_TYPE_THINKING\x10\x01\x12\x18\n" +
|
| 1710 |
+
"\x14CHUNK_TYPE_RETRIEVAL\x10\x02\x12\x19\n" +
|
| 1711 |
+
"\x15CHUNK_TYPE_GENERATION\x10\x03\x12\x17\n" +
|
| 1712 |
+
"\x13CHUNK_TYPE_COMPLETE\x10\x04\x12\x14\n" +
|
| 1713 |
+
"\x10CHUNK_TYPE_ERROR\x10\x05*w\n" +
|
| 1714 |
+
"\vMessageRole\x12\x1c\n" +
|
| 1715 |
+
"\x18MESSAGE_ROLE_UNSPECIFIED\x10\x00\x12\x15\n" +
|
| 1716 |
+
"\x11MESSAGE_ROLE_USER\x10\x01\x12\x1a\n" +
|
| 1717 |
+
"\x16MESSAGE_ROLE_ASSISTANT\x10\x02\x12\x17\n" +
|
| 1718 |
+
"\x13MESSAGE_ROLE_SYSTEM\x10\x03*\x84\x01\n" +
|
| 1719 |
+
"\bStepType\x12\x19\n" +
|
| 1720 |
+
"\x15STEP_TYPE_UNSPECIFIED\x10\x00\x12\x14\n" +
|
| 1721 |
+
"\x10STEP_TYPE_SEARCH\x10\x01\x12\x15\n" +
|
| 1722 |
+
"\x11STEP_TYPE_ANALYZE\x10\x02\x12\x18\n" +
|
| 1723 |
+
"\x14STEP_TYPE_SYNTHESIZE\x10\x03\x12\x16\n" +
|
| 1724 |
+
"\x12STEP_TYPE_VALIDATE\x10\x04*\xac\x01\n" +
|
| 1725 |
+
"\x0fExecutionStatus\x12 \n" +
|
| 1726 |
+
"\x1cEXECUTION_STATUS_UNSPECIFIED\x10\x00\x12\x1c\n" +
|
| 1727 |
+
"\x18EXECUTION_STATUS_PENDING\x10\x01\x12\x1c\n" +
|
| 1728 |
+
"\x18EXECUTION_STATUS_RUNNING\x10\x02\x12\x1e\n" +
|
| 1729 |
+
"\x1aEXECUTION_STATUS_COMPLETED\x10\x03\x12\x1b\n" +
|
| 1730 |
+
"\x17EXECUTION_STATUS_FAILED\x10\x042\xcc\x02\n" +
|
| 1731 |
+
"\fAgentService\x12;\n" +
|
| 1732 |
+
"\fProcessQuery\x12\x14.rag.v1.QueryRequest\x1a\x15.rag.v1.QueryResponse\x12H\n" +
|
| 1733 |
+
"\x12ProcessQueryStream\x12\x14.rag.v1.QueryRequest\x1a\x1a.rag.v1.QueryResponseChunk0\x01\x128\n" +
|
| 1734 |
+
"\vCreateAgent\x12\x1a.rag.v1.CreateAgentRequest\x1a\r.rag.v1.Agent\x128\n" +
|
| 1735 |
+
"\vExecutePlan\x12\x15.rag.v1.ExecutionPlan\x1a\x12.rag.v1.PlanResult\x12A\n" +
|
| 1736 |
+
"\x0eGetQueryStatus\x12\x1a.rag.v1.QueryStatusRequest\x1a\x13.rag.v1.QueryStatusB6Z4github.com/AmaniQuery/amaniquery/pkg/proto/gen/ragv1b\x06proto3"
|
| 1737 |
+
|
| 1738 |
+
var (
|
| 1739 |
+
file_agent_proto_rawDescOnce sync.Once
|
| 1740 |
+
file_agent_proto_rawDescData []byte
|
| 1741 |
+
)
|
| 1742 |
+
|
| 1743 |
+
func file_agent_proto_rawDescGZIP() []byte {
|
| 1744 |
+
file_agent_proto_rawDescOnce.Do(func() {
|
| 1745 |
+
file_agent_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_agent_proto_rawDesc), len(file_agent_proto_rawDesc)))
|
| 1746 |
+
})
|
| 1747 |
+
return file_agent_proto_rawDescData
|
| 1748 |
+
}
|
| 1749 |
+
|
| 1750 |
+
var file_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
|
| 1751 |
+
var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
|
| 1752 |
+
var file_agent_proto_goTypes = []any{
|
| 1753 |
+
(ChunkType)(0), // 0: rag.v1.ChunkType
|
| 1754 |
+
(MessageRole)(0), // 1: rag.v1.MessageRole
|
| 1755 |
+
(StepType)(0), // 2: rag.v1.StepType
|
| 1756 |
+
(ExecutionStatus)(0), // 3: rag.v1.ExecutionStatus
|
| 1757 |
+
(*QueryRequest)(nil), // 4: rag.v1.QueryRequest
|
| 1758 |
+
(*QueryOptions)(nil), // 5: rag.v1.QueryOptions
|
| 1759 |
+
(*QueryResponse)(nil), // 6: rag.v1.QueryResponse
|
| 1760 |
+
(*QueryResponseChunk)(nil), // 7: rag.v1.QueryResponseChunk
|
| 1761 |
+
(*Source)(nil), // 8: rag.v1.Source
|
| 1762 |
+
(*Message)(nil), // 9: rag.v1.Message
|
| 1763 |
+
(*QueryMetadata)(nil), // 10: rag.v1.QueryMetadata
|
| 1764 |
+
(*CreateAgentRequest)(nil), // 11: rag.v1.CreateAgentRequest
|
| 1765 |
+
(*Agent)(nil), // 12: rag.v1.Agent
|
| 1766 |
+
(*AgentConfig)(nil), // 13: rag.v1.AgentConfig
|
| 1767 |
+
(*ExecutionPlan)(nil), // 14: rag.v1.ExecutionPlan
|
| 1768 |
+
(*ExecutionStep)(nil), // 15: rag.v1.ExecutionStep
|
| 1769 |
+
(*PlanResult)(nil), // 16: rag.v1.PlanResult
|
| 1770 |
+
(*StepResult)(nil), // 17: rag.v1.StepResult
|
| 1771 |
+
(*QueryStatusRequest)(nil), // 18: rag.v1.QueryStatusRequest
|
| 1772 |
+
(*QueryStatus)(nil), // 19: rag.v1.QueryStatus
|
| 1773 |
+
(*SecurityContext)(nil), // 20: rag.v1.SecurityContext
|
| 1774 |
+
nil, // 21: rag.v1.QueryRequest.MetadataEntry
|
| 1775 |
+
nil, // 22: rag.v1.Source.MetadataEntry
|
| 1776 |
+
nil, // 23: rag.v1.SecurityContext.ClaimsEntry
|
| 1777 |
+
}
|
| 1778 |
+
var file_agent_proto_depIdxs = []int32{
|
| 1779 |
+
9, // 0: rag.v1.QueryRequest.conversation_history:type_name -> rag.v1.Message
|
| 1780 |
+
21, // 1: rag.v1.QueryRequest.metadata:type_name -> rag.v1.QueryRequest.MetadataEntry
|
| 1781 |
+
5, // 2: rag.v1.QueryRequest.options:type_name -> rag.v1.QueryOptions
|
| 1782 |
+
8, // 3: rag.v1.QueryResponse.sources:type_name -> rag.v1.Source
|
| 1783 |
+
10, // 4: rag.v1.QueryResponse.metadata:type_name -> rag.v1.QueryMetadata
|
| 1784 |
+
0, // 5: rag.v1.QueryResponseChunk.type:type_name -> rag.v1.ChunkType
|
| 1785 |
+
8, // 6: rag.v1.QueryResponseChunk.sources:type_name -> rag.v1.Source
|
| 1786 |
+
22, // 7: rag.v1.Source.metadata:type_name -> rag.v1.Source.MetadataEntry
|
| 1787 |
+
1, // 8: rag.v1.Message.role:type_name -> rag.v1.MessageRole
|
| 1788 |
+
13, // 9: rag.v1.CreateAgentRequest.config:type_name -> rag.v1.AgentConfig
|
| 1789 |
+
13, // 10: rag.v1.Agent.config:type_name -> rag.v1.AgentConfig
|
| 1790 |
+
15, // 11: rag.v1.ExecutionPlan.steps:type_name -> rag.v1.ExecutionStep
|
| 1791 |
+
2, // 12: rag.v1.ExecutionStep.type:type_name -> rag.v1.StepType
|
| 1792 |
+
3, // 13: rag.v1.PlanResult.status:type_name -> rag.v1.ExecutionStatus
|
| 1793 |
+
17, // 14: rag.v1.PlanResult.step_results:type_name -> rag.v1.StepResult
|
| 1794 |
+
3, // 15: rag.v1.StepResult.status:type_name -> rag.v1.ExecutionStatus
|
| 1795 |
+
3, // 16: rag.v1.QueryStatus.status:type_name -> rag.v1.ExecutionStatus
|
| 1796 |
+
23, // 17: rag.v1.SecurityContext.claims:type_name -> rag.v1.SecurityContext.ClaimsEntry
|
| 1797 |
+
4, // 18: rag.v1.AgentService.ProcessQuery:input_type -> rag.v1.QueryRequest
|
| 1798 |
+
4, // 19: rag.v1.AgentService.ProcessQueryStream:input_type -> rag.v1.QueryRequest
|
| 1799 |
+
11, // 20: rag.v1.AgentService.CreateAgent:input_type -> rag.v1.CreateAgentRequest
|
| 1800 |
+
14, // 21: rag.v1.AgentService.ExecutePlan:input_type -> rag.v1.ExecutionPlan
|
| 1801 |
+
18, // 22: rag.v1.AgentService.GetQueryStatus:input_type -> rag.v1.QueryStatusRequest
|
| 1802 |
+
6, // 23: rag.v1.AgentService.ProcessQuery:output_type -> rag.v1.QueryResponse
|
| 1803 |
+
7, // 24: rag.v1.AgentService.ProcessQueryStream:output_type -> rag.v1.QueryResponseChunk
|
| 1804 |
+
12, // 25: rag.v1.AgentService.CreateAgent:output_type -> rag.v1.Agent
|
| 1805 |
+
16, // 26: rag.v1.AgentService.ExecutePlan:output_type -> rag.v1.PlanResult
|
| 1806 |
+
19, // 27: rag.v1.AgentService.GetQueryStatus:output_type -> rag.v1.QueryStatus
|
| 1807 |
+
23, // [23:28] is the sub-list for method output_type
|
| 1808 |
+
18, // [18:23] is the sub-list for method input_type
|
| 1809 |
+
18, // [18:18] is the sub-list for extension type_name
|
| 1810 |
+
18, // [18:18] is the sub-list for extension extendee
|
| 1811 |
+
0, // [0:18] is the sub-list for field type_name
|
| 1812 |
+
}
|
| 1813 |
+
|
| 1814 |
+
func init() { file_agent_proto_init() }
|
| 1815 |
+
func file_agent_proto_init() {
|
| 1816 |
+
if File_agent_proto != nil {
|
| 1817 |
+
return
|
| 1818 |
+
}
|
| 1819 |
+
type x struct{}
|
| 1820 |
+
out := protoimpl.TypeBuilder{
|
| 1821 |
+
File: protoimpl.DescBuilder{
|
| 1822 |
+
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
| 1823 |
+
RawDescriptor: unsafe.Slice(unsafe.StringData(file_agent_proto_rawDesc), len(file_agent_proto_rawDesc)),
|
| 1824 |
+
NumEnums: 4,
|
| 1825 |
+
NumMessages: 20,
|
| 1826 |
+
NumExtensions: 0,
|
| 1827 |
+
NumServices: 1,
|
| 1828 |
+
},
|
| 1829 |
+
GoTypes: file_agent_proto_goTypes,
|
| 1830 |
+
DependencyIndexes: file_agent_proto_depIdxs,
|
| 1831 |
+
EnumInfos: file_agent_proto_enumTypes,
|
| 1832 |
+
MessageInfos: file_agent_proto_msgTypes,
|
| 1833 |
+
}.Build()
|
| 1834 |
+
File_agent_proto = out.File
|
| 1835 |
+
file_agent_proto_goTypes = nil
|
| 1836 |
+
file_agent_proto_depIdxs = nil
|
| 1837 |
+
}
|
agent_grpc.pb.go
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
| 2 |
+
// versions:
|
| 3 |
+
// - protoc-gen-go-grpc v1.6.0
|
| 4 |
+
// - protoc v6.33.2
|
| 5 |
+
// source: agent.proto
|
| 6 |
+
|
| 7 |
+
package ragv1
|
| 8 |
+
|
| 9 |
+
import (
|
| 10 |
+
context "context"
|
| 11 |
+
grpc "google.golang.org/grpc"
|
| 12 |
+
codes "google.golang.org/grpc/codes"
|
| 13 |
+
status "google.golang.org/grpc/status"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
// This is a compile-time assertion to ensure that this generated file
|
| 17 |
+
// is compatible with the grpc package it is being compiled against.
|
| 18 |
+
// Requires gRPC-Go v1.64.0 or later.
|
| 19 |
+
const _ = grpc.SupportPackageIsVersion9
|
| 20 |
+
|
| 21 |
+
const (
|
| 22 |
+
AgentService_ProcessQuery_FullMethodName = "/rag.v1.AgentService/ProcessQuery"
|
| 23 |
+
AgentService_ProcessQueryStream_FullMethodName = "/rag.v1.AgentService/ProcessQueryStream"
|
| 24 |
+
AgentService_CreateAgent_FullMethodName = "/rag.v1.AgentService/CreateAgent"
|
| 25 |
+
AgentService_ExecutePlan_FullMethodName = "/rag.v1.AgentService/ExecutePlan"
|
| 26 |
+
AgentService_GetQueryStatus_FullMethodName = "/rag.v1.AgentService/GetQueryStatus"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
// AgentServiceClient is the client API for AgentService service.
|
| 30 |
+
//
|
| 31 |
+
// 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.
|
| 32 |
+
//
|
| 33 |
+
// AgentService handles query processing and orchestration
|
| 34 |
+
type AgentServiceClient interface {
|
| 35 |
+
// Process a single query and return a complete response
|
| 36 |
+
ProcessQuery(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*QueryResponse, error)
|
| 37 |
+
// Process a query with streaming response for real-time updates
|
| 38 |
+
ProcessQueryStream(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[QueryResponseChunk], error)
|
| 39 |
+
// Create a new agent with specific configuration
|
| 40 |
+
CreateAgent(ctx context.Context, in *CreateAgentRequest, opts ...grpc.CallOption) (*Agent, error)
|
| 41 |
+
// Execute a pre-defined execution plan
|
| 42 |
+
ExecutePlan(ctx context.Context, in *ExecutionPlan, opts ...grpc.CallOption) (*PlanResult, error)
|
| 43 |
+
// Get the status of an ongoing query
|
| 44 |
+
GetQueryStatus(ctx context.Context, in *QueryStatusRequest, opts ...grpc.CallOption) (*QueryStatus, error)
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
type agentServiceClient struct {
|
| 48 |
+
cc grpc.ClientConnInterface
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
func NewAgentServiceClient(cc grpc.ClientConnInterface) AgentServiceClient {
|
| 52 |
+
return &agentServiceClient{cc}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
func (c *agentServiceClient) ProcessQuery(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*QueryResponse, error) {
|
| 56 |
+
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
| 57 |
+
out := new(QueryResponse)
|
| 58 |
+
err := c.cc.Invoke(ctx, AgentService_ProcessQuery_FullMethodName, in, out, cOpts...)
|
| 59 |
+
if err != nil {
|
| 60 |
+
return nil, err
|
| 61 |
+
}
|
| 62 |
+
return out, nil
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
func (c *agentServiceClient) ProcessQueryStream(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[QueryResponseChunk], error) {
|
| 66 |
+
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
| 67 |
+
stream, err := c.cc.NewStream(ctx, &AgentService_ServiceDesc.Streams[0], AgentService_ProcessQueryStream_FullMethodName, cOpts...)
|
| 68 |
+
if err != nil {
|
| 69 |
+
return nil, err
|
| 70 |
+
}
|
| 71 |
+
x := &grpc.GenericClientStream[QueryRequest, QueryResponseChunk]{ClientStream: stream}
|
| 72 |
+
if err := x.ClientStream.SendMsg(in); err != nil {
|
| 73 |
+
return nil, err
|
| 74 |
+
}
|
| 75 |
+
if err := x.ClientStream.CloseSend(); err != nil {
|
| 76 |
+
return nil, err
|
| 77 |
+
}
|
| 78 |
+
return x, nil
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
| 82 |
+
type AgentService_ProcessQueryStreamClient = grpc.ServerStreamingClient[QueryResponseChunk]
|
| 83 |
+
|
| 84 |
+
func (c *agentServiceClient) CreateAgent(ctx context.Context, in *CreateAgentRequest, opts ...grpc.CallOption) (*Agent, error) {
|
| 85 |
+
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
| 86 |
+
out := new(Agent)
|
| 87 |
+
err := c.cc.Invoke(ctx, AgentService_CreateAgent_FullMethodName, in, out, cOpts...)
|
| 88 |
+
if err != nil {
|
| 89 |
+
return nil, err
|
| 90 |
+
}
|
| 91 |
+
return out, nil
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
func (c *agentServiceClient) ExecutePlan(ctx context.Context, in *ExecutionPlan, opts ...grpc.CallOption) (*PlanResult, error) {
|
| 95 |
+
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
| 96 |
+
out := new(PlanResult)
|
| 97 |
+
err := c.cc.Invoke(ctx, AgentService_ExecutePlan_FullMethodName, in, out, cOpts...)
|
| 98 |
+
if err != nil {
|
| 99 |
+
return nil, err
|
| 100 |
+
}
|
| 101 |
+
return out, nil
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
func (c *agentServiceClient) GetQueryStatus(ctx context.Context, in *QueryStatusRequest, opts ...grpc.CallOption) (*QueryStatus, error) {
|
| 105 |
+
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
| 106 |
+
out := new(QueryStatus)
|
| 107 |
+
err := c.cc.Invoke(ctx, AgentService_GetQueryStatus_FullMethodName, in, out, cOpts...)
|
| 108 |
+
if err != nil {
|
| 109 |
+
return nil, err
|
| 110 |
+
}
|
| 111 |
+
return out, nil
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// AgentServiceServer is the server API for AgentService service.
|
| 115 |
+
// All implementations must embed UnimplementedAgentServiceServer
|
| 116 |
+
// for forward compatibility.
|
| 117 |
+
//
|
| 118 |
+
// AgentService handles query processing and orchestration
|
| 119 |
+
type AgentServiceServer interface {
|
| 120 |
+
// Process a single query and return a complete response
|
| 121 |
+
ProcessQuery(context.Context, *QueryRequest) (*QueryResponse, error)
|
| 122 |
+
// Process a query with streaming response for real-time updates
|
| 123 |
+
ProcessQueryStream(*QueryRequest, grpc.ServerStreamingServer[QueryResponseChunk]) error
|
| 124 |
+
// Create a new agent with specific configuration
|
| 125 |
+
CreateAgent(context.Context, *CreateAgentRequest) (*Agent, error)
|
| 126 |
+
// Execute a pre-defined execution plan
|
| 127 |
+
ExecutePlan(context.Context, *ExecutionPlan) (*PlanResult, error)
|
| 128 |
+
// Get the status of an ongoing query
|
| 129 |
+
GetQueryStatus(context.Context, *QueryStatusRequest) (*QueryStatus, error)
|
| 130 |
+
mustEmbedUnimplementedAgentServiceServer()
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// UnimplementedAgentServiceServer must be embedded to have
|
| 134 |
+
// forward compatible implementations.
|
| 135 |
+
//
|
| 136 |
+
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
| 137 |
+
// pointer dereference when methods are called.
|
| 138 |
+
type UnimplementedAgentServiceServer struct{}
|
| 139 |
+
|
| 140 |
+
func (UnimplementedAgentServiceServer) ProcessQuery(context.Context, *QueryRequest) (*QueryResponse, error) {
|
| 141 |
+
return nil, status.Error(codes.Unimplemented, "method ProcessQuery not implemented")
|
| 142 |
+
}
|
| 143 |
+
func (UnimplementedAgentServiceServer) ProcessQueryStream(*QueryRequest, grpc.ServerStreamingServer[QueryResponseChunk]) error {
|
| 144 |
+
return status.Error(codes.Unimplemented, "method ProcessQueryStream not implemented")
|
| 145 |
+
}
|
| 146 |
+
func (UnimplementedAgentServiceServer) CreateAgent(context.Context, *CreateAgentRequest) (*Agent, error) {
|
| 147 |
+
return nil, status.Error(codes.Unimplemented, "method CreateAgent not implemented")
|
| 148 |
+
}
|
| 149 |
+
func (UnimplementedAgentServiceServer) ExecutePlan(context.Context, *ExecutionPlan) (*PlanResult, error) {
|
| 150 |
+
return nil, status.Error(codes.Unimplemented, "method ExecutePlan not implemented")
|
| 151 |
+
}
|
| 152 |
+
func (UnimplementedAgentServiceServer) GetQueryStatus(context.Context, *QueryStatusRequest) (*QueryStatus, error) {
|
| 153 |
+
return nil, status.Error(codes.Unimplemented, "method GetQueryStatus not implemented")
|
| 154 |
+
}
|
| 155 |
+
func (UnimplementedAgentServiceServer) mustEmbedUnimplementedAgentServiceServer() {}
|
| 156 |
+
func (UnimplementedAgentServiceServer) testEmbeddedByValue() {}
|
| 157 |
+
|
| 158 |
+
// UnsafeAgentServiceServer may be embedded to opt out of forward compatibility for this service.
|
| 159 |
+
// Use of this interface is not recommended, as added methods to AgentServiceServer will
|
| 160 |
+
// result in compilation errors.
|
| 161 |
+
type UnsafeAgentServiceServer interface {
|
| 162 |
+
mustEmbedUnimplementedAgentServiceServer()
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
func RegisterAgentServiceServer(s grpc.ServiceRegistrar, srv AgentServiceServer) {
|
| 166 |
+
// If the following call panics, it indicates UnimplementedAgentServiceServer was
|
| 167 |
+
// embedded by pointer and is nil. This will cause panics if an
|
| 168 |
+
// unimplemented method is ever invoked, so we test this at initialization
|
| 169 |
+
// time to prevent it from happening at runtime later due to I/O.
|
| 170 |
+
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
| 171 |
+
t.testEmbeddedByValue()
|
| 172 |
+
}
|
| 173 |
+
s.RegisterService(&AgentService_ServiceDesc, srv)
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
func _AgentService_ProcessQuery_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
| 177 |
+
in := new(QueryRequest)
|
| 178 |
+
if err := dec(in); err != nil {
|
| 179 |
+
return nil, err
|
| 180 |
+
}
|
| 181 |
+
if interceptor == nil {
|
| 182 |
+
return srv.(AgentServiceServer).ProcessQuery(ctx, in)
|
| 183 |
+
}
|
| 184 |
+
info := &grpc.UnaryServerInfo{
|
| 185 |
+
Server: srv,
|
| 186 |
+
FullMethod: AgentService_ProcessQuery_FullMethodName,
|
| 187 |
+
}
|
| 188 |
+
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
| 189 |
+
return srv.(AgentServiceServer).ProcessQuery(ctx, req.(*QueryRequest))
|
| 190 |
+
}
|
| 191 |
+
return interceptor(ctx, in, info, handler)
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
func _AgentService_ProcessQueryStream_Handler(srv interface{}, stream grpc.ServerStream) error {
|
| 195 |
+
m := new(QueryRequest)
|
| 196 |
+
if err := stream.RecvMsg(m); err != nil {
|
| 197 |
+
return err
|
| 198 |
+
}
|
| 199 |
+
return srv.(AgentServiceServer).ProcessQueryStream(m, &grpc.GenericServerStream[QueryRequest, QueryResponseChunk]{ServerStream: stream})
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
| 203 |
+
type AgentService_ProcessQueryStreamServer = grpc.ServerStreamingServer[QueryResponseChunk]
|
| 204 |
+
|
| 205 |
+
func _AgentService_CreateAgent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
| 206 |
+
in := new(CreateAgentRequest)
|
| 207 |
+
if err := dec(in); err != nil {
|
| 208 |
+
return nil, err
|
| 209 |
+
}
|
| 210 |
+
if interceptor == nil {
|
| 211 |
+
return srv.(AgentServiceServer).CreateAgent(ctx, in)
|
| 212 |
+
}
|
| 213 |
+
info := &grpc.UnaryServerInfo{
|
| 214 |
+
Server: srv,
|
| 215 |
+
FullMethod: AgentService_CreateAgent_FullMethodName,
|
| 216 |
+
}
|
| 217 |
+
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
| 218 |
+
return srv.(AgentServiceServer).CreateAgent(ctx, req.(*CreateAgentRequest))
|
| 219 |
+
}
|
| 220 |
+
return interceptor(ctx, in, info, handler)
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
func _AgentService_ExecutePlan_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
| 224 |
+
in := new(ExecutionPlan)
|
| 225 |
+
if err := dec(in); err != nil {
|
| 226 |
+
return nil, err
|
| 227 |
+
}
|
| 228 |
+
if interceptor == nil {
|
| 229 |
+
return srv.(AgentServiceServer).ExecutePlan(ctx, in)
|
| 230 |
+
}
|
| 231 |
+
info := &grpc.UnaryServerInfo{
|
| 232 |
+
Server: srv,
|
| 233 |
+
FullMethod: AgentService_ExecutePlan_FullMethodName,
|
| 234 |
+
}
|
| 235 |
+
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
| 236 |
+
return srv.(AgentServiceServer).ExecutePlan(ctx, req.(*ExecutionPlan))
|
| 237 |
+
}
|
| 238 |
+
return interceptor(ctx, in, info, handler)
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
func _AgentService_GetQueryStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
| 242 |
+
in := new(QueryStatusRequest)
|
| 243 |
+
if err := dec(in); err != nil {
|
| 244 |
+
return nil, err
|
| 245 |
+
}
|
| 246 |
+
if interceptor == nil {
|
| 247 |
+
return srv.(AgentServiceServer).GetQueryStatus(ctx, in)
|
| 248 |
+
}
|
| 249 |
+
info := &grpc.UnaryServerInfo{
|
| 250 |
+
Server: srv,
|
| 251 |
+
FullMethod: AgentService_GetQueryStatus_FullMethodName,
|
| 252 |
+
}
|
| 253 |
+
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
| 254 |
+
return srv.(AgentServiceServer).GetQueryStatus(ctx, req.(*QueryStatusRequest))
|
| 255 |
+
}
|
| 256 |
+
return interceptor(ctx, in, info, handler)
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// AgentService_ServiceDesc is the grpc.ServiceDesc for AgentService service.
|
| 260 |
+
// It's only intended for direct use with grpc.RegisterService,
|
| 261 |
+
// and not to be introspected or modified (even as a copy)
|
| 262 |
+
var AgentService_ServiceDesc = grpc.ServiceDesc{
|
| 263 |
+
ServiceName: "rag.v1.AgentService",
|
| 264 |
+
HandlerType: (*AgentServiceServer)(nil),
|
| 265 |
+
Methods: []grpc.MethodDesc{
|
| 266 |
+
{
|
| 267 |
+
MethodName: "ProcessQuery",
|
| 268 |
+
Handler: _AgentService_ProcessQuery_Handler,
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
MethodName: "CreateAgent",
|
| 272 |
+
Handler: _AgentService_CreateAgent_Handler,
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
MethodName: "ExecutePlan",
|
| 276 |
+
Handler: _AgentService_ExecutePlan_Handler,
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
MethodName: "GetQueryStatus",
|
| 280 |
+
Handler: _AgentService_GetQueryStatus_Handler,
|
| 281 |
+
},
|
| 282 |
+
},
|
| 283 |
+
Streams: []grpc.StreamDesc{
|
| 284 |
+
{
|
| 285 |
+
StreamName: "ProcessQueryStream",
|
| 286 |
+
Handler: _AgentService_ProcessQueryStream_Handler,
|
| 287 |
+
ServerStreams: true,
|
| 288 |
+
},
|
| 289 |
+
},
|
| 290 |
+
Metadata: "agent.proto",
|
| 291 |
+
}
|
cmd/agent-server/main.go
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package main provides the entry point for the Agent Orchestrator service.
|
| 2 |
+
// This is the primary service that coordinates query processing and orchestrates
|
| 3 |
+
// interactions between the Retriever and Generator services.
|
| 4 |
+
package main
|
| 5 |
+
|
| 6 |
+
import (
|
| 7 |
+
"context"
|
| 8 |
+
"fmt"
|
| 9 |
+
"net"
|
| 10 |
+
"os"
|
| 11 |
+
"os/signal"
|
| 12 |
+
"syscall"
|
| 13 |
+
"time"
|
| 14 |
+
|
| 15 |
+
"github.com/AmaniQuery/amaniquery/internal/agent"
|
| 16 |
+
"github.com/AmaniQuery/amaniquery/internal/cache"
|
| 17 |
+
"github.com/AmaniQuery/amaniquery/internal/generator"
|
| 18 |
+
"github.com/AmaniQuery/amaniquery/internal/generator/llm"
|
| 19 |
+
"github.com/AmaniQuery/amaniquery/internal/retriever"
|
| 20 |
+
"github.com/AmaniQuery/amaniquery/internal/retriever/keyword"
|
| 21 |
+
"github.com/AmaniQuery/amaniquery/internal/retriever/vector"
|
| 22 |
+
"github.com/AmaniQuery/amaniquery/internal/router"
|
| 23 |
+
"github.com/AmaniQuery/amaniquery/pkg/config"
|
| 24 |
+
"github.com/AmaniQuery/amaniquery/pkg/observability"
|
| 25 |
+
|
| 26 |
+
"go.uber.org/zap"
|
| 27 |
+
"google.golang.org/grpc"
|
| 28 |
+
"google.golang.org/grpc/health"
|
| 29 |
+
"google.golang.org/grpc/health/grpc_health_v1"
|
| 30 |
+
"google.golang.org/grpc/reflection"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
func main() {
|
| 34 |
+
// Load configuration
|
| 35 |
+
cfg, err := config.Load()
|
| 36 |
+
if err != nil {
|
| 37 |
+
fmt.Fprintf(os.Stderr, "failed to load configuration: %v\n", err)
|
| 38 |
+
os.Exit(1)
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Initialize logger
|
| 42 |
+
logger, err := observability.NewLogger(cfg.Observability.LogLevel, cfg.Observability.LogFormat)
|
| 43 |
+
if err != nil {
|
| 44 |
+
fmt.Fprintf(os.Stderr, "failed to initialize logger: %v\n", err)
|
| 45 |
+
os.Exit(1)
|
| 46 |
+
}
|
| 47 |
+
defer logger.Sync()
|
| 48 |
+
|
| 49 |
+
logger.Info("starting AmaniQuery agent server",
|
| 50 |
+
zap.String("version", cfg.Version),
|
| 51 |
+
zap.String("environment", cfg.Environment),
|
| 52 |
+
zap.Strings("llm_providers", cfg.LLM.GetConfiguredProviders()),
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
// Initialize observability (tracing + metrics)
|
| 56 |
+
tracingShutdown, err := observability.InitProvider(observability.Config{
|
| 57 |
+
ServiceName: "amaniquery-agent",
|
| 58 |
+
ServiceVersion: cfg.Version,
|
| 59 |
+
TracingEnabled: cfg.Observability.TracingEnabled,
|
| 60 |
+
TracingEndpoint: cfg.Observability.TracingEndpoint,
|
| 61 |
+
MetricsEnabled: cfg.Observability.MetricsEnabled,
|
| 62 |
+
MetricsPort: cfg.Observability.MetricsPort,
|
| 63 |
+
})
|
| 64 |
+
if err != nil {
|
| 65 |
+
logger.Warn("failed to initialize tracing, continuing without", zap.Error(err))
|
| 66 |
+
} else {
|
| 67 |
+
defer tracingShutdown(context.Background())
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Start metrics server
|
| 71 |
+
if cfg.Observability.MetricsEnabled {
|
| 72 |
+
metricsServer := observability.StartMetricsServer(cfg.Observability.MetricsPort)
|
| 73 |
+
defer metricsServer.Shutdown(context.Background())
|
| 74 |
+
logger.Info("metrics server started", zap.Int("port", cfg.Observability.MetricsPort))
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Build dependencies
|
| 78 |
+
deps, err := buildDependencies(cfg, logger)
|
| 79 |
+
if err != nil {
|
| 80 |
+
logger.Fatal("failed to build dependencies", zap.Error(err))
|
| 81 |
+
}
|
| 82 |
+
defer deps.Close()
|
| 83 |
+
|
| 84 |
+
// Create gRPC server with interceptors
|
| 85 |
+
grpcServer := grpc.NewServer(
|
| 86 |
+
grpc.ChainUnaryInterceptor(
|
| 87 |
+
observability.UnaryServerInterceptor(),
|
| 88 |
+
agent.LoggingInterceptor(logger),
|
| 89 |
+
agent.RecoveryInterceptor(),
|
| 90 |
+
),
|
| 91 |
+
grpc.ChainStreamInterceptor(
|
| 92 |
+
observability.StreamServerInterceptor(),
|
| 93 |
+
),
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
// Register Agent service
|
| 97 |
+
agentServer := agent.NewServer(deps, logger)
|
| 98 |
+
agent.RegisterAgentServiceServer(grpcServer, agentServer)
|
| 99 |
+
|
| 100 |
+
// Register health service
|
| 101 |
+
healthServer := health.NewServer()
|
| 102 |
+
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
| 103 |
+
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
| 104 |
+
|
| 105 |
+
// Enable reflection for grpcurl
|
| 106 |
+
reflection.Register(grpcServer)
|
| 107 |
+
|
| 108 |
+
// Start gRPC server
|
| 109 |
+
addr := fmt.Sprintf(":%d", cfg.Server.GRPCPort)
|
| 110 |
+
listener, err := net.Listen("tcp", addr)
|
| 111 |
+
if err != nil {
|
| 112 |
+
logger.Fatal("failed to listen", zap.String("addr", addr), zap.Error(err))
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
logger.Info("gRPC server starting",
|
| 116 |
+
zap.String("addr", addr),
|
| 117 |
+
zap.Int("http_port", cfg.Server.HTTPPort),
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
// Graceful shutdown handling
|
| 121 |
+
errChan := make(chan error, 1)
|
| 122 |
+
go func() {
|
| 123 |
+
errChan <- grpcServer.Serve(listener)
|
| 124 |
+
}()
|
| 125 |
+
|
| 126 |
+
// Wait for shutdown signal
|
| 127 |
+
quit := make(chan os.Signal, 1)
|
| 128 |
+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
| 129 |
+
|
| 130 |
+
select {
|
| 131 |
+
case err := <-errChan:
|
| 132 |
+
logger.Fatal("server error", zap.Error(err))
|
| 133 |
+
case sig := <-quit:
|
| 134 |
+
logger.Info("shutting down", zap.String("signal", sig.String()))
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Graceful shutdown with timeout
|
| 138 |
+
ctx, cancel := context.WithTimeout(context.Background(), cfg.Server.GracefulTimeout)
|
| 139 |
+
defer cancel()
|
| 140 |
+
|
| 141 |
+
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
|
| 142 |
+
grpcServer.GracefulStop()
|
| 143 |
+
|
| 144 |
+
logger.Info("server stopped gracefully")
|
| 145 |
+
_ = ctx // used for cleanup operations
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// buildDependencies initializes all service dependencies
|
| 149 |
+
func buildDependencies(cfg *config.Config, logger *zap.Logger) (*agent.Dependencies, error) {
|
| 150 |
+
deps := &agent.Dependencies{}
|
| 151 |
+
|
| 152 |
+
// Initialize vector store client (Qdrant)
|
| 153 |
+
logger.Info("connecting to vector store",
|
| 154 |
+
zap.String("type", cfg.VectorStore.Type),
|
| 155 |
+
zap.String("host", cfg.VectorStore.Host),
|
| 156 |
+
zap.Int("port", cfg.VectorStore.Port),
|
| 157 |
+
)
|
| 158 |
+
vectorClient, err := vector.NewQdrantClient(vector.Config{
|
| 159 |
+
Host: cfg.VectorStore.Host,
|
| 160 |
+
Port: cfg.VectorStore.Port,
|
| 161 |
+
APIKey: cfg.VectorStore.APIKey,
|
| 162 |
+
Collection: cfg.VectorStore.Collection,
|
| 163 |
+
Dimension: cfg.VectorStore.Dimension,
|
| 164 |
+
Distance: cfg.VectorStore.Distance,
|
| 165 |
+
})
|
| 166 |
+
if err != nil {
|
| 167 |
+
logger.Warn("failed to connect to vector store, continuing without", zap.Error(err))
|
| 168 |
+
} else {
|
| 169 |
+
deps.VectorStore = vectorClient
|
| 170 |
+
logger.Info("connected to vector store")
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Initialize cache
|
| 174 |
+
logger.Info("connecting to cache", zap.String("url", cfg.Cache.RedisURL))
|
| 175 |
+
cacheClient, err := cache.New(cache.Config{
|
| 176 |
+
RedisURL: cfg.Cache.RedisURL,
|
| 177 |
+
LocalSize: cfg.Cache.LocalSize,
|
| 178 |
+
TTL: cfg.Cache.TTL,
|
| 179 |
+
MaxRetries: cfg.Cache.MaxRetries,
|
| 180 |
+
PoolSize: cfg.Cache.PoolSize,
|
| 181 |
+
})
|
| 182 |
+
if err != nil {
|
| 183 |
+
logger.Warn("failed to initialize cache, continuing without", zap.Error(err))
|
| 184 |
+
} else {
|
| 185 |
+
deps.Cache = cacheClient
|
| 186 |
+
logger.Info("cache initialized")
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// Initialize embedding client
|
| 190 |
+
logger.Info("initializing embedding client",
|
| 191 |
+
zap.String("provider", cfg.Embedding.Provider),
|
| 192 |
+
zap.String("model", cfg.Embedding.Model),
|
| 193 |
+
)
|
| 194 |
+
embeddingClient := generator.NewOpenAIEmbeddingClient(generator.EmbeddingConfig{
|
| 195 |
+
Provider: cfg.Embedding.Provider,
|
| 196 |
+
APIKey: cfg.Embedding.APIKey,
|
| 197 |
+
Model: cfg.Embedding.Model,
|
| 198 |
+
Dimension: cfg.Embedding.Dimension,
|
| 199 |
+
BatchSize: cfg.Embedding.BatchSize,
|
| 200 |
+
Timeout: 30 * time.Second,
|
| 201 |
+
MaxRetries: 3,
|
| 202 |
+
})
|
| 203 |
+
deps.EmbeddingClient = embeddingClient
|
| 204 |
+
logger.Info("embedding client initialized")
|
| 205 |
+
|
| 206 |
+
// Initialize multi-provider LLM client with fallback
|
| 207 |
+
// Fallback order: Gemini → Moonshot → Ollama → OpenAI → Anthropic
|
| 208 |
+
logger.Info("initializing LLM client with fallback",
|
| 209 |
+
zap.Strings("providers", cfg.LLM.GetConfiguredProviders()),
|
| 210 |
+
)
|
| 211 |
+
llmClient := llm.NewFallbackClient(llm.Config{
|
| 212 |
+
GeminiAPIKey: cfg.LLM.GeminiAPIKey,
|
| 213 |
+
MoonshotAPIKey: cfg.LLM.MoonshotAPIKey,
|
| 214 |
+
OllamaBaseURL: cfg.LLM.OllamaBaseURL,
|
| 215 |
+
OpenAIAPIKey: cfg.LLM.OpenAIAPIKey,
|
| 216 |
+
AnthropicAPIKey: cfg.LLM.AnthropicAPIKey,
|
| 217 |
+
DefaultModel: cfg.LLM.DefaultModel,
|
| 218 |
+
MaxTokens: cfg.LLM.MaxTokens,
|
| 219 |
+
Temperature: cfg.LLM.Temperature,
|
| 220 |
+
Timeout: cfg.LLM.Timeout,
|
| 221 |
+
MaxRetries: cfg.LLM.MaxRetries,
|
| 222 |
+
EnableFallback: cfg.LLM.EnableFallback,
|
| 223 |
+
Logger: logger,
|
| 224 |
+
})
|
| 225 |
+
deps.LLMClient = llmClient
|
| 226 |
+
logger.Info("LLM client initialized",
|
| 227 |
+
zap.Int("provider_count", len(llmClient.GetAvailableProviders())),
|
| 228 |
+
zap.Strings("available_providers", toStringSlice(llmClient.GetAvailableProviders())),
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
// Initialize keyword search engine
|
| 232 |
+
logger.Info("initializing keyword search engine")
|
| 233 |
+
keywordEngine, err := keyword.NewBleveEngine(keyword.Config{
|
| 234 |
+
InMemory: true, // Use in-memory for development
|
| 235 |
+
})
|
| 236 |
+
if err != nil {
|
| 237 |
+
logger.Warn("failed to initialize keyword engine, continuing without", zap.Error(err))
|
| 238 |
+
} else {
|
| 239 |
+
deps.KeywordEngine = keywordEngine
|
| 240 |
+
logger.Info("keyword search engine initialized")
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
// Initialize query router
|
| 244 |
+
queryRouter := router.NewRouter(router.DefaultRouterConfig())
|
| 245 |
+
deps.Router = queryRouter
|
| 246 |
+
logger.Info("query router initialized")
|
| 247 |
+
|
| 248 |
+
// Initialize hybrid retriever
|
| 249 |
+
if deps.VectorStore != nil || deps.KeywordEngine != nil {
|
| 250 |
+
hybridRetriever := retriever.NewHybridRetriever(
|
| 251 |
+
deps.VectorStore,
|
| 252 |
+
deps.KeywordEngine,
|
| 253 |
+
deps.EmbeddingClient,
|
| 254 |
+
retriever.DefaultConfig(),
|
| 255 |
+
)
|
| 256 |
+
deps.Retriever = hybridRetriever
|
| 257 |
+
logger.Info("hybrid retriever initialized")
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
return deps, nil
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
// toStringSlice converts Provider slice to string slice for logging
|
| 264 |
+
func toStringSlice(providers []llm.Provider) []string {
|
| 265 |
+
result := make([]string, len(providers))
|
| 266 |
+
for i, p := range providers {
|
| 267 |
+
result[i] = string(p)
|
| 268 |
+
}
|
| 269 |
+
return result
|
| 270 |
+
}
|
cmd/generator-server/main.go
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package main provides the entry point for the Generator service.
|
| 2 |
+
// This service handles LLM interactions for text generation and embeddings.
|
| 3 |
+
package main
|
| 4 |
+
|
| 5 |
+
import (
|
| 6 |
+
"context"
|
| 7 |
+
"fmt"
|
| 8 |
+
"net"
|
| 9 |
+
"os"
|
| 10 |
+
"os/signal"
|
| 11 |
+
"syscall"
|
| 12 |
+
|
| 13 |
+
"github.com/AmaniQuery/amaniquery/internal/generator"
|
| 14 |
+
"github.com/AmaniQuery/amaniquery/internal/generator/llm"
|
| 15 |
+
"github.com/AmaniQuery/amaniquery/pkg/config"
|
| 16 |
+
"github.com/AmaniQuery/amaniquery/pkg/observability"
|
| 17 |
+
|
| 18 |
+
"go.uber.org/zap"
|
| 19 |
+
"google.golang.org/grpc"
|
| 20 |
+
"google.golang.org/grpc/codes"
|
| 21 |
+
"google.golang.org/grpc/health"
|
| 22 |
+
"google.golang.org/grpc/health/grpc_health_v1"
|
| 23 |
+
"google.golang.org/grpc/reflection"
|
| 24 |
+
"google.golang.org/grpc/status"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
func main() {
|
| 28 |
+
// Load configuration
|
| 29 |
+
cfg, err := config.Load()
|
| 30 |
+
if err != nil {
|
| 31 |
+
fmt.Fprintf(os.Stderr, "failed to load configuration: %v\n", err)
|
| 32 |
+
os.Exit(1)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Initialize logger
|
| 36 |
+
logger, err := observability.NewLogger(cfg.Observability.LogLevel, cfg.Observability.LogFormat)
|
| 37 |
+
if err != nil {
|
| 38 |
+
fmt.Fprintf(os.Stderr, "failed to initialize logger: %v\n", err)
|
| 39 |
+
os.Exit(1)
|
| 40 |
+
}
|
| 41 |
+
defer logger.Sync()
|
| 42 |
+
|
| 43 |
+
logger.Info("starting AmaniQuery generator server",
|
| 44 |
+
zap.String("version", cfg.Version),
|
| 45 |
+
zap.Strings("llm_providers", cfg.LLM.GetConfiguredProviders()),
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
// Initialize observability
|
| 49 |
+
tracingShutdown, err := observability.InitProvider(observability.Config{
|
| 50 |
+
ServiceName: "amaniquery-generator",
|
| 51 |
+
ServiceVersion: cfg.Version,
|
| 52 |
+
TracingEnabled: cfg.Observability.TracingEnabled,
|
| 53 |
+
TracingEndpoint: cfg.Observability.TracingEndpoint,
|
| 54 |
+
})
|
| 55 |
+
if err != nil {
|
| 56 |
+
logger.Warn("failed to initialize tracing", zap.Error(err))
|
| 57 |
+
} else {
|
| 58 |
+
defer tracingShutdown(context.Background())
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Build dependencies
|
| 62 |
+
deps, err := buildGeneratorDependencies(cfg, logger)
|
| 63 |
+
if err != nil {
|
| 64 |
+
logger.Fatal("failed to build dependencies", zap.Error(err))
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Create gRPC server
|
| 68 |
+
grpcServer := grpc.NewServer(
|
| 69 |
+
grpc.ChainUnaryInterceptor(
|
| 70 |
+
observability.UnaryServerInterceptor(),
|
| 71 |
+
),
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
// Register generator service
|
| 75 |
+
generatorServer := NewGeneratorServer(deps, logger)
|
| 76 |
+
RegisterGeneratorServiceServer(grpcServer, generatorServer)
|
| 77 |
+
|
| 78 |
+
// Register health service
|
| 79 |
+
healthServer := health.NewServer()
|
| 80 |
+
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
| 81 |
+
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
| 82 |
+
|
| 83 |
+
// Enable reflection
|
| 84 |
+
reflection.Register(grpcServer)
|
| 85 |
+
|
| 86 |
+
// Start server
|
| 87 |
+
port := 9092 // Different port from other servers
|
| 88 |
+
addr := fmt.Sprintf(":%d", port)
|
| 89 |
+
listener, err := net.Listen("tcp", addr)
|
| 90 |
+
if err != nil {
|
| 91 |
+
logger.Fatal("failed to listen", zap.String("addr", addr), zap.Error(err))
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
logger.Info("gRPC generator server starting", zap.String("addr", addr))
|
| 95 |
+
|
| 96 |
+
// Graceful shutdown
|
| 97 |
+
errChan := make(chan error, 1)
|
| 98 |
+
go func() {
|
| 99 |
+
errChan <- grpcServer.Serve(listener)
|
| 100 |
+
}()
|
| 101 |
+
|
| 102 |
+
quit := make(chan os.Signal, 1)
|
| 103 |
+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
| 104 |
+
|
| 105 |
+
select {
|
| 106 |
+
case err := <-errChan:
|
| 107 |
+
logger.Fatal("server error", zap.Error(err))
|
| 108 |
+
case sig := <-quit:
|
| 109 |
+
logger.Info("shutting down", zap.String("signal", sig.String()))
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
grpcServer.GracefulStop()
|
| 113 |
+
logger.Info("generator server stopped")
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// GeneratorDependencies holds generator service dependencies
|
| 117 |
+
type GeneratorDependencies struct {
|
| 118 |
+
LLMClient *llm.FallbackClient
|
| 119 |
+
EmbeddingClient *generator.OpenAIEmbeddingClient
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
func buildGeneratorDependencies(cfg *config.Config, logger *zap.Logger) (*GeneratorDependencies, error) {
|
| 123 |
+
// Initialize LLM client with fallback
|
| 124 |
+
llmClient := llm.NewFallbackClient(llm.Config{
|
| 125 |
+
GeminiAPIKey: cfg.LLM.GeminiAPIKey,
|
| 126 |
+
MoonshotAPIKey: cfg.LLM.MoonshotAPIKey,
|
| 127 |
+
OllamaBaseURL: cfg.LLM.OllamaBaseURL,
|
| 128 |
+
OpenAIAPIKey: cfg.LLM.OpenAIAPIKey,
|
| 129 |
+
AnthropicAPIKey: cfg.LLM.AnthropicAPIKey,
|
| 130 |
+
DefaultModel: cfg.LLM.DefaultModel,
|
| 131 |
+
MaxTokens: cfg.LLM.MaxTokens,
|
| 132 |
+
Temperature: cfg.LLM.Temperature,
|
| 133 |
+
Timeout: cfg.LLM.Timeout,
|
| 134 |
+
MaxRetries: cfg.LLM.MaxRetries,
|
| 135 |
+
EnableFallback: cfg.LLM.EnableFallback,
|
| 136 |
+
Logger: logger,
|
| 137 |
+
})
|
| 138 |
+
|
| 139 |
+
// Initialize embedding client
|
| 140 |
+
embeddingClient := generator.NewOpenAIEmbeddingClient(generator.EmbeddingConfig{
|
| 141 |
+
Provider: cfg.Embedding.Provider,
|
| 142 |
+
APIKey: cfg.Embedding.APIKey,
|
| 143 |
+
Model: cfg.Embedding.Model,
|
| 144 |
+
Dimension: cfg.Embedding.Dimension,
|
| 145 |
+
BatchSize: cfg.Embedding.BatchSize,
|
| 146 |
+
})
|
| 147 |
+
|
| 148 |
+
return &GeneratorDependencies{
|
| 149 |
+
LLMClient: llmClient,
|
| 150 |
+
EmbeddingClient: embeddingClient,
|
| 151 |
+
}, nil
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
// GeneratorServer implements the GeneratorService gRPC server
|
| 155 |
+
type GeneratorServer struct {
|
| 156 |
+
UnimplementedGeneratorServiceServer
|
| 157 |
+
deps *GeneratorDependencies
|
| 158 |
+
logger *zap.Logger
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// NewGeneratorServer creates a new generator server
|
| 162 |
+
func NewGeneratorServer(deps *GeneratorDependencies, logger *zap.Logger) *GeneratorServer {
|
| 163 |
+
return &GeneratorServer{
|
| 164 |
+
deps: deps,
|
| 165 |
+
logger: logger,
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// Generate performs text generation
|
| 170 |
+
func (s *GeneratorServer) Generate(ctx context.Context, req *GenerateRequest) (*GenerateResponse, error) {
|
| 171 |
+
if len(req.Messages) == 0 {
|
| 172 |
+
return nil, status.Error(codes.InvalidArgument, "messages are required")
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// Convert messages
|
| 176 |
+
messages := make([]llm.Message, len(req.Messages))
|
| 177 |
+
for i, m := range req.Messages {
|
| 178 |
+
messages[i] = llm.Message{
|
| 179 |
+
Role: m.Role,
|
| 180 |
+
Content: m.Content,
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// Generate response
|
| 185 |
+
resp, err := s.deps.LLMClient.Generate(ctx, messages, llm.Options{
|
| 186 |
+
Model: req.Model,
|
| 187 |
+
Temperature: req.Temperature,
|
| 188 |
+
MaxTokens: int(req.MaxTokens),
|
| 189 |
+
})
|
| 190 |
+
if err != nil {
|
| 191 |
+
return nil, status.Error(codes.Internal, err.Error())
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
return &GenerateResponse{
|
| 195 |
+
Content: resp.Content,
|
| 196 |
+
FinishReason: resp.FinishReason,
|
| 197 |
+
Model: resp.Model,
|
| 198 |
+
Provider: string(resp.Provider),
|
| 199 |
+
Usage: &TokenUsage{
|
| 200 |
+
PromptTokens: int32(resp.Usage.PromptTokens),
|
| 201 |
+
CompletionTokens: int32(resp.Usage.CompletionTokens),
|
| 202 |
+
TotalTokens: int32(resp.Usage.TotalTokens),
|
| 203 |
+
},
|
| 204 |
+
}, nil
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// GenerateEmbedding generates embeddings for text
|
| 208 |
+
func (s *GeneratorServer) GenerateEmbedding(ctx context.Context, req *EmbeddingRequest) (*EmbeddingResponse, error) {
|
| 209 |
+
if req.Text == "" {
|
| 210 |
+
return nil, status.Error(codes.InvalidArgument, "text is required")
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
embedding, err := s.deps.EmbeddingClient.Generate(ctx, req.Text)
|
| 214 |
+
if err != nil {
|
| 215 |
+
return nil, status.Error(codes.Internal, err.Error())
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
return &EmbeddingResponse{
|
| 219 |
+
Embedding: embedding,
|
| 220 |
+
Dimension: int32(len(embedding)),
|
| 221 |
+
}, nil
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// BatchGenerateEmbeddings generates embeddings for multiple texts
|
| 225 |
+
func (s *GeneratorServer) BatchGenerateEmbeddings(ctx context.Context, req *BatchEmbeddingRequest) (*BatchEmbeddingResponse, error) {
|
| 226 |
+
if len(req.Texts) == 0 {
|
| 227 |
+
return nil, status.Error(codes.InvalidArgument, "texts are required")
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
embeddings, err := s.deps.EmbeddingClient.GenerateBatch(ctx, req.Texts)
|
| 231 |
+
if err != nil {
|
| 232 |
+
return nil, status.Error(codes.Internal, err.Error())
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
return &BatchEmbeddingResponse{
|
| 236 |
+
Embeddings: embeddings,
|
| 237 |
+
}, nil
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
// Stub types - replace with generated protobuf code
|
| 241 |
+
type Message struct {
|
| 242 |
+
Role string
|
| 243 |
+
Content string
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
type GenerateRequest struct {
|
| 247 |
+
Messages []*Message
|
| 248 |
+
Model string
|
| 249 |
+
Temperature float32
|
| 250 |
+
MaxTokens int32
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
type GenerateResponse struct {
|
| 254 |
+
Content string
|
| 255 |
+
FinishReason string
|
| 256 |
+
Model string
|
| 257 |
+
Provider string
|
| 258 |
+
Usage *TokenUsage
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
type TokenUsage struct {
|
| 262 |
+
PromptTokens int32
|
| 263 |
+
CompletionTokens int32
|
| 264 |
+
TotalTokens int32
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
type EmbeddingRequest struct {
|
| 268 |
+
Text string
|
| 269 |
+
Model string
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
type EmbeddingResponse struct {
|
| 273 |
+
Embedding []float32
|
| 274 |
+
Dimension int32
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
type BatchEmbeddingRequest struct {
|
| 278 |
+
Texts []string
|
| 279 |
+
Model string
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
type BatchEmbeddingResponse struct {
|
| 283 |
+
Embeddings [][]float32
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
type UnimplementedGeneratorServiceServer struct{}
|
| 287 |
+
|
| 288 |
+
func RegisterGeneratorServiceServer(s *grpc.Server, srv *GeneratorServer) {
|
| 289 |
+
// Registration happens when protobuf is generated
|
| 290 |
+
}
|
cmd/retriever-server/main.go
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package main provides the entry point for the Retriever service.
|
| 2 |
+
// This service handles document retrieval using vector, keyword, and graph search.
|
| 3 |
+
package main
|
| 4 |
+
|
| 5 |
+
import (
|
| 6 |
+
"context"
|
| 7 |
+
"fmt"
|
| 8 |
+
"net"
|
| 9 |
+
"os"
|
| 10 |
+
"os/signal"
|
| 11 |
+
"syscall"
|
| 12 |
+
|
| 13 |
+
"github.com/AmaniQuery/amaniquery/internal/generator"
|
| 14 |
+
"github.com/AmaniQuery/amaniquery/internal/retriever"
|
| 15 |
+
"github.com/AmaniQuery/amaniquery/internal/retriever/keyword"
|
| 16 |
+
"github.com/AmaniQuery/amaniquery/internal/retriever/vector"
|
| 17 |
+
"github.com/AmaniQuery/amaniquery/pkg/config"
|
| 18 |
+
"github.com/AmaniQuery/amaniquery/pkg/observability"
|
| 19 |
+
|
| 20 |
+
"go.uber.org/zap"
|
| 21 |
+
"google.golang.org/grpc"
|
| 22 |
+
"google.golang.org/grpc/codes"
|
| 23 |
+
"google.golang.org/grpc/health"
|
| 24 |
+
"google.golang.org/grpc/health/grpc_health_v1"
|
| 25 |
+
"google.golang.org/grpc/reflection"
|
| 26 |
+
"google.golang.org/grpc/status"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
func main() {
|
| 30 |
+
// Load configuration
|
| 31 |
+
cfg, err := config.Load()
|
| 32 |
+
if err != nil {
|
| 33 |
+
fmt.Fprintf(os.Stderr, "failed to load configuration: %v\n", err)
|
| 34 |
+
os.Exit(1)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Initialize logger
|
| 38 |
+
logger, err := observability.NewLogger(cfg.Observability.LogLevel, cfg.Observability.LogFormat)
|
| 39 |
+
if err != nil {
|
| 40 |
+
fmt.Fprintf(os.Stderr, "failed to initialize logger: %v\n", err)
|
| 41 |
+
os.Exit(1)
|
| 42 |
+
}
|
| 43 |
+
defer logger.Sync()
|
| 44 |
+
|
| 45 |
+
logger.Info("starting AmaniQuery retriever server",
|
| 46 |
+
zap.String("version", cfg.Version),
|
| 47 |
+
zap.String("environment", cfg.Environment),
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
// Initialize observability
|
| 51 |
+
tracingShutdown, err := observability.InitProvider(observability.Config{
|
| 52 |
+
ServiceName: "amaniquery-retriever",
|
| 53 |
+
ServiceVersion: cfg.Version,
|
| 54 |
+
TracingEnabled: cfg.Observability.TracingEnabled,
|
| 55 |
+
TracingEndpoint: cfg.Observability.TracingEndpoint,
|
| 56 |
+
})
|
| 57 |
+
if err != nil {
|
| 58 |
+
logger.Warn("failed to initialize tracing", zap.Error(err))
|
| 59 |
+
} else {
|
| 60 |
+
defer tracingShutdown(context.Background())
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Build dependencies
|
| 64 |
+
deps, err := buildRetrieverDependencies(cfg, logger)
|
| 65 |
+
if err != nil {
|
| 66 |
+
logger.Fatal("failed to build dependencies", zap.Error(err))
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// Create gRPC server
|
| 70 |
+
grpcServer := grpc.NewServer(
|
| 71 |
+
grpc.ChainUnaryInterceptor(
|
| 72 |
+
observability.UnaryServerInterceptor(),
|
| 73 |
+
),
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
// Register retriever service
|
| 77 |
+
retrieverServer := NewRetrieverServer(deps, logger)
|
| 78 |
+
RegisterRetrieverServiceServer(grpcServer, retrieverServer)
|
| 79 |
+
|
| 80 |
+
// Register health service
|
| 81 |
+
healthServer := health.NewServer()
|
| 82 |
+
grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
|
| 83 |
+
healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
| 84 |
+
|
| 85 |
+
// Enable reflection
|
| 86 |
+
reflection.Register(grpcServer)
|
| 87 |
+
|
| 88 |
+
// Start server
|
| 89 |
+
port := 9091 // Different port from agent server
|
| 90 |
+
addr := fmt.Sprintf(":%d", port)
|
| 91 |
+
listener, err := net.Listen("tcp", addr)
|
| 92 |
+
if err != nil {
|
| 93 |
+
logger.Fatal("failed to listen", zap.String("addr", addr), zap.Error(err))
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
logger.Info("gRPC retriever server starting", zap.String("addr", addr))
|
| 97 |
+
|
| 98 |
+
// Graceful shutdown
|
| 99 |
+
errChan := make(chan error, 1)
|
| 100 |
+
go func() {
|
| 101 |
+
errChan <- grpcServer.Serve(listener)
|
| 102 |
+
}()
|
| 103 |
+
|
| 104 |
+
quit := make(chan os.Signal, 1)
|
| 105 |
+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
| 106 |
+
|
| 107 |
+
select {
|
| 108 |
+
case err := <-errChan:
|
| 109 |
+
logger.Fatal("server error", zap.Error(err))
|
| 110 |
+
case sig := <-quit:
|
| 111 |
+
logger.Info("shutting down", zap.String("signal", sig.String()))
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
grpcServer.GracefulStop()
|
| 115 |
+
logger.Info("retriever server stopped")
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// RetrieverDependencies holds retriever service dependencies
|
| 119 |
+
type RetrieverDependencies struct {
|
| 120 |
+
VectorStore *vector.QdrantClient
|
| 121 |
+
KeywordEngine *keyword.BleveEngine
|
| 122 |
+
EmbeddingClient *generator.OpenAIEmbeddingClient
|
| 123 |
+
HybridRetriever *retriever.HybridRetriever
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
func buildRetrieverDependencies(cfg *config.Config, logger *zap.Logger) (*RetrieverDependencies, error) {
|
| 127 |
+
deps := &RetrieverDependencies{}
|
| 128 |
+
|
| 129 |
+
// Initialize vector store
|
| 130 |
+
vectorClient, err := vector.NewQdrantClient(vector.Config{
|
| 131 |
+
Host: cfg.VectorStore.Host,
|
| 132 |
+
Port: cfg.VectorStore.Port,
|
| 133 |
+
APIKey: cfg.VectorStore.APIKey,
|
| 134 |
+
Collection: cfg.VectorStore.Collection,
|
| 135 |
+
Dimension: cfg.VectorStore.Dimension,
|
| 136 |
+
Distance: cfg.VectorStore.Distance,
|
| 137 |
+
})
|
| 138 |
+
if err != nil {
|
| 139 |
+
logger.Warn("failed to connect to vector store", zap.Error(err))
|
| 140 |
+
} else {
|
| 141 |
+
deps.VectorStore = vectorClient
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// Initialize keyword engine
|
| 145 |
+
keywordEngine, err := keyword.NewBleveEngine(keyword.Config{
|
| 146 |
+
InMemory: true,
|
| 147 |
+
})
|
| 148 |
+
if err != nil {
|
| 149 |
+
logger.Warn("failed to initialize keyword engine", zap.Error(err))
|
| 150 |
+
} else {
|
| 151 |
+
deps.KeywordEngine = keywordEngine
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
// Initialize embedding client
|
| 155 |
+
embeddingClient := generator.NewOpenAIEmbeddingClient(generator.EmbeddingConfig{
|
| 156 |
+
Provider: cfg.Embedding.Provider,
|
| 157 |
+
APIKey: cfg.Embedding.APIKey,
|
| 158 |
+
Model: cfg.Embedding.Model,
|
| 159 |
+
Dimension: cfg.Embedding.Dimension,
|
| 160 |
+
BatchSize: cfg.Embedding.BatchSize,
|
| 161 |
+
})
|
| 162 |
+
deps.EmbeddingClient = embeddingClient
|
| 163 |
+
|
| 164 |
+
// Initialize hybrid retriever
|
| 165 |
+
deps.HybridRetriever = retriever.NewHybridRetriever(
|
| 166 |
+
deps.VectorStore,
|
| 167 |
+
deps.KeywordEngine,
|
| 168 |
+
deps.EmbeddingClient,
|
| 169 |
+
retriever.DefaultConfig(),
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
return deps, nil
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// RetrieverServer implements the RetrieverService gRPC server
|
| 176 |
+
type RetrieverServer struct {
|
| 177 |
+
UnimplementedRetrieverServiceServer
|
| 178 |
+
deps *RetrieverDependencies
|
| 179 |
+
logger *zap.Logger
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// NewRetrieverServer creates a new retriever server
|
| 183 |
+
func NewRetrieverServer(deps *RetrieverDependencies, logger *zap.Logger) *RetrieverServer {
|
| 184 |
+
return &RetrieverServer{
|
| 185 |
+
deps: deps,
|
| 186 |
+
logger: logger,
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// HybridSearch performs hybrid search
|
| 191 |
+
func (s *RetrieverServer) HybridSearch(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
|
| 192 |
+
if req.Query == "" {
|
| 193 |
+
return nil, status.Error(codes.InvalidArgument, "query is required")
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
topK := int(req.TopK)
|
| 197 |
+
if topK == 0 {
|
| 198 |
+
topK = 10
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
searchResp, err := s.deps.HybridRetriever.Search(ctx, retriever.SearchRequest{
|
| 202 |
+
Query: req.Query,
|
| 203 |
+
TopK: topK,
|
| 204 |
+
UseVector: true,
|
| 205 |
+
UseKeyword: true,
|
| 206 |
+
})
|
| 207 |
+
if err != nil {
|
| 208 |
+
return nil, status.Error(codes.Internal, err.Error())
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Convert results
|
| 212 |
+
documents := make([]*Document, len(searchResp.Results))
|
| 213 |
+
for i, r := range searchResp.Results {
|
| 214 |
+
documents[i] = &Document{
|
| 215 |
+
Id: r.ID,
|
| 216 |
+
Content: r.Content,
|
| 217 |
+
Title: r.Title,
|
| 218 |
+
Source: r.Source,
|
| 219 |
+
Score: r.Score,
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
return &SearchResponse{
|
| 224 |
+
Documents: documents,
|
| 225 |
+
TotalCount: int32(searchResp.TotalCount),
|
| 226 |
+
SearchTimeMs: searchResp.SearchTimeMs,
|
| 227 |
+
}, nil
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
// IndexDocument indexes a document
|
| 231 |
+
func (s *RetrieverServer) IndexDocument(ctx context.Context, req *IndexRequest) (*IndexResponse, error) {
|
| 232 |
+
if req.Document == nil {
|
| 233 |
+
return nil, status.Error(codes.InvalidArgument, "document is required")
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
// Generate embedding
|
| 237 |
+
embedding, err := s.deps.EmbeddingClient.Generate(ctx, req.Document.Content)
|
| 238 |
+
if err != nil {
|
| 239 |
+
return nil, status.Error(codes.Internal, "failed to generate embedding")
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// Index in vector store
|
| 243 |
+
if s.deps.VectorStore != nil {
|
| 244 |
+
err = s.deps.VectorStore.Index(ctx, vector.Document{
|
| 245 |
+
ID: req.Document.Id,
|
| 246 |
+
Content: req.Document.Content,
|
| 247 |
+
Embedding: embedding,
|
| 248 |
+
Metadata: convertMetadata(req.Document.Metadata),
|
| 249 |
+
})
|
| 250 |
+
if err != nil {
|
| 251 |
+
return nil, status.Error(codes.Internal, "failed to index in vector store")
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
// Index in keyword engine
|
| 256 |
+
if s.deps.KeywordEngine != nil {
|
| 257 |
+
err = s.deps.KeywordEngine.Index(ctx, keyword.Document{
|
| 258 |
+
ID: req.Document.Id,
|
| 259 |
+
Title: req.Document.Title,
|
| 260 |
+
Content: req.Document.Content,
|
| 261 |
+
Source: req.Document.Source,
|
| 262 |
+
})
|
| 263 |
+
if err != nil {
|
| 264 |
+
s.logger.Warn("failed to index in keyword engine", zap.Error(err))
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
return &IndexResponse{
|
| 269 |
+
DocumentId: req.Document.Id,
|
| 270 |
+
Success: true,
|
| 271 |
+
}, nil
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
func convertMetadata(m map[string]string) map[string]interface{} {
|
| 275 |
+
result := make(map[string]interface{}, len(m))
|
| 276 |
+
for k, v := range m {
|
| 277 |
+
result[k] = v
|
| 278 |
+
}
|
| 279 |
+
return result
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
// Stub types for protobuf - replace with generated code
|
| 283 |
+
type SearchRequest struct {
|
| 284 |
+
Query string
|
| 285 |
+
TopK int32
|
| 286 |
+
Embedding []float32
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
type SearchResponse struct {
|
| 290 |
+
Documents []*Document
|
| 291 |
+
TotalCount int32
|
| 292 |
+
SearchTimeMs int64
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
type Document struct {
|
| 296 |
+
Id string
|
| 297 |
+
Content string
|
| 298 |
+
Title string
|
| 299 |
+
Source string
|
| 300 |
+
Score float32
|
| 301 |
+
Metadata map[string]string
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
type IndexRequest struct {
|
| 305 |
+
Document *Document
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
type IndexResponse struct {
|
| 309 |
+
DocumentId string
|
| 310 |
+
Success bool
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
type UnimplementedRetrieverServiceServer struct{}
|
| 314 |
+
|
| 315 |
+
func RegisterRetrieverServiceServer(s *grpc.Server, srv *RetrieverServer) {
|
| 316 |
+
// Registration happens when protobuf is generated
|
| 317 |
+
}
|
cmd/worker/main.go
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package main provides the Temporal worker for RAG pipeline execution
|
| 2 |
+
package main
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"context"
|
| 6 |
+
"fmt"
|
| 7 |
+
"os"
|
| 8 |
+
"os/signal"
|
| 9 |
+
"syscall"
|
| 10 |
+
|
| 11 |
+
"github.com/AmaniQuery/amaniquery/internal/generator"
|
| 12 |
+
"github.com/AmaniQuery/amaniquery/internal/generator/llm"
|
| 13 |
+
"github.com/AmaniQuery/amaniquery/internal/guardrails"
|
| 14 |
+
"github.com/AmaniQuery/amaniquery/internal/retriever/graph"
|
| 15 |
+
"github.com/AmaniQuery/amaniquery/internal/retriever/keyword"
|
| 16 |
+
"github.com/AmaniQuery/amaniquery/internal/retriever/vector"
|
| 17 |
+
"github.com/AmaniQuery/amaniquery/internal/workflow"
|
| 18 |
+
"github.com/AmaniQuery/amaniquery/pkg/config"
|
| 19 |
+
"github.com/AmaniQuery/amaniquery/pkg/observability"
|
| 20 |
+
|
| 21 |
+
"go.temporal.io/sdk/client"
|
| 22 |
+
"go.temporal.io/sdk/worker"
|
| 23 |
+
"go.uber.org/zap"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
const taskQueue = "amaniquery-rag"
|
| 27 |
+
|
| 28 |
+
func main() {
|
| 29 |
+
// Load configuration
|
| 30 |
+
cfg, err := config.Load()
|
| 31 |
+
if err != nil {
|
| 32 |
+
fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err)
|
| 33 |
+
os.Exit(1)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Initialize logger
|
| 37 |
+
logger, err := observability.NewLogger(cfg.Observability.LogLevel, cfg.Observability.LogFormat)
|
| 38 |
+
if err != nil {
|
| 39 |
+
fmt.Fprintf(os.Stderr, "failed to init logger: %v\n", err)
|
| 40 |
+
os.Exit(1)
|
| 41 |
+
}
|
| 42 |
+
defer logger.Sync()
|
| 43 |
+
|
| 44 |
+
logger.Info("starting Temporal worker",
|
| 45 |
+
zap.String("task_queue", taskQueue),
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
// Create Temporal client
|
| 49 |
+
temporalClient, err := client.Dial(client.Options{
|
| 50 |
+
HostPort: getEnv("TEMPORAL_HOST", "localhost:7233"),
|
| 51 |
+
Logger: NewTemporalZapLogger(logger),
|
| 52 |
+
})
|
| 53 |
+
if err != nil {
|
| 54 |
+
logger.Fatal("failed to create Temporal client", zap.Error(err))
|
| 55 |
+
}
|
| 56 |
+
defer temporalClient.Close()
|
| 57 |
+
|
| 58 |
+
// Build activity dependencies
|
| 59 |
+
deps, err := buildDependencies(cfg, logger)
|
| 60 |
+
if err != nil {
|
| 61 |
+
logger.Fatal("failed to build dependencies", zap.Error(err))
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Create activities instance
|
| 65 |
+
activities := workflow.NewActivities(deps)
|
| 66 |
+
|
| 67 |
+
// Create worker
|
| 68 |
+
w := worker.New(temporalClient, taskQueue, worker.Options{
|
| 69 |
+
MaxConcurrentActivityExecutionSize: 10,
|
| 70 |
+
MaxConcurrentWorkflowTaskExecutionSize: 10,
|
| 71 |
+
})
|
| 72 |
+
|
| 73 |
+
// Register workflow and activities
|
| 74 |
+
w.RegisterWorkflow(workflow.RAGWorkflow)
|
| 75 |
+
w.RegisterActivity(activities.ValidateInput)
|
| 76 |
+
w.RegisterActivity(activities.GenerateEmbedding)
|
| 77 |
+
w.RegisterActivity(activities.VectorSearch)
|
| 78 |
+
w.RegisterActivity(activities.KeywordSearch)
|
| 79 |
+
w.RegisterActivity(activities.GraphSearch)
|
| 80 |
+
w.RegisterActivity(activities.RankSources)
|
| 81 |
+
w.RegisterActivity(activities.GenerateResponse)
|
| 82 |
+
w.RegisterActivity(activities.ValidateOutput)
|
| 83 |
+
|
| 84 |
+
// Start worker
|
| 85 |
+
errChan := make(chan error, 1)
|
| 86 |
+
go func() {
|
| 87 |
+
errChan <- w.Run(worker.InterruptCh())
|
| 88 |
+
}()
|
| 89 |
+
|
| 90 |
+
logger.Info("Temporal worker started", zap.String("task_queue", taskQueue))
|
| 91 |
+
|
| 92 |
+
// Wait for shutdown
|
| 93 |
+
quit := make(chan os.Signal, 1)
|
| 94 |
+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
| 95 |
+
|
| 96 |
+
select {
|
| 97 |
+
case err := <-errChan:
|
| 98 |
+
logger.Fatal("worker error", zap.Error(err))
|
| 99 |
+
case sig := <-quit:
|
| 100 |
+
logger.Info("shutting down", zap.String("signal", sig.String()))
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
w.Stop()
|
| 104 |
+
logger.Info("worker stopped")
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
func buildDependencies(cfg *config.Config, logger *zap.Logger) (*workflow.ActivityDependencies, error) {
|
| 108 |
+
deps := &workflow.ActivityDependencies{}
|
| 109 |
+
|
| 110 |
+
// Embedding client
|
| 111 |
+
embeddingClient := generator.NewOpenAIEmbeddingClient(generator.EmbeddingConfig{
|
| 112 |
+
Provider: cfg.Embedding.Provider,
|
| 113 |
+
APIKey: cfg.Embedding.APIKey,
|
| 114 |
+
Model: cfg.Embedding.Model,
|
| 115 |
+
Dimension: cfg.Embedding.Dimension,
|
| 116 |
+
})
|
| 117 |
+
deps.EmbeddingClient = embeddingClient
|
| 118 |
+
|
| 119 |
+
// Vector store
|
| 120 |
+
vectorClient, err := vector.NewQdrantClient(vector.Config{
|
| 121 |
+
Host: cfg.VectorStore.Host,
|
| 122 |
+
Port: cfg.VectorStore.Port,
|
| 123 |
+
APIKey: cfg.VectorStore.APIKey,
|
| 124 |
+
Collection: cfg.VectorStore.Collection,
|
| 125 |
+
})
|
| 126 |
+
if err != nil {
|
| 127 |
+
logger.Warn("vector store unavailable", zap.Error(err))
|
| 128 |
+
} else {
|
| 129 |
+
deps.VectorStore = &vectorStoreAdapter{client: vectorClient}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// Keyword engine
|
| 133 |
+
keywordEngine, err := keyword.NewBleveEngine(keyword.Config{InMemory: true})
|
| 134 |
+
if err != nil {
|
| 135 |
+
logger.Warn("keyword engine unavailable", zap.Error(err))
|
| 136 |
+
} else {
|
| 137 |
+
deps.KeywordEngine = &keywordAdapter{engine: keywordEngine}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// Graph store (Neo4j)
|
| 141 |
+
neo4jCfg := graph.DefaultConfig()
|
| 142 |
+
if uri := os.Getenv("NEO4J_URI"); uri != "" {
|
| 143 |
+
neo4jCfg.URI = uri
|
| 144 |
+
}
|
| 145 |
+
if user := os.Getenv("NEO4J_USERNAME"); user != "" {
|
| 146 |
+
neo4jCfg.Username = user
|
| 147 |
+
neo4jCfg.Password = os.Getenv("NEO4J_PASSWORD")
|
| 148 |
+
}
|
| 149 |
+
graphClient, err := graph.NewClient(neo4jCfg)
|
| 150 |
+
if err != nil {
|
| 151 |
+
logger.Warn("graph store unavailable", zap.Error(err))
|
| 152 |
+
} else {
|
| 153 |
+
deps.GraphStore = &graphAdapter{client: graphClient}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// Guardrails
|
| 157 |
+
guardrailsCfg := guardrails.DefaultConfig()
|
| 158 |
+
if url := os.Getenv("GUARDRAILS_URL"); url != "" {
|
| 159 |
+
guardrailsCfg.BaseURL = url
|
| 160 |
+
}
|
| 161 |
+
guardrailsClient := guardrails.NewClient(guardrailsCfg)
|
| 162 |
+
deps.Guardrails = &guardrailsAdapter{client: guardrailsClient}
|
| 163 |
+
|
| 164 |
+
// LLM client
|
| 165 |
+
llmClient := llm.NewFallbackClient(llm.Config{
|
| 166 |
+
GeminiAPIKey: cfg.LLM.GeminiAPIKey,
|
| 167 |
+
MoonshotAPIKey: cfg.LLM.MoonshotAPIKey,
|
| 168 |
+
OllamaBaseURL: cfg.LLM.OllamaBaseURL,
|
| 169 |
+
OpenAIAPIKey: cfg.LLM.OpenAIAPIKey,
|
| 170 |
+
AnthropicAPIKey: cfg.LLM.AnthropicAPIKey,
|
| 171 |
+
Logger: logger,
|
| 172 |
+
})
|
| 173 |
+
deps.LLMClient = &llmAdapter{client: llmClient}
|
| 174 |
+
|
| 175 |
+
return deps, nil
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// Adapters to match workflow interfaces
|
| 179 |
+
|
| 180 |
+
type vectorStoreAdapter struct {
|
| 181 |
+
client *vector.QdrantClient
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
func (a *vectorStoreAdapter) Search(ctx context.Context, embedding []float32, topK int, filters map[string]interface{}) ([]workflow.Source, error) {
|
| 185 |
+
results, err := a.client.Search(ctx, embedding, topK, filters)
|
| 186 |
+
if err != nil {
|
| 187 |
+
return nil, err
|
| 188 |
+
}
|
| 189 |
+
sources := make([]workflow.Source, len(results))
|
| 190 |
+
for i, r := range results {
|
| 191 |
+
sources[i] = workflow.Source{
|
| 192 |
+
ID: r.ID,
|
| 193 |
+
Title: r.Title,
|
| 194 |
+
Content: r.Content,
|
| 195 |
+
Score: r.Score,
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
return sources, nil
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
type keywordAdapter struct {
|
| 202 |
+
engine *keyword.BleveEngine
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
func (a *keywordAdapter) Search(ctx context.Context, query string, topK int) ([]workflow.Source, error) {
|
| 206 |
+
results, err := a.engine.Search(ctx, query, topK)
|
| 207 |
+
if err != nil {
|
| 208 |
+
return nil, err
|
| 209 |
+
}
|
| 210 |
+
sources := make([]workflow.Source, len(results))
|
| 211 |
+
for i, r := range results {
|
| 212 |
+
sources[i] = workflow.Source{
|
| 213 |
+
ID: r.ID,
|
| 214 |
+
Title: r.Title,
|
| 215 |
+
Content: r.Content,
|
| 216 |
+
Score: r.Score,
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
return sources, nil
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
type graphAdapter struct {
|
| 223 |
+
client *graph.Client
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
func (a *graphAdapter) Search(ctx context.Context, query string, embedding []float32, topK int) ([]workflow.Source, error) {
|
| 227 |
+
results, err := a.client.Search(ctx, graph.SearchRequest{
|
| 228 |
+
Query: query,
|
| 229 |
+
Embedding: embedding,
|
| 230 |
+
TopK: topK,
|
| 231 |
+
MaxHops: 2,
|
| 232 |
+
})
|
| 233 |
+
if err != nil {
|
| 234 |
+
return nil, err
|
| 235 |
+
}
|
| 236 |
+
sources := make([]workflow.Source, len(results))
|
| 237 |
+
for i, r := range results {
|
| 238 |
+
sources[i] = workflow.Source{
|
| 239 |
+
ID: r.ID,
|
| 240 |
+
Title: r.Title,
|
| 241 |
+
Content: r.Content,
|
| 242 |
+
Score: r.Score,
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
return sources, nil
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
type guardrailsAdapter struct {
|
| 249 |
+
client *guardrails.Client
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
func (a *guardrailsAdapter) ValidateInput(ctx context.Context, input string) (bool, string, error) {
|
| 253 |
+
resp, err := a.client.ValidateInput(ctx, input)
|
| 254 |
+
if err != nil {
|
| 255 |
+
return true, "", err // Fail open
|
| 256 |
+
}
|
| 257 |
+
return !resp.Blocked, resp.Reason, nil
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
func (a *guardrailsAdapter) ValidateOutput(ctx context.Context, input, output string) (bool, string, error) {
|
| 261 |
+
resp, err := a.client.ValidateOutput(ctx, input, output)
|
| 262 |
+
if err != nil {
|
| 263 |
+
return true, "", err
|
| 264 |
+
}
|
| 265 |
+
return !resp.Blocked, resp.Reason, nil
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
type llmAdapter struct {
|
| 269 |
+
client *llm.FallbackClient
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
func (a *llmAdapter) Generate(ctx context.Context, prompt string, options workflow.GenerateOptions) (string, int, error) {
|
| 273 |
+
resp, err := a.client.Generate(ctx, []llm.Message{
|
| 274 |
+
{Role: "user", Content: prompt},
|
| 275 |
+
}, llm.Options{
|
| 276 |
+
Temperature: options.Temperature,
|
| 277 |
+
MaxTokens: options.MaxTokens,
|
| 278 |
+
})
|
| 279 |
+
if err != nil {
|
| 280 |
+
return "", 0, err
|
| 281 |
+
}
|
| 282 |
+
return resp.Content, resp.Usage.TotalTokens, nil
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
func getEnv(key, fallback string) string {
|
| 286 |
+
if v := os.Getenv(key); v != "" {
|
| 287 |
+
return v
|
| 288 |
+
}
|
| 289 |
+
return fallback
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// TemporalZapLogger adapts zap.Logger for Temporal
|
| 293 |
+
type TemporalZapLogger struct {
|
| 294 |
+
logger *zap.Logger
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
func NewTemporalZapLogger(logger *zap.Logger) *TemporalZapLogger {
|
| 298 |
+
return &TemporalZapLogger{logger: logger.Named("temporal")}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
func (l *TemporalZapLogger) Debug(msg string, keyvals ...interface{}) {
|
| 302 |
+
l.logger.Debug(msg, toZapFields(keyvals)...)
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
func (l *TemporalZapLogger) Info(msg string, keyvals ...interface{}) {
|
| 306 |
+
l.logger.Info(msg, toZapFields(keyvals)...)
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
func (l *TemporalZapLogger) Warn(msg string, keyvals ...interface{}) {
|
| 310 |
+
l.logger.Warn(msg, toZapFields(keyvals)...)
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
func (l *TemporalZapLogger) Error(msg string, keyvals ...interface{}) {
|
| 314 |
+
l.logger.Error(msg, toZapFields(keyvals)...)
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
func toZapFields(keyvals []interface{}) []zap.Field {
|
| 318 |
+
fields := make([]zap.Field, 0, len(keyvals)/2)
|
| 319 |
+
for i := 0; i < len(keyvals)-1; i += 2 {
|
| 320 |
+
key, ok := keyvals[i].(string)
|
| 321 |
+
if !ok {
|
| 322 |
+
continue
|
| 323 |
+
}
|
| 324 |
+
fields = append(fields, zap.Any(key, keyvals[i+1]))
|
| 325 |
+
}
|
| 326 |
+
return fields
|
| 327 |
+
}
|
config.example.yaml
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AmaniQuery Configuration
|
| 2 |
+
# Copy this file to config.yaml and customize as needed
|
| 3 |
+
|
| 4 |
+
version: "1.0.0"
|
| 5 |
+
environment: "development"
|
| 6 |
+
|
| 7 |
+
server:
|
| 8 |
+
grpc_port: 9090
|
| 9 |
+
http_port: 8080
|
| 10 |
+
graceful_timeout: 30s
|
| 11 |
+
max_connections: 1000
|
| 12 |
+
|
| 13 |
+
vector_store:
|
| 14 |
+
type: qdrant
|
| 15 |
+
host: localhost
|
| 16 |
+
port: 6334
|
| 17 |
+
# api_key: set via QDRANT_API_KEY env var
|
| 18 |
+
collection: amaniquery
|
| 19 |
+
dimension: 1536
|
| 20 |
+
distance: Cosine
|
| 21 |
+
|
| 22 |
+
cache:
|
| 23 |
+
redis_url: redis://localhost:6379
|
| 24 |
+
local_size: 10000
|
| 25 |
+
ttl: 1h
|
| 26 |
+
max_retries: 3
|
| 27 |
+
pool_size: 10
|
| 28 |
+
|
| 29 |
+
# LLM Configuration with Multi-Provider Fallback
|
| 30 |
+
# Fallback order: Gemini → Moonshot → Ollama → OpenAI → Anthropic
|
| 31 |
+
# Set API keys via environment variables:
|
| 32 |
+
# GEMINI_API_KEY or GOOGLE_API_KEY
|
| 33 |
+
# MOONSHOT_API_KEY
|
| 34 |
+
# OLLAMA_BASE_URL (for local Ollama, default: http://localhost:11434)
|
| 35 |
+
# OPENAI_API_KEY
|
| 36 |
+
# ANTHROPIC_API_KEY
|
| 37 |
+
llm:
|
| 38 |
+
default_model: gemini-1.5-flash # Used when provider doesn't specify model
|
| 39 |
+
max_tokens: 4096
|
| 40 |
+
temperature: 0.7
|
| 41 |
+
timeout: 60s
|
| 42 |
+
max_retries: 3
|
| 43 |
+
enable_fallback: true # Automatically try next provider on failure
|
| 44 |
+
ollama_base_url: http://localhost:11434 # For local Ollama
|
| 45 |
+
|
| 46 |
+
embedding:
|
| 47 |
+
provider: openai # openai for text-embedding-3-small
|
| 48 |
+
# api_key: falls back to OPENAI_API_KEY
|
| 49 |
+
model: text-embedding-3-small
|
| 50 |
+
dimension: 1536
|
| 51 |
+
batch_size: 100
|
| 52 |
+
|
| 53 |
+
observability:
|
| 54 |
+
tracing_enabled: true
|
| 55 |
+
tracing_endpoint: localhost:4317
|
| 56 |
+
metrics_enabled: true
|
| 57 |
+
metrics_port: 9091
|
| 58 |
+
log_level: info # debug, info, warn, error
|
| 59 |
+
log_format: json # json, console
|
| 60 |
+
|
| 61 |
+
security:
|
| 62 |
+
# jwt_secret: set via JWT_SECRET env var
|
| 63 |
+
jwt_issuer: amaniquery
|
| 64 |
+
enable_mtls: false
|
| 65 |
+
# cert_file: /path/to/cert.pem
|
| 66 |
+
# key_file: /path/to/key.pem
|
| 67 |
+
# ca_file: /path/to/ca.pem
|
deploy.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AmaniQuery Render Deployment Guide
|
| 2 |
+
|
| 3 |
+
This guide details the steps to deploy the AmaniQuery monorepo components to [Render.com](https://render.com).
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
1. **GitHub Repository**: Ensure your code is pushed to a GitHub repository connected to Render.
|
| 8 |
+
2. **Docker**, **Node.js**, and **Go** knowledge.
|
| 9 |
+
3. **Render Account**: Created and ready.
|
| 10 |
+
|
| 11 |
+
## 1. Database & Infrastructure (Render Postgres & Redis)
|
| 12 |
+
|
| 13 |
+
Before deploying services, set up your managed data stores.
|
| 14 |
+
|
| 15 |
+
### PostgreSQL
|
| 16 |
+
|
| 17 |
+
- **Type**: PostgreSQL
|
| 18 |
+
- **Name**: `amaniquery-db`
|
| 19 |
+
- **Region**: Frankfurt (EU-Central) or nearest.
|
| 20 |
+
- **Environment**:
|
| 21 |
+
- `POSTGRES_USER`: `amaniquery`
|
| 22 |
+
- `POSTGRES_DB`: `amaniquery`
|
| 23 |
+
- **Internal Connection URL**: Copy this for use in service env vars.
|
| 24 |
+
|
| 25 |
+
### Redis
|
| 26 |
+
|
| 27 |
+
- **Type**: Redis
|
| 28 |
+
- **Name**: `amaniquery-redis`
|
| 29 |
+
- **Max Memory Policy**: `allkeys-lru`
|
| 30 |
+
- **Internal Connection URL**: Copy this (`redis://...`).
|
| 31 |
+
|
| 32 |
+
## 2. Backend Services (Docker Runtime)
|
| 33 |
+
|
| 34 |
+
Since AmaniQuery is a monorepo, using **Docker** as the Runtime is recommended for Go services to handle build context correctly (copying `go.mod` from root).
|
| 35 |
+
|
| 36 |
+
### Common Configuration for All Backend Services
|
| 37 |
+
|
| 38 |
+
- **Runtime**: Docker
|
| 39 |
+
- **Repository**: `your-repo/amaniquery`
|
| 40 |
+
- **Region**: Same as Database.
|
| 41 |
+
|
| 42 |
+
| Service Name | Dockerfile Path | Build Context Directory | Env Vars |
|
| 43 |
+
| :--- | :--- | :--- | :--- |
|
| 44 |
+
| `amaniquery-portal` | `services/portal/Dockerfile` | `.` (Root) | `SERVER_HTTP_PORT=8080`, `DB_HOST=...` |
|
| 45 |
+
| `amaniquery-ingestion`| `services/ingestion/Dockerfile`| `.` (Root) | `QDRANT_URL=...`, `RABBITMQ_URL=...` |
|
| 46 |
+
| `amaniquery-voice` | `services/voice/Dockerfile` | `.` (Root) | `OPENAI_API_KEY=...` |
|
| 47 |
+
| `amaniquery-files` | `services/files/Dockerfile` | `.` (Root) | `MINIO_ENDPOINT=...` |
|
| 48 |
+
| `amaniquery-notifications`| `services/notifications/Dockerfile.gateway` | `.` (Root) | `MAILTRAP_API_KEY=...` |
|
| 49 |
+
|
| 50 |
+
> [!TIP]
|
| 51 |
+
> **Root Directory Setting**: In Render, set "Root Directory" to `.` (default) so Docker builds have access to the full monorepo context.
|
| 52 |
+
|
| 53 |
+
## 3. Frontend Applications (Static Sites)
|
| 54 |
+
|
| 55 |
+
We will use the **Static Site** type for frontends, relying on the `Dockerfile` or Render's Native Node build.
|
| 56 |
+
*Recommendation*: Use **Static Site** with Node build command for faster deploys, or **Docker** if you need Nginx customization.
|
| 57 |
+
|
| 58 |
+
### Option A: Static Site (Native Node - Recommended)
|
| 59 |
+
|
| 60 |
+
- **Build Command**: `yarn && yarn turbo run build --filter=admin-portal`
|
| 61 |
+
- **Publish Directory**: `frontend/apps/admin/dist`
|
| 62 |
+
- **Root Directory**: `frontend`
|
| 63 |
+
|
| 64 |
+
### Option B: Docker (Using our new Turbo Dockerfiles)
|
| 65 |
+
|
| 66 |
+
- **Runtime**: Docker
|
| 67 |
+
- **Dockerfile Path**: `frontend/apps/admin/Dockerfile`
|
| 68 |
+
- **Context**: `frontend` (Important: context is `frontend` subfolder, not root, for these specific Dockerfiles)
|
| 69 |
+
|
| 70 |
+
| App Name | Build Command (Static) | Publish Dir | Context |
|
| 71 |
+
| :--- | :--- | :--- | :--- |
|
| 72 |
+
| `admin-portal` | `yarn build:admin` | `apps/admin/dist` | `frontend` |
|
| 73 |
+
| `developer-portal` | `yarn build:dev` | `apps/developer-portal/dist` | `frontend` |
|
| 74 |
+
| `web-app` | `yarn build:web` | `apps/web/dist` | `frontend` |
|
| 75 |
+
|
| 76 |
+
> *Note: You may need to add helper scripts in `frontend/package.json` like `"build:admin": "turbo run build --filter=admin-portal"` to keep commands simple.*
|
| 77 |
+
|
| 78 |
+
## 4. HuggingFace Spaces (Docker Deployment)
|
| 79 |
+
|
| 80 |
+
For users preferring HuggingFace Spaces (free tier: 2 vCPU, 16GB RAM, 50GB disk), follow these steps.
|
| 81 |
+
|
| 82 |
+
### Prerequisites
|
| 83 |
+
|
| 84 |
+
- HuggingFace Account
|
| 85 |
+
- External Databases (Managed Services):
|
| 86 |
+
- **PostgreSQL**: Neon.tech (Free Tier available)
|
| 87 |
+
- **Redis**: Upstash (Free Tier available)
|
| 88 |
+
- **Qdrant**: Qdrant Cloud (Free Tier available)
|
| 89 |
+
|
| 90 |
+
### Deployment Steps
|
| 91 |
+
|
| 92 |
+
1. **Create a New Space**:
|
| 93 |
+
- Go to [HuggingFace Spaces](https://huggingface.co/new-space)
|
| 94 |
+
- Enter a name (e.g., `amaniquery`)
|
| 95 |
+
- Select **Docker** as the Space SDK
|
| 96 |
+
- Choose "Blank" for the template
|
| 97 |
+
|
| 98 |
+
2. **Deploy via Script (Recommended)**:
|
| 99 |
+
- Set `HF_TOKEN` in your local `.env`.
|
| 100 |
+
- Run: `python scripts/deploy_hf.py agent`
|
| 101 |
+
- This deploys both the Go Agent and Rust Memory Service in a single container (Sidecar pattern) for maximum efficiency and localhost communication.
|
| 102 |
+
|
| 103 |
+
3. **Deploy Manually (Alternative)**:
|
| 104 |
+
- Clone your Space's repository locally.
|
| 105 |
+
- Copy `deployments/huggingface/Dockerfile.hf` to `Dockerfile` in the root.
|
| 106 |
+
- Copy `deployments/huggingface/README.md` to the root.
|
| 107 |
+
- Push to HuggingFace.
|
| 108 |
+
|
| 109 |
+
4. **Configure Secrets**:
|
| 110 |
+
- Go to **Settings** -> **Variables and Secrets** in your Space.
|
| 111 |
+
- Add the secrets listed in `deployments/huggingface/.env.hf.example`.
|
| 112 |
+
|
| 113 |
+
5. **Status**:
|
| 114 |
+
- The Space will build and start both `agent-server` and `memory-server`.
|
| 115 |
+
- The Go agent will automatically connect to the local memory service.
|
| 116 |
+
|
| 117 |
+
6. **Automated Deployment (Optional)**:
|
| 118 |
+
We provided a script `scripts/deploy_hf.py` to automate the deployment process.
|
| 119 |
+
|
| 120 |
+
**Prerequisites**:
|
| 121 |
+
- `HF_TOKEN` must be set in your `.env` file (Get it from [HF Settings](https://huggingface.co/settings/tokens)).
|
| 122 |
+
- The Spaces must be created first (e.g., `AmaniQuery/amaniquery-agent` and `AmaniQuery/amaniquery-memory`).
|
| 123 |
+
|
| 124 |
+
**Usage**:
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
# Deploy Agent
|
| 128 |
+
python scripts/deploy_hf.py agent
|
| 129 |
+
|
| 130 |
+
# Deploy Memory Service
|
| 131 |
+
python scripts/deploy_hf.py memory
|
| 132 |
+
|
| 133 |
+
# Deploy Both
|
| 134 |
+
python scripts/deploy_hf.py all
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
## 5. Environment Variables Checklist
|
| 138 |
+
|
| 139 |
+
Transfer these from your `env.example` files to Render's "Environment" tab for each service.
|
| 140 |
+
|
| 141 |
+
- [ ] **Portal**: `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`, `REDIS_URL`, `JWT_SECRET`
|
| 142 |
+
- [ ] **Voice**: `OPENAI_API_KEY`, `ELEVENLABS_API_KEY`, `REDIS_URL`
|
| 143 |
+
- [ ] **Frontend**: `VITE_API_BASE_URL` (Set this to the `https://...onrender.com` URL of your Portal service).
|
| 144 |
+
|
| 145 |
+
## 6. Deployment Order
|
| 146 |
+
|
| 147 |
+
1. **Infrastructure** (Postgres/Redis) - Wait for healthy.
|
| 148 |
+
2. **Backend Services** (Portal, etc.) - Deploy & check logs.
|
| 149 |
+
3. **Frontend Apps** - Deploy & update `VITE_API_BASE_URL` with backend URL.
|
docs/API_GATEWAY.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API Gateway for RAG Agent Framework
|
| 2 |
+
|
| 3 |
+
A production-ready, multi-protocol API Gateway for the AmaniQuery RAG Agent Framework. Provides unified access for frontend clients with comprehensive security, observability, and performance features.
|
| 4 |
+
|
| 5 |
+
## Architecture
|
| 6 |
+
|
| 7 |
+

|
| 8 |
+
|
| 9 |
+
```mermaid
|
| 10 |
+
graph TB
|
| 11 |
+
subgraph "Client Layer"
|
| 12 |
+
WEB[Web Browser]
|
| 13 |
+
MOBILE[Mobile App]
|
| 14 |
+
DEVELOPER[Developer Portal]
|
| 15 |
+
end
|
| 16 |
+
|
| 17 |
+
subgraph "API Gateway Layer"
|
| 18 |
+
GW[API Gateway<br/>Go Service]
|
| 19 |
+
|
| 20 |
+
subgraph "Middleware Stack"
|
| 21 |
+
CORS[CORS Handler]
|
| 22 |
+
RATE[Rate Limiter]
|
| 23 |
+
AUTH[JWT Validator]
|
| 24 |
+
AUDIT[Audit Logger]
|
| 25 |
+
end
|
| 26 |
+
|
| 27 |
+
subgraph "Protocol Handlers"
|
| 28 |
+
REST[REST Handler]
|
| 29 |
+
WS[WebSocket Handler]
|
| 30 |
+
GQL[GraphQL Handler]
|
| 31 |
+
end
|
| 32 |
+
end
|
| 33 |
+
|
| 34 |
+
subgraph "Backend Services"
|
| 35 |
+
AGENT[Agent Service]
|
| 36 |
+
RETRIEVER[Retriever Service]
|
| 37 |
+
GENERATOR[Generator Service]
|
| 38 |
+
MEMORY[Memory Service]
|
| 39 |
+
end
|
| 40 |
+
|
| 41 |
+
WEB --> GW
|
| 42 |
+
MOBILE --> GW
|
| 43 |
+
DEVELOPER --> GW
|
| 44 |
+
|
| 45 |
+
GW --> CORS --> RATE --> AUTH --> AUDIT
|
| 46 |
+
AUDIT --> REST
|
| 47 |
+
AUDIT --> WS
|
| 48 |
+
AUDIT --> GQL
|
| 49 |
+
|
| 50 |
+
REST --> AGENT
|
| 51 |
+
WS --> AGENT
|
| 52 |
+
GQL --> AGENT
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
## Features
|
| 56 |
+
|
| 57 |
+
### Multi-Protocol Support
|
| 58 |
+
- **REST API** - Standard HTTP endpoints for queries, agents, memory
|
| 59 |
+
- **WebSocket** - Real-time streaming for query responses
|
| 60 |
+
- **Server-Sent Events** - Lightweight streaming alternative
|
| 61 |
+
- **GraphQL** - Flexible query interface (placeholder)
|
| 62 |
+
|
| 63 |
+
### Security
|
| 64 |
+
- **JWT Authentication** - Token-based auth with HMAC/RSA signing
|
| 65 |
+
- **OPA Authorization** - Fine-grained policy-based access control
|
| 66 |
+
- **Rate Limiting** - Token bucket with per-tenant/user isolation
|
| 67 |
+
- **CORS** - Configurable cross-origin policies
|
| 68 |
+
- **Security Headers** - HSTS, CSP, X-Frame-Options, etc.
|
| 69 |
+
|
| 70 |
+
### Observability
|
| 71 |
+
- **Prometheus Metrics** - Request counts, latencies, cache hits
|
| 72 |
+
- **OpenTelemetry Tracing** - Distributed request tracing
|
| 73 |
+
- **Audit Logging** - Structured logs for compliance
|
| 74 |
+
|
| 75 |
+
### Performance
|
| 76 |
+
- **Redis Caching** - Query response caching with smart TTL
|
| 77 |
+
- **Circuit Breakers** - Failure isolation per service
|
| 78 |
+
- **Connection Pooling** - Efficient gRPC connections
|
| 79 |
+
|
| 80 |
+
## Quick Start
|
| 81 |
+
|
| 82 |
+
### Prerequisites
|
| 83 |
+
- Go 1.21+
|
| 84 |
+
- Docker & Docker Compose
|
| 85 |
+
- Redis (for caching/rate limiting)
|
| 86 |
+
|
| 87 |
+
### Running Locally
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
# Clone the repository
|
| 91 |
+
cd AmaniQuery
|
| 92 |
+
|
| 93 |
+
# Copy example config
|
| 94 |
+
cp gateway.example.yaml gateway.yaml
|
| 95 |
+
|
| 96 |
+
# Run with Docker Compose
|
| 97 |
+
cd deployments/docker
|
| 98 |
+
docker-compose up -d api-gateway
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### Configuration
|
| 102 |
+
|
| 103 |
+
See `gateway.example.yaml` for all options. Key settings:
|
| 104 |
+
|
| 105 |
+
```yaml
|
| 106 |
+
server:
|
| 107 |
+
bindAddr: ":8443"
|
| 108 |
+
|
| 109 |
+
auth:
|
| 110 |
+
jwtSecret: "${JWT_SECRET}"
|
| 111 |
+
|
| 112 |
+
cache:
|
| 113 |
+
redisAddr: "redis:6379"
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
Environment variables override config with `GATEWAY_` prefix.
|
| 117 |
+
|
| 118 |
+
## API Endpoints
|
| 119 |
+
|
| 120 |
+
### Queries
|
| 121 |
+
| Method | Path | Description |
|
| 122 |
+
|--------|------|-------------|
|
| 123 |
+
| POST | `/v2/queries` | Execute RAG query |
|
| 124 |
+
| GET | `/v2/queries/{id}` | Get async query result |
|
| 125 |
+
| WS | `/v2/queries/stream` | Streaming query |
|
| 126 |
+
|
| 127 |
+
### Agents
|
| 128 |
+
| Method | Path | Description |
|
| 129 |
+
|--------|------|-------------|
|
| 130 |
+
| POST | `/v2/agents` | Create agent |
|
| 131 |
+
| GET | `/v2/agents/{id}` | Get agent |
|
| 132 |
+
| DELETE | `/v2/agents/{id}` | Delete agent |
|
| 133 |
+
| POST | `/v2/agents/{id}/execute` | Execute plan |
|
| 134 |
+
|
| 135 |
+
### Memory
|
| 136 |
+
| Method | Path | Description |
|
| 137 |
+
|--------|------|-------------|
|
| 138 |
+
| GET | `/v2/memory/context` | Get context window |
|
| 139 |
+
| POST | `/v2/memory/sessions/{id}/consolidate` | Consolidate memory |
|
| 140 |
+
|
| 141 |
+
### Admin
|
| 142 |
+
| Method | Path | Description |
|
| 143 |
+
|--------|------|-------------|
|
| 144 |
+
| GET | `/admin/health` | Health check |
|
| 145 |
+
| GET | `/admin/metrics` | Prometheus metrics |
|
| 146 |
+
|
| 147 |
+
## WebSocket Protocol
|
| 148 |
+
|
| 149 |
+
```javascript
|
| 150 |
+
// Connect
|
| 151 |
+
const ws = new WebSocket('wss://api.example.com/v2/queries/stream?token=JWT');
|
| 152 |
+
|
| 153 |
+
// Send query
|
| 154 |
+
ws.send(JSON.stringify({
|
| 155 |
+
type: 'query',
|
| 156 |
+
payload: { query: 'What is RAG?', userId: 'user-123' }
|
| 157 |
+
}));
|
| 158 |
+
|
| 159 |
+
// Receive chunks
|
| 160 |
+
ws.onmessage = (e) => {
|
| 161 |
+
const msg = JSON.parse(e.data);
|
| 162 |
+
if (msg.type === 'chunk') console.log(msg.data);
|
| 163 |
+
if (msg.type === 'done') console.log('Complete');
|
| 164 |
+
};
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
## Deployment
|
| 168 |
+
|
| 169 |
+
### Docker
|
| 170 |
+
```bash
|
| 171 |
+
docker build -f deployments/docker/Dockerfile.gateway -t api-gateway .
|
| 172 |
+
docker run -p 8443:8443 api-gateway
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
### Kubernetes
|
| 176 |
+
```bash
|
| 177 |
+
kubectl apply -f deployments/k8s/gateway.yaml
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
## Project Structure
|
| 181 |
+
|
| 182 |
+
```
|
| 183 |
+
internal/gateway/
|
| 184 |
+
├── config.go # Configuration
|
| 185 |
+
├── gateway.go # Main server
|
| 186 |
+
├── types.go # Request/response types
|
| 187 |
+
├── cache/
|
| 188 |
+
│ └── cache.go # Redis cache
|
| 189 |
+
├── handlers/
|
| 190 |
+
│ ├── query.go # Query endpoints
|
| 191 |
+
│ ├── websocket.go # WebSocket streaming
|
| 192 |
+
│ ├── agent.go # Agent CRUD
|
| 193 |
+
│ ├── memory.go # Memory endpoints
|
| 194 |
+
│ ├── admin.go # Health/metrics
|
| 195 |
+
│ └���─ auth.go # Token endpoint
|
| 196 |
+
├── middleware/
|
| 197 |
+
│ ├── cors.go # CORS handling
|
| 198 |
+
│ ├── ratelimit.go # Rate limiting
|
| 199 |
+
│ ├── auth.go # JWT + OPA auth
|
| 200 |
+
│ ├── audit.go # Audit logging
|
| 201 |
+
│ └── tracing.go # OpenTelemetry
|
| 202 |
+
├── observability/
|
| 203 |
+
│ └── metrics.go # Prometheus metrics
|
| 204 |
+
└── services/
|
| 205 |
+
├── registry.go # Service discovery
|
| 206 |
+
└── clients.go # gRPC clients
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
## License
|
| 210 |
+
|
| 211 |
+
Apache 2.0
|
gateway.example.yaml
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gateway Configuration Example
|
| 2 |
+
server:
|
| 3 |
+
bindAddr: ":8443"
|
| 4 |
+
readTimeout: 30s
|
| 5 |
+
writeTimeout: 60s
|
| 6 |
+
idleTimeout: 120s
|
| 7 |
+
shutdownTimeout: 30s
|
| 8 |
+
maxHeaderBytes: 1048576
|
| 9 |
+
enableHttp2: true
|
| 10 |
+
|
| 11 |
+
tls:
|
| 12 |
+
enabled: false
|
| 13 |
+
certFile: "/certs/tls.crt"
|
| 14 |
+
keyFile: "/certs/tls.key"
|
| 15 |
+
minVersion: "1.2"
|
| 16 |
+
|
| 17 |
+
rateLimit:
|
| 18 |
+
enabled: true
|
| 19 |
+
requestsPerSec: 100
|
| 20 |
+
burstSize: 200
|
| 21 |
+
perTenant: true
|
| 22 |
+
perUser: true
|
| 23 |
+
redisEnabled: true
|
| 24 |
+
cleanupInterval: 10m
|
| 25 |
+
|
| 26 |
+
cors:
|
| 27 |
+
allowedOrigins:
|
| 28 |
+
- "https://app.rag-agent.io"
|
| 29 |
+
- "https://admin.rag-agent.io"
|
| 30 |
+
- "http://localhost:3000"
|
| 31 |
+
allowedMethods:
|
| 32 |
+
- "GET"
|
| 33 |
+
- "POST"
|
| 34 |
+
- "PUT"
|
| 35 |
+
- "DELETE"
|
| 36 |
+
- "OPTIONS"
|
| 37 |
+
allowedHeaders:
|
| 38 |
+
- "Authorization"
|
| 39 |
+
- "Content-Type"
|
| 40 |
+
- "X-Request-ID"
|
| 41 |
+
- "X-Client-Version"
|
| 42 |
+
allowCredentials: true
|
| 43 |
+
maxAge: 3600
|
| 44 |
+
|
| 45 |
+
auth:
|
| 46 |
+
jwtSecret: "${JWT_SECRET}"
|
| 47 |
+
jwtIssuer: "rag-agent"
|
| 48 |
+
jwtAudience: "api"
|
| 49 |
+
tokenDuration: 24h
|
| 50 |
+
opaEnabled: false
|
| 51 |
+
opaAddr: "http://opa:8181"
|
| 52 |
+
opaPolicy: "authz/allow"
|
| 53 |
+
skipPaths:
|
| 54 |
+
- "/admin/health"
|
| 55 |
+
- "/admin/metrics"
|
| 56 |
+
|
| 57 |
+
cache:
|
| 58 |
+
enabled: true
|
| 59 |
+
redisAddr: "redis:6379"
|
| 60 |
+
redisPassword: ""
|
| 61 |
+
redisDb: 0
|
| 62 |
+
defaultTtl: 5m
|
| 63 |
+
maxEntrySize: 1048576
|
| 64 |
+
keyPrefix: "rag:gateway:"
|
| 65 |
+
|
| 66 |
+
serviceDiscovery:
|
| 67 |
+
enabled: false
|
| 68 |
+
consulAddr: "consul:8500"
|
| 69 |
+
consulToken: ""
|
| 70 |
+
serviceRefreshInterval: 30s
|
| 71 |
+
agentServiceAddr: "agent-server:9090"
|
| 72 |
+
retrieverServiceAddr: "retriever-server:9091"
|
| 73 |
+
generatorServiceAddr: "generator-server:9092"
|
| 74 |
+
memoryServiceAddr: "memory-server:9093"
|
| 75 |
+
|
| 76 |
+
circuitBreaker:
|
| 77 |
+
maxRequests: 5
|
| 78 |
+
interval: 60s
|
| 79 |
+
timeout: 30s
|
| 80 |
+
failureThreshold: 3
|
| 81 |
+
|
| 82 |
+
observability:
|
| 83 |
+
metricsEnabled: true
|
| 84 |
+
metricsPath: "/admin/metrics"
|
| 85 |
+
tracingEnabled: true
|
| 86 |
+
tracingEndpoint: "jaeger:4317"
|
| 87 |
+
serviceName: "api-gateway"
|
| 88 |
+
auditLogEnabled: true
|
| 89 |
+
|
| 90 |
+
websocket:
|
| 91 |
+
readBufferSize: 1024
|
| 92 |
+
writeBufferSize: 1024
|
| 93 |
+
pingInterval: 30s
|
| 94 |
+
pongWait: 60s
|
| 95 |
+
writeWait: 10s
|
| 96 |
+
maxMessageSize: 524288
|
generator.pb.go
ADDED
|
@@ -0,0 +1,1414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Code generated by protoc-gen-go. DO NOT EDIT.
|
| 2 |
+
// versions:
|
| 3 |
+
// protoc-gen-go v1.36.11
|
| 4 |
+
// protoc v6.33.2
|
| 5 |
+
// source: generator.proto
|
| 6 |
+
|
| 7 |
+
package ragv1
|
| 8 |
+
|
| 9 |
+
import (
|
| 10 |
+
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
| 11 |
+
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
| 12 |
+
reflect "reflect"
|
| 13 |
+
sync "sync"
|
| 14 |
+
unsafe "unsafe"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
const (
|
| 18 |
+
// Verify that this generated code is sufficiently up-to-date.
|
| 19 |
+
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
| 20 |
+
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
| 21 |
+
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
// GenerateRequest for LLM generation
|
| 25 |
+
type GenerateRequest struct {
|
| 26 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 27 |
+
// System prompt
|
| 28 |
+
SystemPrompt string `protobuf:"bytes,1,opt,name=system_prompt,json=systemPrompt,proto3" json:"system_prompt,omitempty"`
|
| 29 |
+
// User query/prompt
|
| 30 |
+
Prompt string `protobuf:"bytes,2,opt,name=prompt,proto3" json:"prompt,omitempty"`
|
| 31 |
+
// Context from retrieved documents
|
| 32 |
+
Context []*ContextDocument `protobuf:"bytes,3,rep,name=context,proto3" json:"context,omitempty"`
|
| 33 |
+
// Conversation history
|
| 34 |
+
History []*ChatMessage `protobuf:"bytes,4,rep,name=history,proto3" json:"history,omitempty"`
|
| 35 |
+
// Generation configuration
|
| 36 |
+
Config *GenerateConfig `protobuf:"bytes,5,opt,name=config,proto3" json:"config,omitempty"`
|
| 37 |
+
unknownFields protoimpl.UnknownFields
|
| 38 |
+
sizeCache protoimpl.SizeCache
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
func (x *GenerateRequest) Reset() {
|
| 42 |
+
*x = GenerateRequest{}
|
| 43 |
+
mi := &file_generator_proto_msgTypes[0]
|
| 44 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 45 |
+
ms.StoreMessageInfo(mi)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
func (x *GenerateRequest) String() string {
|
| 49 |
+
return protoimpl.X.MessageStringOf(x)
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
func (*GenerateRequest) ProtoMessage() {}
|
| 53 |
+
|
| 54 |
+
func (x *GenerateRequest) ProtoReflect() protoreflect.Message {
|
| 55 |
+
mi := &file_generator_proto_msgTypes[0]
|
| 56 |
+
if x != nil {
|
| 57 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 58 |
+
if ms.LoadMessageInfo() == nil {
|
| 59 |
+
ms.StoreMessageInfo(mi)
|
| 60 |
+
}
|
| 61 |
+
return ms
|
| 62 |
+
}
|
| 63 |
+
return mi.MessageOf(x)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Deprecated: Use GenerateRequest.ProtoReflect.Descriptor instead.
|
| 67 |
+
func (*GenerateRequest) Descriptor() ([]byte, []int) {
|
| 68 |
+
return file_generator_proto_rawDescGZIP(), []int{0}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
func (x *GenerateRequest) GetSystemPrompt() string {
|
| 72 |
+
if x != nil {
|
| 73 |
+
return x.SystemPrompt
|
| 74 |
+
}
|
| 75 |
+
return ""
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
func (x *GenerateRequest) GetPrompt() string {
|
| 79 |
+
if x != nil {
|
| 80 |
+
return x.Prompt
|
| 81 |
+
}
|
| 82 |
+
return ""
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
func (x *GenerateRequest) GetContext() []*ContextDocument {
|
| 86 |
+
if x != nil {
|
| 87 |
+
return x.Context
|
| 88 |
+
}
|
| 89 |
+
return nil
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
func (x *GenerateRequest) GetHistory() []*ChatMessage {
|
| 93 |
+
if x != nil {
|
| 94 |
+
return x.History
|
| 95 |
+
}
|
| 96 |
+
return nil
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
func (x *GenerateRequest) GetConfig() *GenerateConfig {
|
| 100 |
+
if x != nil {
|
| 101 |
+
return x.Config
|
| 102 |
+
}
|
| 103 |
+
return nil
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// ContextDocument represents retrieved context
|
| 107 |
+
type ContextDocument struct {
|
| 108 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 109 |
+
// Document content
|
| 110 |
+
Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"`
|
| 111 |
+
// Document title
|
| 112 |
+
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
|
| 113 |
+
// Source reference
|
| 114 |
+
Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"`
|
| 115 |
+
// Relevance score
|
| 116 |
+
Score float32 `protobuf:"fixed32,4,opt,name=score,proto3" json:"score,omitempty"`
|
| 117 |
+
unknownFields protoimpl.UnknownFields
|
| 118 |
+
sizeCache protoimpl.SizeCache
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
func (x *ContextDocument) Reset() {
|
| 122 |
+
*x = ContextDocument{}
|
| 123 |
+
mi := &file_generator_proto_msgTypes[1]
|
| 124 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 125 |
+
ms.StoreMessageInfo(mi)
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
func (x *ContextDocument) String() string {
|
| 129 |
+
return protoimpl.X.MessageStringOf(x)
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
func (*ContextDocument) ProtoMessage() {}
|
| 133 |
+
|
| 134 |
+
func (x *ContextDocument) ProtoReflect() protoreflect.Message {
|
| 135 |
+
mi := &file_generator_proto_msgTypes[1]
|
| 136 |
+
if x != nil {
|
| 137 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 138 |
+
if ms.LoadMessageInfo() == nil {
|
| 139 |
+
ms.StoreMessageInfo(mi)
|
| 140 |
+
}
|
| 141 |
+
return ms
|
| 142 |
+
}
|
| 143 |
+
return mi.MessageOf(x)
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Deprecated: Use ContextDocument.ProtoReflect.Descriptor instead.
|
| 147 |
+
func (*ContextDocument) Descriptor() ([]byte, []int) {
|
| 148 |
+
return file_generator_proto_rawDescGZIP(), []int{1}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
func (x *ContextDocument) GetContent() string {
|
| 152 |
+
if x != nil {
|
| 153 |
+
return x.Content
|
| 154 |
+
}
|
| 155 |
+
return ""
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
func (x *ContextDocument) GetTitle() string {
|
| 159 |
+
if x != nil {
|
| 160 |
+
return x.Title
|
| 161 |
+
}
|
| 162 |
+
return ""
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
func (x *ContextDocument) GetSource() string {
|
| 166 |
+
if x != nil {
|
| 167 |
+
return x.Source
|
| 168 |
+
}
|
| 169 |
+
return ""
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
func (x *ContextDocument) GetScore() float32 {
|
| 173 |
+
if x != nil {
|
| 174 |
+
return x.Score
|
| 175 |
+
}
|
| 176 |
+
return 0
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// ChatMessage for conversation history
|
| 180 |
+
type ChatMessage struct {
|
| 181 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 182 |
+
// Role: user, assistant, system
|
| 183 |
+
Role string `protobuf:"bytes,1,opt,name=role,proto3" json:"role,omitempty"`
|
| 184 |
+
// Message content
|
| 185 |
+
Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
|
| 186 |
+
unknownFields protoimpl.UnknownFields
|
| 187 |
+
sizeCache protoimpl.SizeCache
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
func (x *ChatMessage) Reset() {
|
| 191 |
+
*x = ChatMessage{}
|
| 192 |
+
mi := &file_generator_proto_msgTypes[2]
|
| 193 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 194 |
+
ms.StoreMessageInfo(mi)
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
func (x *ChatMessage) String() string {
|
| 198 |
+
return protoimpl.X.MessageStringOf(x)
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
func (*ChatMessage) ProtoMessage() {}
|
| 202 |
+
|
| 203 |
+
func (x *ChatMessage) ProtoReflect() protoreflect.Message {
|
| 204 |
+
mi := &file_generator_proto_msgTypes[2]
|
| 205 |
+
if x != nil {
|
| 206 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 207 |
+
if ms.LoadMessageInfo() == nil {
|
| 208 |
+
ms.StoreMessageInfo(mi)
|
| 209 |
+
}
|
| 210 |
+
return ms
|
| 211 |
+
}
|
| 212 |
+
return mi.MessageOf(x)
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// Deprecated: Use ChatMessage.ProtoReflect.Descriptor instead.
|
| 216 |
+
func (*ChatMessage) Descriptor() ([]byte, []int) {
|
| 217 |
+
return file_generator_proto_rawDescGZIP(), []int{2}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
func (x *ChatMessage) GetRole() string {
|
| 221 |
+
if x != nil {
|
| 222 |
+
return x.Role
|
| 223 |
+
}
|
| 224 |
+
return ""
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
func (x *ChatMessage) GetContent() string {
|
| 228 |
+
if x != nil {
|
| 229 |
+
return x.Content
|
| 230 |
+
}
|
| 231 |
+
return ""
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// GenerateConfig for generation parameters
|
| 235 |
+
type GenerateConfig struct {
|
| 236 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 237 |
+
// Model to use
|
| 238 |
+
Model string `protobuf:"bytes,1,opt,name=model,proto3" json:"model,omitempty"`
|
| 239 |
+
// Temperature (0.0 - 2.0)
|
| 240 |
+
Temperature float32 `protobuf:"fixed32,2,opt,name=temperature,proto3" json:"temperature,omitempty"`
|
| 241 |
+
// Maximum tokens to generate
|
| 242 |
+
MaxTokens int32 `protobuf:"varint,3,opt,name=max_tokens,json=maxTokens,proto3" json:"max_tokens,omitempty"`
|
| 243 |
+
// Top-p sampling
|
| 244 |
+
TopP float32 `protobuf:"fixed32,4,opt,name=top_p,json=topP,proto3" json:"top_p,omitempty"`
|
| 245 |
+
// Frequency penalty
|
| 246 |
+
FrequencyPenalty float32 `protobuf:"fixed32,5,opt,name=frequency_penalty,json=frequencyPenalty,proto3" json:"frequency_penalty,omitempty"`
|
| 247 |
+
// Presence penalty
|
| 248 |
+
PresencePenalty float32 `protobuf:"fixed32,6,opt,name=presence_penalty,json=presencePenalty,proto3" json:"presence_penalty,omitempty"`
|
| 249 |
+
// Stop sequences
|
| 250 |
+
StopSequences []string `protobuf:"bytes,7,rep,name=stop_sequences,json=stopSequences,proto3" json:"stop_sequences,omitempty"`
|
| 251 |
+
// Response format: text, json
|
| 252 |
+
ResponseFormat string `protobuf:"bytes,8,opt,name=response_format,json=responseFormat,proto3" json:"response_format,omitempty"`
|
| 253 |
+
unknownFields protoimpl.UnknownFields
|
| 254 |
+
sizeCache protoimpl.SizeCache
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
func (x *GenerateConfig) Reset() {
|
| 258 |
+
*x = GenerateConfig{}
|
| 259 |
+
mi := &file_generator_proto_msgTypes[3]
|
| 260 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 261 |
+
ms.StoreMessageInfo(mi)
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
func (x *GenerateConfig) String() string {
|
| 265 |
+
return protoimpl.X.MessageStringOf(x)
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
func (*GenerateConfig) ProtoMessage() {}
|
| 269 |
+
|
| 270 |
+
func (x *GenerateConfig) ProtoReflect() protoreflect.Message {
|
| 271 |
+
mi := &file_generator_proto_msgTypes[3]
|
| 272 |
+
if x != nil {
|
| 273 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 274 |
+
if ms.LoadMessageInfo() == nil {
|
| 275 |
+
ms.StoreMessageInfo(mi)
|
| 276 |
+
}
|
| 277 |
+
return ms
|
| 278 |
+
}
|
| 279 |
+
return mi.MessageOf(x)
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
// Deprecated: Use GenerateConfig.ProtoReflect.Descriptor instead.
|
| 283 |
+
func (*GenerateConfig) Descriptor() ([]byte, []int) {
|
| 284 |
+
return file_generator_proto_rawDescGZIP(), []int{3}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
func (x *GenerateConfig) GetModel() string {
|
| 288 |
+
if x != nil {
|
| 289 |
+
return x.Model
|
| 290 |
+
}
|
| 291 |
+
return ""
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
func (x *GenerateConfig) GetTemperature() float32 {
|
| 295 |
+
if x != nil {
|
| 296 |
+
return x.Temperature
|
| 297 |
+
}
|
| 298 |
+
return 0
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
func (x *GenerateConfig) GetMaxTokens() int32 {
|
| 302 |
+
if x != nil {
|
| 303 |
+
return x.MaxTokens
|
| 304 |
+
}
|
| 305 |
+
return 0
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
func (x *GenerateConfig) GetTopP() float32 {
|
| 309 |
+
if x != nil {
|
| 310 |
+
return x.TopP
|
| 311 |
+
}
|
| 312 |
+
return 0
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
func (x *GenerateConfig) GetFrequencyPenalty() float32 {
|
| 316 |
+
if x != nil {
|
| 317 |
+
return x.FrequencyPenalty
|
| 318 |
+
}
|
| 319 |
+
return 0
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
func (x *GenerateConfig) GetPresencePenalty() float32 {
|
| 323 |
+
if x != nil {
|
| 324 |
+
return x.PresencePenalty
|
| 325 |
+
}
|
| 326 |
+
return 0
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
func (x *GenerateConfig) GetStopSequences() []string {
|
| 330 |
+
if x != nil {
|
| 331 |
+
return x.StopSequences
|
| 332 |
+
}
|
| 333 |
+
return nil
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
func (x *GenerateConfig) GetResponseFormat() string {
|
| 337 |
+
if x != nil {
|
| 338 |
+
return x.ResponseFormat
|
| 339 |
+
}
|
| 340 |
+
return ""
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// GenerateResponse contains the generated text
|
| 344 |
+
type GenerateResponse struct {
|
| 345 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 346 |
+
// Generated text
|
| 347 |
+
Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"`
|
| 348 |
+
// Token usage
|
| 349 |
+
Usage *TokenUsage `protobuf:"bytes,2,opt,name=usage,proto3" json:"usage,omitempty"`
|
| 350 |
+
// Finish reason: stop, length, content_filter
|
| 351 |
+
FinishReason string `protobuf:"bytes,3,opt,name=finish_reason,json=finishReason,proto3" json:"finish_reason,omitempty"`
|
| 352 |
+
// Model used
|
| 353 |
+
Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"`
|
| 354 |
+
// Generation metadata
|
| 355 |
+
Metadata *GenerateMetadata `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
| 356 |
+
unknownFields protoimpl.UnknownFields
|
| 357 |
+
sizeCache protoimpl.SizeCache
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
func (x *GenerateResponse) Reset() {
|
| 361 |
+
*x = GenerateResponse{}
|
| 362 |
+
mi := &file_generator_proto_msgTypes[4]
|
| 363 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 364 |
+
ms.StoreMessageInfo(mi)
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
func (x *GenerateResponse) String() string {
|
| 368 |
+
return protoimpl.X.MessageStringOf(x)
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
func (*GenerateResponse) ProtoMessage() {}
|
| 372 |
+
|
| 373 |
+
func (x *GenerateResponse) ProtoReflect() protoreflect.Message {
|
| 374 |
+
mi := &file_generator_proto_msgTypes[4]
|
| 375 |
+
if x != nil {
|
| 376 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 377 |
+
if ms.LoadMessageInfo() == nil {
|
| 378 |
+
ms.StoreMessageInfo(mi)
|
| 379 |
+
}
|
| 380 |
+
return ms
|
| 381 |
+
}
|
| 382 |
+
return mi.MessageOf(x)
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
// Deprecated: Use GenerateResponse.ProtoReflect.Descriptor instead.
|
| 386 |
+
func (*GenerateResponse) Descriptor() ([]byte, []int) {
|
| 387 |
+
return file_generator_proto_rawDescGZIP(), []int{4}
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
func (x *GenerateResponse) GetText() string {
|
| 391 |
+
if x != nil {
|
| 392 |
+
return x.Text
|
| 393 |
+
}
|
| 394 |
+
return ""
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
func (x *GenerateResponse) GetUsage() *TokenUsage {
|
| 398 |
+
if x != nil {
|
| 399 |
+
return x.Usage
|
| 400 |
+
}
|
| 401 |
+
return nil
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
func (x *GenerateResponse) GetFinishReason() string {
|
| 405 |
+
if x != nil {
|
| 406 |
+
return x.FinishReason
|
| 407 |
+
}
|
| 408 |
+
return ""
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
func (x *GenerateResponse) GetModel() string {
|
| 412 |
+
if x != nil {
|
| 413 |
+
return x.Model
|
| 414 |
+
}
|
| 415 |
+
return ""
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
func (x *GenerateResponse) GetMetadata() *GenerateMetadata {
|
| 419 |
+
if x != nil {
|
| 420 |
+
return x.Metadata
|
| 421 |
+
}
|
| 422 |
+
return nil
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
// TokenUsage tracks token consumption
|
| 426 |
+
type TokenUsage struct {
|
| 427 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 428 |
+
// Prompt tokens
|
| 429 |
+
PromptTokens int32 `protobuf:"varint,1,opt,name=prompt_tokens,json=promptTokens,proto3" json:"prompt_tokens,omitempty"`
|
| 430 |
+
// Completion tokens
|
| 431 |
+
CompletionTokens int32 `protobuf:"varint,2,opt,name=completion_tokens,json=completionTokens,proto3" json:"completion_tokens,omitempty"`
|
| 432 |
+
// Total tokens
|
| 433 |
+
TotalTokens int32 `protobuf:"varint,3,opt,name=total_tokens,json=totalTokens,proto3" json:"total_tokens,omitempty"`
|
| 434 |
+
// Estimated cost in USD
|
| 435 |
+
EstimatedCost float32 `protobuf:"fixed32,4,opt,name=estimated_cost,json=estimatedCost,proto3" json:"estimated_cost,omitempty"`
|
| 436 |
+
unknownFields protoimpl.UnknownFields
|
| 437 |
+
sizeCache protoimpl.SizeCache
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
func (x *TokenUsage) Reset() {
|
| 441 |
+
*x = TokenUsage{}
|
| 442 |
+
mi := &file_generator_proto_msgTypes[5]
|
| 443 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 444 |
+
ms.StoreMessageInfo(mi)
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
func (x *TokenUsage) String() string {
|
| 448 |
+
return protoimpl.X.MessageStringOf(x)
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
func (*TokenUsage) ProtoMessage() {}
|
| 452 |
+
|
| 453 |
+
func (x *TokenUsage) ProtoReflect() protoreflect.Message {
|
| 454 |
+
mi := &file_generator_proto_msgTypes[5]
|
| 455 |
+
if x != nil {
|
| 456 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 457 |
+
if ms.LoadMessageInfo() == nil {
|
| 458 |
+
ms.StoreMessageInfo(mi)
|
| 459 |
+
}
|
| 460 |
+
return ms
|
| 461 |
+
}
|
| 462 |
+
return mi.MessageOf(x)
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
// Deprecated: Use TokenUsage.ProtoReflect.Descriptor instead.
|
| 466 |
+
func (*TokenUsage) Descriptor() ([]byte, []int) {
|
| 467 |
+
return file_generator_proto_rawDescGZIP(), []int{5}
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
func (x *TokenUsage) GetPromptTokens() int32 {
|
| 471 |
+
if x != nil {
|
| 472 |
+
return x.PromptTokens
|
| 473 |
+
}
|
| 474 |
+
return 0
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
func (x *TokenUsage) GetCompletionTokens() int32 {
|
| 478 |
+
if x != nil {
|
| 479 |
+
return x.CompletionTokens
|
| 480 |
+
}
|
| 481 |
+
return 0
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
func (x *TokenUsage) GetTotalTokens() int32 {
|
| 485 |
+
if x != nil {
|
| 486 |
+
return x.TotalTokens
|
| 487 |
+
}
|
| 488 |
+
return 0
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
func (x *TokenUsage) GetEstimatedCost() float32 {
|
| 492 |
+
if x != nil {
|
| 493 |
+
return x.EstimatedCost
|
| 494 |
+
}
|
| 495 |
+
return 0
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
// GenerateMetadata for generation info
|
| 499 |
+
type GenerateMetadata struct {
|
| 500 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 501 |
+
// Latency in milliseconds
|
| 502 |
+
LatencyMs int64 `protobuf:"varint,1,opt,name=latency_ms,json=latencyMs,proto3" json:"latency_ms,omitempty"`
|
| 503 |
+
// Provider used
|
| 504 |
+
Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"`
|
| 505 |
+
// Trace ID
|
| 506 |
+
TraceId string `protobuf:"bytes,3,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"`
|
| 507 |
+
unknownFields protoimpl.UnknownFields
|
| 508 |
+
sizeCache protoimpl.SizeCache
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
func (x *GenerateMetadata) Reset() {
|
| 512 |
+
*x = GenerateMetadata{}
|
| 513 |
+
mi := &file_generator_proto_msgTypes[6]
|
| 514 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 515 |
+
ms.StoreMessageInfo(mi)
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
func (x *GenerateMetadata) String() string {
|
| 519 |
+
return protoimpl.X.MessageStringOf(x)
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
func (*GenerateMetadata) ProtoMessage() {}
|
| 523 |
+
|
| 524 |
+
func (x *GenerateMetadata) ProtoReflect() protoreflect.Message {
|
| 525 |
+
mi := &file_generator_proto_msgTypes[6]
|
| 526 |
+
if x != nil {
|
| 527 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 528 |
+
if ms.LoadMessageInfo() == nil {
|
| 529 |
+
ms.StoreMessageInfo(mi)
|
| 530 |
+
}
|
| 531 |
+
return ms
|
| 532 |
+
}
|
| 533 |
+
return mi.MessageOf(x)
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
// Deprecated: Use GenerateMetadata.ProtoReflect.Descriptor instead.
|
| 537 |
+
func (*GenerateMetadata) Descriptor() ([]byte, []int) {
|
| 538 |
+
return file_generator_proto_rawDescGZIP(), []int{6}
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
func (x *GenerateMetadata) GetLatencyMs() int64 {
|
| 542 |
+
if x != nil {
|
| 543 |
+
return x.LatencyMs
|
| 544 |
+
}
|
| 545 |
+
return 0
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
func (x *GenerateMetadata) GetProvider() string {
|
| 549 |
+
if x != nil {
|
| 550 |
+
return x.Provider
|
| 551 |
+
}
|
| 552 |
+
return ""
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
func (x *GenerateMetadata) GetTraceId() string {
|
| 556 |
+
if x != nil {
|
| 557 |
+
return x.TraceId
|
| 558 |
+
}
|
| 559 |
+
return ""
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
// GenerateChunk for streaming responses
|
| 563 |
+
type GenerateChunk struct {
|
| 564 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 565 |
+
// Text delta
|
| 566 |
+
Delta string `protobuf:"bytes,1,opt,name=delta,proto3" json:"delta,omitempty"`
|
| 567 |
+
// Is this the final chunk?
|
| 568 |
+
IsFinal bool `protobuf:"varint,2,opt,name=is_final,json=isFinal,proto3" json:"is_final,omitempty"`
|
| 569 |
+
// Token usage (in final chunk)
|
| 570 |
+
Usage *TokenUsage `protobuf:"bytes,3,opt,name=usage,proto3" json:"usage,omitempty"`
|
| 571 |
+
// Finish reason (in final chunk)
|
| 572 |
+
FinishReason string `protobuf:"bytes,4,opt,name=finish_reason,json=finishReason,proto3" json:"finish_reason,omitempty"`
|
| 573 |
+
unknownFields protoimpl.UnknownFields
|
| 574 |
+
sizeCache protoimpl.SizeCache
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
func (x *GenerateChunk) Reset() {
|
| 578 |
+
*x = GenerateChunk{}
|
| 579 |
+
mi := &file_generator_proto_msgTypes[7]
|
| 580 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 581 |
+
ms.StoreMessageInfo(mi)
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
func (x *GenerateChunk) String() string {
|
| 585 |
+
return protoimpl.X.MessageStringOf(x)
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
func (*GenerateChunk) ProtoMessage() {}
|
| 589 |
+
|
| 590 |
+
func (x *GenerateChunk) ProtoReflect() protoreflect.Message {
|
| 591 |
+
mi := &file_generator_proto_msgTypes[7]
|
| 592 |
+
if x != nil {
|
| 593 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 594 |
+
if ms.LoadMessageInfo() == nil {
|
| 595 |
+
ms.StoreMessageInfo(mi)
|
| 596 |
+
}
|
| 597 |
+
return ms
|
| 598 |
+
}
|
| 599 |
+
return mi.MessageOf(x)
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
// Deprecated: Use GenerateChunk.ProtoReflect.Descriptor instead.
|
| 603 |
+
func (*GenerateChunk) Descriptor() ([]byte, []int) {
|
| 604 |
+
return file_generator_proto_rawDescGZIP(), []int{7}
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
func (x *GenerateChunk) GetDelta() string {
|
| 608 |
+
if x != nil {
|
| 609 |
+
return x.Delta
|
| 610 |
+
}
|
| 611 |
+
return ""
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
func (x *GenerateChunk) GetIsFinal() bool {
|
| 615 |
+
if x != nil {
|
| 616 |
+
return x.IsFinal
|
| 617 |
+
}
|
| 618 |
+
return false
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
func (x *GenerateChunk) GetUsage() *TokenUsage {
|
| 622 |
+
if x != nil {
|
| 623 |
+
return x.Usage
|
| 624 |
+
}
|
| 625 |
+
return nil
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
func (x *GenerateChunk) GetFinishReason() string {
|
| 629 |
+
if x != nil {
|
| 630 |
+
return x.FinishReason
|
| 631 |
+
}
|
| 632 |
+
return ""
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
// EmbeddingRequest for generating embeddings
|
| 636 |
+
type EmbeddingRequest struct {
|
| 637 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 638 |
+
// Text to embed
|
| 639 |
+
Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"`
|
| 640 |
+
// Model to use
|
| 641 |
+
Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"`
|
| 642 |
+
// Embedding dimensions (if configurable)
|
| 643 |
+
Dimensions int32 `protobuf:"varint,3,opt,name=dimensions,proto3" json:"dimensions,omitempty"`
|
| 644 |
+
unknownFields protoimpl.UnknownFields
|
| 645 |
+
sizeCache protoimpl.SizeCache
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
func (x *EmbeddingRequest) Reset() {
|
| 649 |
+
*x = EmbeddingRequest{}
|
| 650 |
+
mi := &file_generator_proto_msgTypes[8]
|
| 651 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 652 |
+
ms.StoreMessageInfo(mi)
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
func (x *EmbeddingRequest) String() string {
|
| 656 |
+
return protoimpl.X.MessageStringOf(x)
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
func (*EmbeddingRequest) ProtoMessage() {}
|
| 660 |
+
|
| 661 |
+
func (x *EmbeddingRequest) ProtoReflect() protoreflect.Message {
|
| 662 |
+
mi := &file_generator_proto_msgTypes[8]
|
| 663 |
+
if x != nil {
|
| 664 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 665 |
+
if ms.LoadMessageInfo() == nil {
|
| 666 |
+
ms.StoreMessageInfo(mi)
|
| 667 |
+
}
|
| 668 |
+
return ms
|
| 669 |
+
}
|
| 670 |
+
return mi.MessageOf(x)
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
// Deprecated: Use EmbeddingRequest.ProtoReflect.Descriptor instead.
|
| 674 |
+
func (*EmbeddingRequest) Descriptor() ([]byte, []int) {
|
| 675 |
+
return file_generator_proto_rawDescGZIP(), []int{8}
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
func (x *EmbeddingRequest) GetText() string {
|
| 679 |
+
if x != nil {
|
| 680 |
+
return x.Text
|
| 681 |
+
}
|
| 682 |
+
return ""
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
func (x *EmbeddingRequest) GetModel() string {
|
| 686 |
+
if x != nil {
|
| 687 |
+
return x.Model
|
| 688 |
+
}
|
| 689 |
+
return ""
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
func (x *EmbeddingRequest) GetDimensions() int32 {
|
| 693 |
+
if x != nil {
|
| 694 |
+
return x.Dimensions
|
| 695 |
+
}
|
| 696 |
+
return 0
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
// EmbeddingResponse contains the embedding
|
| 700 |
+
type EmbeddingResponse struct {
|
| 701 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 702 |
+
// Embedding vector
|
| 703 |
+
Embedding []float32 `protobuf:"fixed32,1,rep,packed,name=embedding,proto3" json:"embedding,omitempty"`
|
| 704 |
+
// Model used
|
| 705 |
+
Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"`
|
| 706 |
+
// Token usage
|
| 707 |
+
Tokens int32 `protobuf:"varint,3,opt,name=tokens,proto3" json:"tokens,omitempty"`
|
| 708 |
+
// Dimensions
|
| 709 |
+
Dimensions int32 `protobuf:"varint,4,opt,name=dimensions,proto3" json:"dimensions,omitempty"`
|
| 710 |
+
unknownFields protoimpl.UnknownFields
|
| 711 |
+
sizeCache protoimpl.SizeCache
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
func (x *EmbeddingResponse) Reset() {
|
| 715 |
+
*x = EmbeddingResponse{}
|
| 716 |
+
mi := &file_generator_proto_msgTypes[9]
|
| 717 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 718 |
+
ms.StoreMessageInfo(mi)
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
func (x *EmbeddingResponse) String() string {
|
| 722 |
+
return protoimpl.X.MessageStringOf(x)
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
func (*EmbeddingResponse) ProtoMessage() {}
|
| 726 |
+
|
| 727 |
+
func (x *EmbeddingResponse) ProtoReflect() protoreflect.Message {
|
| 728 |
+
mi := &file_generator_proto_msgTypes[9]
|
| 729 |
+
if x != nil {
|
| 730 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 731 |
+
if ms.LoadMessageInfo() == nil {
|
| 732 |
+
ms.StoreMessageInfo(mi)
|
| 733 |
+
}
|
| 734 |
+
return ms
|
| 735 |
+
}
|
| 736 |
+
return mi.MessageOf(x)
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
// Deprecated: Use EmbeddingResponse.ProtoReflect.Descriptor instead.
|
| 740 |
+
func (*EmbeddingResponse) Descriptor() ([]byte, []int) {
|
| 741 |
+
return file_generator_proto_rawDescGZIP(), []int{9}
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
func (x *EmbeddingResponse) GetEmbedding() []float32 {
|
| 745 |
+
if x != nil {
|
| 746 |
+
return x.Embedding
|
| 747 |
+
}
|
| 748 |
+
return nil
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
func (x *EmbeddingResponse) GetModel() string {
|
| 752 |
+
if x != nil {
|
| 753 |
+
return x.Model
|
| 754 |
+
}
|
| 755 |
+
return ""
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
func (x *EmbeddingResponse) GetTokens() int32 {
|
| 759 |
+
if x != nil {
|
| 760 |
+
return x.Tokens
|
| 761 |
+
}
|
| 762 |
+
return 0
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
func (x *EmbeddingResponse) GetDimensions() int32 {
|
| 766 |
+
if x != nil {
|
| 767 |
+
return x.Dimensions
|
| 768 |
+
}
|
| 769 |
+
return 0
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
// BatchEmbeddingRequest for bulk embeddings
|
| 773 |
+
type BatchEmbeddingRequest struct {
|
| 774 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 775 |
+
// Texts to embed
|
| 776 |
+
Texts []string `protobuf:"bytes,1,rep,name=texts,proto3" json:"texts,omitempty"`
|
| 777 |
+
// Model to use
|
| 778 |
+
Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"`
|
| 779 |
+
// Embedding dimensions
|
| 780 |
+
Dimensions int32 `protobuf:"varint,3,opt,name=dimensions,proto3" json:"dimensions,omitempty"`
|
| 781 |
+
unknownFields protoimpl.UnknownFields
|
| 782 |
+
sizeCache protoimpl.SizeCache
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
func (x *BatchEmbeddingRequest) Reset() {
|
| 786 |
+
*x = BatchEmbeddingRequest{}
|
| 787 |
+
mi := &file_generator_proto_msgTypes[10]
|
| 788 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 789 |
+
ms.StoreMessageInfo(mi)
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
func (x *BatchEmbeddingRequest) String() string {
|
| 793 |
+
return protoimpl.X.MessageStringOf(x)
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
func (*BatchEmbeddingRequest) ProtoMessage() {}
|
| 797 |
+
|
| 798 |
+
func (x *BatchEmbeddingRequest) ProtoReflect() protoreflect.Message {
|
| 799 |
+
mi := &file_generator_proto_msgTypes[10]
|
| 800 |
+
if x != nil {
|
| 801 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 802 |
+
if ms.LoadMessageInfo() == nil {
|
| 803 |
+
ms.StoreMessageInfo(mi)
|
| 804 |
+
}
|
| 805 |
+
return ms
|
| 806 |
+
}
|
| 807 |
+
return mi.MessageOf(x)
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
// Deprecated: Use BatchEmbeddingRequest.ProtoReflect.Descriptor instead.
|
| 811 |
+
func (*BatchEmbeddingRequest) Descriptor() ([]byte, []int) {
|
| 812 |
+
return file_generator_proto_rawDescGZIP(), []int{10}
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
func (x *BatchEmbeddingRequest) GetTexts() []string {
|
| 816 |
+
if x != nil {
|
| 817 |
+
return x.Texts
|
| 818 |
+
}
|
| 819 |
+
return nil
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
func (x *BatchEmbeddingRequest) GetModel() string {
|
| 823 |
+
if x != nil {
|
| 824 |
+
return x.Model
|
| 825 |
+
}
|
| 826 |
+
return ""
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
func (x *BatchEmbeddingRequest) GetDimensions() int32 {
|
| 830 |
+
if x != nil {
|
| 831 |
+
return x.Dimensions
|
| 832 |
+
}
|
| 833 |
+
return 0
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
// BatchEmbeddingResponse for bulk embeddings
|
| 837 |
+
type BatchEmbeddingResponse struct {
|
| 838 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 839 |
+
// Embeddings
|
| 840 |
+
Embeddings []*EmbeddingResult `protobuf:"bytes,1,rep,name=embeddings,proto3" json:"embeddings,omitempty"`
|
| 841 |
+
// Total tokens used
|
| 842 |
+
TotalTokens int32 `protobuf:"varint,2,opt,name=total_tokens,json=totalTokens,proto3" json:"total_tokens,omitempty"`
|
| 843 |
+
// Model used
|
| 844 |
+
Model string `protobuf:"bytes,3,opt,name=model,proto3" json:"model,omitempty"`
|
| 845 |
+
unknownFields protoimpl.UnknownFields
|
| 846 |
+
sizeCache protoimpl.SizeCache
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
func (x *BatchEmbeddingResponse) Reset() {
|
| 850 |
+
*x = BatchEmbeddingResponse{}
|
| 851 |
+
mi := &file_generator_proto_msgTypes[11]
|
| 852 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 853 |
+
ms.StoreMessageInfo(mi)
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
func (x *BatchEmbeddingResponse) String() string {
|
| 857 |
+
return protoimpl.X.MessageStringOf(x)
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
func (*BatchEmbeddingResponse) ProtoMessage() {}
|
| 861 |
+
|
| 862 |
+
func (x *BatchEmbeddingResponse) ProtoReflect() protoreflect.Message {
|
| 863 |
+
mi := &file_generator_proto_msgTypes[11]
|
| 864 |
+
if x != nil {
|
| 865 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 866 |
+
if ms.LoadMessageInfo() == nil {
|
| 867 |
+
ms.StoreMessageInfo(mi)
|
| 868 |
+
}
|
| 869 |
+
return ms
|
| 870 |
+
}
|
| 871 |
+
return mi.MessageOf(x)
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
// Deprecated: Use BatchEmbeddingResponse.ProtoReflect.Descriptor instead.
|
| 875 |
+
func (*BatchEmbeddingResponse) Descriptor() ([]byte, []int) {
|
| 876 |
+
return file_generator_proto_rawDescGZIP(), []int{11}
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
func (x *BatchEmbeddingResponse) GetEmbeddings() []*EmbeddingResult {
|
| 880 |
+
if x != nil {
|
| 881 |
+
return x.Embeddings
|
| 882 |
+
}
|
| 883 |
+
return nil
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
func (x *BatchEmbeddingResponse) GetTotalTokens() int32 {
|
| 887 |
+
if x != nil {
|
| 888 |
+
return x.TotalTokens
|
| 889 |
+
}
|
| 890 |
+
return 0
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
func (x *BatchEmbeddingResponse) GetModel() string {
|
| 894 |
+
if x != nil {
|
| 895 |
+
return x.Model
|
| 896 |
+
}
|
| 897 |
+
return ""
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
// EmbeddingResult for individual embedding
|
| 901 |
+
type EmbeddingResult struct {
|
| 902 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 903 |
+
// Index in the batch
|
| 904 |
+
Index int32 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"`
|
| 905 |
+
// Embedding vector
|
| 906 |
+
Embedding []float32 `protobuf:"fixed32,2,rep,packed,name=embedding,proto3" json:"embedding,omitempty"`
|
| 907 |
+
// Tokens used
|
| 908 |
+
Tokens int32 `protobuf:"varint,3,opt,name=tokens,proto3" json:"tokens,omitempty"`
|
| 909 |
+
unknownFields protoimpl.UnknownFields
|
| 910 |
+
sizeCache protoimpl.SizeCache
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
func (x *EmbeddingResult) Reset() {
|
| 914 |
+
*x = EmbeddingResult{}
|
| 915 |
+
mi := &file_generator_proto_msgTypes[12]
|
| 916 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 917 |
+
ms.StoreMessageInfo(mi)
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
func (x *EmbeddingResult) String() string {
|
| 921 |
+
return protoimpl.X.MessageStringOf(x)
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
func (*EmbeddingResult) ProtoMessage() {}
|
| 925 |
+
|
| 926 |
+
func (x *EmbeddingResult) ProtoReflect() protoreflect.Message {
|
| 927 |
+
mi := &file_generator_proto_msgTypes[12]
|
| 928 |
+
if x != nil {
|
| 929 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 930 |
+
if ms.LoadMessageInfo() == nil {
|
| 931 |
+
ms.StoreMessageInfo(mi)
|
| 932 |
+
}
|
| 933 |
+
return ms
|
| 934 |
+
}
|
| 935 |
+
return mi.MessageOf(x)
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
// Deprecated: Use EmbeddingResult.ProtoReflect.Descriptor instead.
|
| 939 |
+
func (*EmbeddingResult) Descriptor() ([]byte, []int) {
|
| 940 |
+
return file_generator_proto_rawDescGZIP(), []int{12}
|
| 941 |
+
}
|
| 942 |
+
|
| 943 |
+
func (x *EmbeddingResult) GetIndex() int32 {
|
| 944 |
+
if x != nil {
|
| 945 |
+
return x.Index
|
| 946 |
+
}
|
| 947 |
+
return 0
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
func (x *EmbeddingResult) GetEmbedding() []float32 {
|
| 951 |
+
if x != nil {
|
| 952 |
+
return x.Embedding
|
| 953 |
+
}
|
| 954 |
+
return nil
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
func (x *EmbeddingResult) GetTokens() int32 {
|
| 958 |
+
if x != nil {
|
| 959 |
+
return x.Tokens
|
| 960 |
+
}
|
| 961 |
+
return 0
|
| 962 |
+
}
|
| 963 |
+
|
| 964 |
+
// RerankRequest for document reranking
|
| 965 |
+
type RerankRequest struct {
|
| 966 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 967 |
+
// Query for relevance scoring
|
| 968 |
+
Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"`
|
| 969 |
+
// Documents to rerank
|
| 970 |
+
Documents []*RerankDocument `protobuf:"bytes,2,rep,name=documents,proto3" json:"documents,omitempty"`
|
| 971 |
+
// Number of top results to return
|
| 972 |
+
TopN int32 `protobuf:"varint,3,opt,name=top_n,json=topN,proto3" json:"top_n,omitempty"`
|
| 973 |
+
// Model to use for reranking
|
| 974 |
+
Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"`
|
| 975 |
+
unknownFields protoimpl.UnknownFields
|
| 976 |
+
sizeCache protoimpl.SizeCache
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
func (x *RerankRequest) Reset() {
|
| 980 |
+
*x = RerankRequest{}
|
| 981 |
+
mi := &file_generator_proto_msgTypes[13]
|
| 982 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 983 |
+
ms.StoreMessageInfo(mi)
|
| 984 |
+
}
|
| 985 |
+
|
| 986 |
+
func (x *RerankRequest) String() string {
|
| 987 |
+
return protoimpl.X.MessageStringOf(x)
|
| 988 |
+
}
|
| 989 |
+
|
| 990 |
+
func (*RerankRequest) ProtoMessage() {}
|
| 991 |
+
|
| 992 |
+
func (x *RerankRequest) ProtoReflect() protoreflect.Message {
|
| 993 |
+
mi := &file_generator_proto_msgTypes[13]
|
| 994 |
+
if x != nil {
|
| 995 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 996 |
+
if ms.LoadMessageInfo() == nil {
|
| 997 |
+
ms.StoreMessageInfo(mi)
|
| 998 |
+
}
|
| 999 |
+
return ms
|
| 1000 |
+
}
|
| 1001 |
+
return mi.MessageOf(x)
|
| 1002 |
+
}
|
| 1003 |
+
|
| 1004 |
+
// Deprecated: Use RerankRequest.ProtoReflect.Descriptor instead.
|
| 1005 |
+
func (*RerankRequest) Descriptor() ([]byte, []int) {
|
| 1006 |
+
return file_generator_proto_rawDescGZIP(), []int{13}
|
| 1007 |
+
}
|
| 1008 |
+
|
| 1009 |
+
func (x *RerankRequest) GetQuery() string {
|
| 1010 |
+
if x != nil {
|
| 1011 |
+
return x.Query
|
| 1012 |
+
}
|
| 1013 |
+
return ""
|
| 1014 |
+
}
|
| 1015 |
+
|
| 1016 |
+
func (x *RerankRequest) GetDocuments() []*RerankDocument {
|
| 1017 |
+
if x != nil {
|
| 1018 |
+
return x.Documents
|
| 1019 |
+
}
|
| 1020 |
+
return nil
|
| 1021 |
+
}
|
| 1022 |
+
|
| 1023 |
+
func (x *RerankRequest) GetTopN() int32 {
|
| 1024 |
+
if x != nil {
|
| 1025 |
+
return x.TopN
|
| 1026 |
+
}
|
| 1027 |
+
return 0
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
func (x *RerankRequest) GetModel() string {
|
| 1031 |
+
if x != nil {
|
| 1032 |
+
return x.Model
|
| 1033 |
+
}
|
| 1034 |
+
return ""
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
// RerankDocument for reranking input
|
| 1038 |
+
type RerankDocument struct {
|
| 1039 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1040 |
+
// Document ID
|
| 1041 |
+
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
| 1042 |
+
// Document content
|
| 1043 |
+
Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
|
| 1044 |
+
// Original score (optional)
|
| 1045 |
+
OriginalScore float32 `protobuf:"fixed32,3,opt,name=original_score,json=originalScore,proto3" json:"original_score,omitempty"`
|
| 1046 |
+
unknownFields protoimpl.UnknownFields
|
| 1047 |
+
sizeCache protoimpl.SizeCache
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
func (x *RerankDocument) Reset() {
|
| 1051 |
+
*x = RerankDocument{}
|
| 1052 |
+
mi := &file_generator_proto_msgTypes[14]
|
| 1053 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1054 |
+
ms.StoreMessageInfo(mi)
|
| 1055 |
+
}
|
| 1056 |
+
|
| 1057 |
+
func (x *RerankDocument) String() string {
|
| 1058 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1059 |
+
}
|
| 1060 |
+
|
| 1061 |
+
func (*RerankDocument) ProtoMessage() {}
|
| 1062 |
+
|
| 1063 |
+
func (x *RerankDocument) ProtoReflect() protoreflect.Message {
|
| 1064 |
+
mi := &file_generator_proto_msgTypes[14]
|
| 1065 |
+
if x != nil {
|
| 1066 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1067 |
+
if ms.LoadMessageInfo() == nil {
|
| 1068 |
+
ms.StoreMessageInfo(mi)
|
| 1069 |
+
}
|
| 1070 |
+
return ms
|
| 1071 |
+
}
|
| 1072 |
+
return mi.MessageOf(x)
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
// Deprecated: Use RerankDocument.ProtoReflect.Descriptor instead.
|
| 1076 |
+
func (*RerankDocument) Descriptor() ([]byte, []int) {
|
| 1077 |
+
return file_generator_proto_rawDescGZIP(), []int{14}
|
| 1078 |
+
}
|
| 1079 |
+
|
| 1080 |
+
func (x *RerankDocument) GetId() string {
|
| 1081 |
+
if x != nil {
|
| 1082 |
+
return x.Id
|
| 1083 |
+
}
|
| 1084 |
+
return ""
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
func (x *RerankDocument) GetContent() string {
|
| 1088 |
+
if x != nil {
|
| 1089 |
+
return x.Content
|
| 1090 |
+
}
|
| 1091 |
+
return ""
|
| 1092 |
+
}
|
| 1093 |
+
|
| 1094 |
+
func (x *RerankDocument) GetOriginalScore() float32 {
|
| 1095 |
+
if x != nil {
|
| 1096 |
+
return x.OriginalScore
|
| 1097 |
+
}
|
| 1098 |
+
return 0
|
| 1099 |
+
}
|
| 1100 |
+
|
| 1101 |
+
// RerankResponse contains reranked documents
|
| 1102 |
+
type RerankResponse struct {
|
| 1103 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1104 |
+
// Reranked results
|
| 1105 |
+
Results []*RerankResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"`
|
| 1106 |
+
// Model used
|
| 1107 |
+
Model string `protobuf:"bytes,2,opt,name=model,proto3" json:"model,omitempty"`
|
| 1108 |
+
unknownFields protoimpl.UnknownFields
|
| 1109 |
+
sizeCache protoimpl.SizeCache
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
+
func (x *RerankResponse) Reset() {
|
| 1113 |
+
*x = RerankResponse{}
|
| 1114 |
+
mi := &file_generator_proto_msgTypes[15]
|
| 1115 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1116 |
+
ms.StoreMessageInfo(mi)
|
| 1117 |
+
}
|
| 1118 |
+
|
| 1119 |
+
func (x *RerankResponse) String() string {
|
| 1120 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
func (*RerankResponse) ProtoMessage() {}
|
| 1124 |
+
|
| 1125 |
+
func (x *RerankResponse) ProtoReflect() protoreflect.Message {
|
| 1126 |
+
mi := &file_generator_proto_msgTypes[15]
|
| 1127 |
+
if x != nil {
|
| 1128 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1129 |
+
if ms.LoadMessageInfo() == nil {
|
| 1130 |
+
ms.StoreMessageInfo(mi)
|
| 1131 |
+
}
|
| 1132 |
+
return ms
|
| 1133 |
+
}
|
| 1134 |
+
return mi.MessageOf(x)
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
// Deprecated: Use RerankResponse.ProtoReflect.Descriptor instead.
|
| 1138 |
+
func (*RerankResponse) Descriptor() ([]byte, []int) {
|
| 1139 |
+
return file_generator_proto_rawDescGZIP(), []int{15}
|
| 1140 |
+
}
|
| 1141 |
+
|
| 1142 |
+
func (x *RerankResponse) GetResults() []*RerankResult {
|
| 1143 |
+
if x != nil {
|
| 1144 |
+
return x.Results
|
| 1145 |
+
}
|
| 1146 |
+
return nil
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
+
func (x *RerankResponse) GetModel() string {
|
| 1150 |
+
if x != nil {
|
| 1151 |
+
return x.Model
|
| 1152 |
+
}
|
| 1153 |
+
return ""
|
| 1154 |
+
}
|
| 1155 |
+
|
| 1156 |
+
// RerankResult for reranked document
|
| 1157 |
+
type RerankResult struct {
|
| 1158 |
+
state protoimpl.MessageState `protogen:"open.v1"`
|
| 1159 |
+
// Document ID
|
| 1160 |
+
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
| 1161 |
+
// New relevance score
|
| 1162 |
+
Score float32 `protobuf:"fixed32,2,opt,name=score,proto3" json:"score,omitempty"`
|
| 1163 |
+
// New rank position
|
| 1164 |
+
Rank int32 `protobuf:"varint,3,opt,name=rank,proto3" json:"rank,omitempty"`
|
| 1165 |
+
// Original rank position
|
| 1166 |
+
OriginalRank int32 `protobuf:"varint,4,opt,name=original_rank,json=originalRank,proto3" json:"original_rank,omitempty"`
|
| 1167 |
+
unknownFields protoimpl.UnknownFields
|
| 1168 |
+
sizeCache protoimpl.SizeCache
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
func (x *RerankResult) Reset() {
|
| 1172 |
+
*x = RerankResult{}
|
| 1173 |
+
mi := &file_generator_proto_msgTypes[16]
|
| 1174 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1175 |
+
ms.StoreMessageInfo(mi)
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
func (x *RerankResult) String() string {
|
| 1179 |
+
return protoimpl.X.MessageStringOf(x)
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
func (*RerankResult) ProtoMessage() {}
|
| 1183 |
+
|
| 1184 |
+
func (x *RerankResult) ProtoReflect() protoreflect.Message {
|
| 1185 |
+
mi := &file_generator_proto_msgTypes[16]
|
| 1186 |
+
if x != nil {
|
| 1187 |
+
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
| 1188 |
+
if ms.LoadMessageInfo() == nil {
|
| 1189 |
+
ms.StoreMessageInfo(mi)
|
| 1190 |
+
}
|
| 1191 |
+
return ms
|
| 1192 |
+
}
|
| 1193 |
+
return mi.MessageOf(x)
|
| 1194 |
+
}
|
| 1195 |
+
|
| 1196 |
+
// Deprecated: Use RerankResult.ProtoReflect.Descriptor instead.
|
| 1197 |
+
func (*RerankResult) Descriptor() ([]byte, []int) {
|
| 1198 |
+
return file_generator_proto_rawDescGZIP(), []int{16}
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
func (x *RerankResult) GetId() string {
|
| 1202 |
+
if x != nil {
|
| 1203 |
+
return x.Id
|
| 1204 |
+
}
|
| 1205 |
+
return ""
|
| 1206 |
+
}
|
| 1207 |
+
|
| 1208 |
+
func (x *RerankResult) GetScore() float32 {
|
| 1209 |
+
if x != nil {
|
| 1210 |
+
return x.Score
|
| 1211 |
+
}
|
| 1212 |
+
return 0
|
| 1213 |
+
}
|
| 1214 |
+
|
| 1215 |
+
func (x *RerankResult) GetRank() int32 {
|
| 1216 |
+
if x != nil {
|
| 1217 |
+
return x.Rank
|
| 1218 |
+
}
|
| 1219 |
+
return 0
|
| 1220 |
+
}
|
| 1221 |
+
|
| 1222 |
+
func (x *RerankResult) GetOriginalRank() int32 {
|
| 1223 |
+
if x != nil {
|
| 1224 |
+
return x.OriginalRank
|
| 1225 |
+
}
|
| 1226 |
+
return 0
|
| 1227 |
+
}
|
| 1228 |
+
|
| 1229 |
+
var File_generator_proto protoreflect.FileDescriptor
|
| 1230 |
+
|
| 1231 |
+
const file_generator_proto_rawDesc = "" +
|
| 1232 |
+
"\n" +
|
| 1233 |
+
"\x0fgenerator.proto\x12\x06rag.v1\"\xe0\x01\n" +
|
| 1234 |
+
"\x0fGenerateRequest\x12#\n" +
|
| 1235 |
+
"\rsystem_prompt\x18\x01 \x01(\tR\fsystemPrompt\x12\x16\n" +
|
| 1236 |
+
"\x06prompt\x18\x02 \x01(\tR\x06prompt\x121\n" +
|
| 1237 |
+
"\acontext\x18\x03 \x03(\v2\x17.rag.v1.ContextDocumentR\acontext\x12-\n" +
|
| 1238 |
+
"\ahistory\x18\x04 \x03(\v2\x13.rag.v1.ChatMessageR\ahistory\x12.\n" +
|
| 1239 |
+
"\x06config\x18\x05 \x01(\v2\x16.rag.v1.GenerateConfigR\x06config\"o\n" +
|
| 1240 |
+
"\x0fContextDocument\x12\x18\n" +
|
| 1241 |
+
"\acontent\x18\x01 \x01(\tR\acontent\x12\x14\n" +
|
| 1242 |
+
"\x05title\x18\x02 \x01(\tR\x05title\x12\x16\n" +
|
| 1243 |
+
"\x06source\x18\x03 \x01(\tR\x06source\x12\x14\n" +
|
| 1244 |
+
"\x05score\x18\x04 \x01(\x02R\x05score\";\n" +
|
| 1245 |
+
"\vChatMessage\x12\x12\n" +
|
| 1246 |
+
"\x04role\x18\x01 \x01(\tR\x04role\x12\x18\n" +
|
| 1247 |
+
"\acontent\x18\x02 \x01(\tR\acontent\"\xa4\x02\n" +
|
| 1248 |
+
"\x0eGenerateConfig\x12\x14\n" +
|
| 1249 |
+
"\x05model\x18\x01 \x01(\tR\x05model\x12 \n" +
|
| 1250 |
+
"\vtemperature\x18\x02 \x01(\x02R\vtemperature\x12\x1d\n" +
|
| 1251 |
+
"\n" +
|
| 1252 |
+
"max_tokens\x18\x03 \x01(\x05R\tmaxTokens\x12\x13\n" +
|
| 1253 |
+
"\x05top_p\x18\x04 \x01(\x02R\x04topP\x12+\n" +
|
| 1254 |
+
"\x11frequency_penalty\x18\x05 \x01(\x02R\x10frequencyPenalty\x12)\n" +
|
| 1255 |
+
"\x10presence_penalty\x18\x06 \x01(\x02R\x0fpresencePenalty\x12%\n" +
|
| 1256 |
+
"\x0estop_sequences\x18\a \x03(\tR\rstopSequences\x12'\n" +
|
| 1257 |
+
"\x0fresponse_format\x18\b \x01(\tR\x0eresponseFormat\"\xc1\x01\n" +
|
| 1258 |
+
"\x10GenerateResponse\x12\x12\n" +
|
| 1259 |
+
"\x04text\x18\x01 \x01(\tR\x04text\x12(\n" +
|
| 1260 |
+
"\x05usage\x18\x02 \x01(\v2\x12.rag.v1.TokenUsageR\x05usage\x12#\n" +
|
| 1261 |
+
"\rfinish_reason\x18\x03 \x01(\tR\ffinishReason\x12\x14\n" +
|
| 1262 |
+
"\x05model\x18\x04 \x01(\tR\x05model\x124\n" +
|
| 1263 |
+
"\bmetadata\x18\x05 \x01(\v2\x18.rag.v1.GenerateMetadataR\bmetadata\"\xa8\x01\n" +
|
| 1264 |
+
"\n" +
|
| 1265 |
+
"TokenUsage\x12#\n" +
|
| 1266 |
+
"\rprompt_tokens\x18\x01 \x01(\x05R\fpromptTokens\x12+\n" +
|
| 1267 |
+
"\x11completion_tokens\x18\x02 \x01(\x05R\x10completionTokens\x12!\n" +
|
| 1268 |
+
"\ftotal_tokens\x18\x03 \x01(\x05R\vtotalTokens\x12%\n" +
|
| 1269 |
+
"\x0eestimated_cost\x18\x04 \x01(\x02R\restimatedCost\"h\n" +
|
| 1270 |
+
"\x10GenerateMetadata\x12\x1d\n" +
|
| 1271 |
+
"\n" +
|
| 1272 |
+
"latency_ms\x18\x01 \x01(\x03R\tlatencyMs\x12\x1a\n" +
|
| 1273 |
+
"\bprovider\x18\x02 \x01(\tR\bprovider\x12\x19\n" +
|
| 1274 |
+
"\btrace_id\x18\x03 \x01(\tR\atraceId\"\x8f\x01\n" +
|
| 1275 |
+
"\rGenerateChunk\x12\x14\n" +
|
| 1276 |
+
"\x05delta\x18\x01 \x01(\tR\x05delta\x12\x19\n" +
|
| 1277 |
+
"\bis_final\x18\x02 \x01(\bR\aisFinal\x12(\n" +
|
| 1278 |
+
"\x05usage\x18\x03 \x01(\v2\x12.rag.v1.TokenUsageR\x05usage\x12#\n" +
|
| 1279 |
+
"\rfinish_reason\x18\x04 \x01(\tR\ffinishReason\"\\\n" +
|
| 1280 |
+
"\x10EmbeddingRequest\x12\x12\n" +
|
| 1281 |
+
"\x04text\x18\x01 \x01(\tR\x04text\x12\x14\n" +
|
| 1282 |
+
"\x05model\x18\x02 \x01(\tR\x05model\x12\x1e\n" +
|
| 1283 |
+
"\n" +
|
| 1284 |
+
"dimensions\x18\x03 \x01(\x05R\n" +
|
| 1285 |
+
"dimensions\"\x7f\n" +
|
| 1286 |
+
"\x11EmbeddingResponse\x12\x1c\n" +
|
| 1287 |
+
"\tembedding\x18\x01 \x03(\x02R\tembedding\x12\x14\n" +
|
| 1288 |
+
"\x05model\x18\x02 \x01(\tR\x05model\x12\x16\n" +
|
| 1289 |
+
"\x06tokens\x18\x03 \x01(\x05R\x06tokens\x12\x1e\n" +
|
| 1290 |
+
"\n" +
|
| 1291 |
+
"dimensions\x18\x04 \x01(\x05R\n" +
|
| 1292 |
+
"dimensions\"c\n" +
|
| 1293 |
+
"\x15BatchEmbeddingRequest\x12\x14\n" +
|
| 1294 |
+
"\x05texts\x18\x01 \x03(\tR\x05texts\x12\x14\n" +
|
| 1295 |
+
"\x05model\x18\x02 \x01(\tR\x05model\x12\x1e\n" +
|
| 1296 |
+
"\n" +
|
| 1297 |
+
"dimensions\x18\x03 \x01(\x05R\n" +
|
| 1298 |
+
"dimensions\"\x8a\x01\n" +
|
| 1299 |
+
"\x16BatchEmbeddingResponse\x127\n" +
|
| 1300 |
+
"\n" +
|
| 1301 |
+
"embeddings\x18\x01 \x03(\v2\x17.rag.v1.EmbeddingResultR\n" +
|
| 1302 |
+
"embeddings\x12!\n" +
|
| 1303 |
+
"\ftotal_tokens\x18\x02 \x01(\x05R\vtotalTokens\x12\x14\n" +
|
| 1304 |
+
"\x05model\x18\x03 \x01(\tR\x05model\"]\n" +
|
| 1305 |
+
"\x0fEmbeddingResult\x12\x14\n" +
|
| 1306 |
+
"\x05index\x18\x01 \x01(\x05R\x05index\x12\x1c\n" +
|
| 1307 |
+
"\tembedding\x18\x02 \x03(\x02R\tembedding\x12\x16\n" +
|
| 1308 |
+
"\x06tokens\x18\x03 \x01(\x05R\x06tokens\"\x86\x01\n" +
|
| 1309 |
+
"\rRerankRequest\x12\x14\n" +
|
| 1310 |
+
"\x05query\x18\x01 \x01(\tR\x05query\x124\n" +
|
| 1311 |
+
"\tdocuments\x18\x02 \x03(\v2\x16.rag.v1.RerankDocumentR\tdocuments\x12\x13\n" +
|
| 1312 |
+
"\x05top_n\x18\x03 \x01(\x05R\x04topN\x12\x14\n" +
|
| 1313 |
+
"\x05model\x18\x04 \x01(\tR\x05model\"a\n" +
|
| 1314 |
+
"\x0eRerankDocument\x12\x0e\n" +
|
| 1315 |
+
"\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" +
|
| 1316 |
+
"\acontent\x18\x02 \x01(\tR\acontent\x12%\n" +
|
| 1317 |
+
"\x0eoriginal_score\x18\x03 \x01(\x02R\roriginalScore\"V\n" +
|
| 1318 |
+
"\x0eRerankResponse\x12.\n" +
|
| 1319 |
+
"\aresults\x18\x01 \x03(\v2\x14.rag.v1.RerankResultR\aresults\x12\x14\n" +
|
| 1320 |
+
"\x05model\x18\x02 \x01(\tR\x05model\"m\n" +
|
| 1321 |
+
"\fRerankResult\x12\x0e\n" +
|
| 1322 |
+
"\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
|
| 1323 |
+
"\x05score\x18\x02 \x01(\x02R\x05score\x12\x12\n" +
|
| 1324 |
+
"\x04rank\x18\x03 \x01(\x05R\x04rank\x12#\n" +
|
| 1325 |
+
"\roriginal_rank\x18\x04 \x01(\x05R\foriginalRank2\xf2\x02\n" +
|
| 1326 |
+
"\x10GeneratorService\x12=\n" +
|
| 1327 |
+
"\bGenerate\x12\x17.rag.v1.GenerateRequest\x1a\x18.rag.v1.GenerateResponse\x12B\n" +
|
| 1328 |
+
"\x0eGenerateStream\x12\x17.rag.v1.GenerateRequest\x1a\x15.rag.v1.GenerateChunk0\x01\x12H\n" +
|
| 1329 |
+
"\x11GenerateEmbedding\x12\x18.rag.v1.EmbeddingRequest\x1a\x19.rag.v1.EmbeddingResponse\x12X\n" +
|
| 1330 |
+
"\x17BatchGenerateEmbeddings\x12\x1d.rag.v1.BatchEmbeddingRequest\x1a\x1e.rag.v1.BatchEmbeddingResponse\x127\n" +
|
| 1331 |
+
"\x06Rerank\x12\x15.rag.v1.RerankRequest\x1a\x16.rag.v1.RerankResponseB6Z4github.com/AmaniQuery/amaniquery/pkg/proto/gen/ragv1b\x06proto3"
|
| 1332 |
+
|
| 1333 |
+
var (
|
| 1334 |
+
file_generator_proto_rawDescOnce sync.Once
|
| 1335 |
+
file_generator_proto_rawDescData []byte
|
| 1336 |
+
)
|
| 1337 |
+
|
| 1338 |
+
func file_generator_proto_rawDescGZIP() []byte {
|
| 1339 |
+
file_generator_proto_rawDescOnce.Do(func() {
|
| 1340 |
+
file_generator_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_generator_proto_rawDesc), len(file_generator_proto_rawDesc)))
|
| 1341 |
+
})
|
| 1342 |
+
return file_generator_proto_rawDescData
|
| 1343 |
+
}
|
| 1344 |
+
|
| 1345 |
+
var file_generator_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
|
| 1346 |
+
var file_generator_proto_goTypes = []any{
|
| 1347 |
+
(*GenerateRequest)(nil), // 0: rag.v1.GenerateRequest
|
| 1348 |
+
(*ContextDocument)(nil), // 1: rag.v1.ContextDocument
|
| 1349 |
+
(*ChatMessage)(nil), // 2: rag.v1.ChatMessage
|
| 1350 |
+
(*GenerateConfig)(nil), // 3: rag.v1.GenerateConfig
|
| 1351 |
+
(*GenerateResponse)(nil), // 4: rag.v1.GenerateResponse
|
| 1352 |
+
(*TokenUsage)(nil), // 5: rag.v1.TokenUsage
|
| 1353 |
+
(*GenerateMetadata)(nil), // 6: rag.v1.GenerateMetadata
|
| 1354 |
+
(*GenerateChunk)(nil), // 7: rag.v1.GenerateChunk
|
| 1355 |
+
(*EmbeddingRequest)(nil), // 8: rag.v1.EmbeddingRequest
|
| 1356 |
+
(*EmbeddingResponse)(nil), // 9: rag.v1.EmbeddingResponse
|
| 1357 |
+
(*BatchEmbeddingRequest)(nil), // 10: rag.v1.BatchEmbeddingRequest
|
| 1358 |
+
(*BatchEmbeddingResponse)(nil), // 11: rag.v1.BatchEmbeddingResponse
|
| 1359 |
+
(*EmbeddingResult)(nil), // 12: rag.v1.EmbeddingResult
|
| 1360 |
+
(*RerankRequest)(nil), // 13: rag.v1.RerankRequest
|
| 1361 |
+
(*RerankDocument)(nil), // 14: rag.v1.RerankDocument
|
| 1362 |
+
(*RerankResponse)(nil), // 15: rag.v1.RerankResponse
|
| 1363 |
+
(*RerankResult)(nil), // 16: rag.v1.RerankResult
|
| 1364 |
+
}
|
| 1365 |
+
var file_generator_proto_depIdxs = []int32{
|
| 1366 |
+
1, // 0: rag.v1.GenerateRequest.context:type_name -> rag.v1.ContextDocument
|
| 1367 |
+
2, // 1: rag.v1.GenerateRequest.history:type_name -> rag.v1.ChatMessage
|
| 1368 |
+
3, // 2: rag.v1.GenerateRequest.config:type_name -> rag.v1.GenerateConfig
|
| 1369 |
+
5, // 3: rag.v1.GenerateResponse.usage:type_name -> rag.v1.TokenUsage
|
| 1370 |
+
6, // 4: rag.v1.GenerateResponse.metadata:type_name -> rag.v1.GenerateMetadata
|
| 1371 |
+
5, // 5: rag.v1.GenerateChunk.usage:type_name -> rag.v1.TokenUsage
|
| 1372 |
+
12, // 6: rag.v1.BatchEmbeddingResponse.embeddings:type_name -> rag.v1.EmbeddingResult
|
| 1373 |
+
14, // 7: rag.v1.RerankRequest.documents:type_name -> rag.v1.RerankDocument
|
| 1374 |
+
16, // 8: rag.v1.RerankResponse.results:type_name -> rag.v1.RerankResult
|
| 1375 |
+
0, // 9: rag.v1.GeneratorService.Generate:input_type -> rag.v1.GenerateRequest
|
| 1376 |
+
0, // 10: rag.v1.GeneratorService.GenerateStream:input_type -> rag.v1.GenerateRequest
|
| 1377 |
+
8, // 11: rag.v1.GeneratorService.GenerateEmbedding:input_type -> rag.v1.EmbeddingRequest
|
| 1378 |
+
10, // 12: rag.v1.GeneratorService.BatchGenerateEmbeddings:input_type -> rag.v1.BatchEmbeddingRequest
|
| 1379 |
+
13, // 13: rag.v1.GeneratorService.Rerank:input_type -> rag.v1.RerankRequest
|
| 1380 |
+
4, // 14: rag.v1.GeneratorService.Generate:output_type -> rag.v1.GenerateResponse
|
| 1381 |
+
7, // 15: rag.v1.GeneratorService.GenerateStream:output_type -> rag.v1.GenerateChunk
|
| 1382 |
+
9, // 16: rag.v1.GeneratorService.GenerateEmbedding:output_type -> rag.v1.EmbeddingResponse
|
| 1383 |
+
11, // 17: rag.v1.GeneratorService.BatchGenerateEmbeddings:output_type -> rag.v1.BatchEmbeddingResponse
|
| 1384 |
+
15, // 18: rag.v1.GeneratorService.Rerank:output_type -> rag.v1.RerankResponse
|
| 1385 |
+
14, // [14:19] is the sub-list for method output_type
|
| 1386 |
+
9, // [9:14] is the sub-list for method input_type
|
| 1387 |
+
9, // [9:9] is the sub-list for extension type_name
|
| 1388 |
+
9, // [9:9] is the sub-list for extension extendee
|
| 1389 |
+
0, // [0:9] is the sub-list for field type_name
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
+
func init() { file_generator_proto_init() }
|
| 1393 |
+
func file_generator_proto_init() {
|
| 1394 |
+
if File_generator_proto != nil {
|
| 1395 |
+
return
|
| 1396 |
+
}
|
| 1397 |
+
type x struct{}
|
| 1398 |
+
out := protoimpl.TypeBuilder{
|
| 1399 |
+
File: protoimpl.DescBuilder{
|
| 1400 |
+
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
| 1401 |
+
RawDescriptor: unsafe.Slice(unsafe.StringData(file_generator_proto_rawDesc), len(file_generator_proto_rawDesc)),
|
| 1402 |
+
NumEnums: 0,
|
| 1403 |
+
NumMessages: 17,
|
| 1404 |
+
NumExtensions: 0,
|
| 1405 |
+
NumServices: 1,
|
| 1406 |
+
},
|
| 1407 |
+
GoTypes: file_generator_proto_goTypes,
|
| 1408 |
+
DependencyIndexes: file_generator_proto_depIdxs,
|
| 1409 |
+
MessageInfos: file_generator_proto_msgTypes,
|
| 1410 |
+
}.Build()
|
| 1411 |
+
File_generator_proto = out.File
|
| 1412 |
+
file_generator_proto_goTypes = nil
|
| 1413 |
+
file_generator_proto_depIdxs = nil
|
| 1414 |
+
}
|
generator_grpc.pb.go
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
| 2 |
+
// versions:
|
| 3 |
+
// - protoc-gen-go-grpc v1.6.0
|
| 4 |
+
// - protoc v6.33.2
|
| 5 |
+
// source: generator.proto
|
| 6 |
+
|
| 7 |
+
package ragv1
|
| 8 |
+
|
| 9 |
+
import (
|
| 10 |
+
context "context"
|
| 11 |
+
grpc "google.golang.org/grpc"
|
| 12 |
+
codes "google.golang.org/grpc/codes"
|
| 13 |
+
status "google.golang.org/grpc/status"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
// This is a compile-time assertion to ensure that this generated file
|
| 17 |
+
// is compatible with the grpc package it is being compiled against.
|
| 18 |
+
// Requires gRPC-Go v1.64.0 or later.
|
| 19 |
+
const _ = grpc.SupportPackageIsVersion9
|
| 20 |
+
|
| 21 |
+
const (
|
| 22 |
+
GeneratorService_Generate_FullMethodName = "/rag.v1.GeneratorService/Generate"
|
| 23 |
+
GeneratorService_GenerateStream_FullMethodName = "/rag.v1.GeneratorService/GenerateStream"
|
| 24 |
+
GeneratorService_GenerateEmbedding_FullMethodName = "/rag.v1.GeneratorService/GenerateEmbedding"
|
| 25 |
+
GeneratorService_BatchGenerateEmbeddings_FullMethodName = "/rag.v1.GeneratorService/BatchGenerateEmbeddings"
|
| 26 |
+
GeneratorService_Rerank_FullMethodName = "/rag.v1.GeneratorService/Rerank"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
// GeneratorServiceClient is the client API for GeneratorService service.
|
| 30 |
+
//
|
| 31 |
+
// 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.
|
| 32 |
+
//
|
| 33 |
+
// GeneratorService handles LLM-based response generation
|
| 34 |
+
type GeneratorServiceClient interface {
|
| 35 |
+
// Generate a complete response
|
| 36 |
+
Generate(ctx context.Context, in *GenerateRequest, opts ...grpc.CallOption) (*GenerateResponse, error)
|
| 37 |
+
// Generate with streaming output
|
| 38 |
+
GenerateStream(ctx context.Context, in *GenerateRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GenerateChunk], error)
|
| 39 |
+
// Generate embeddings for text
|
| 40 |
+
GenerateEmbedding(ctx context.Context, in *EmbeddingRequest, opts ...grpc.CallOption) (*EmbeddingResponse, error)
|
| 41 |
+
// Batch generate embeddings
|
| 42 |
+
BatchGenerateEmbeddings(ctx context.Context, in *BatchEmbeddingRequest, opts ...grpc.CallOption) (*BatchEmbeddingResponse, error)
|
| 43 |
+
// Rerank documents based on query relevance
|
| 44 |
+
Rerank(ctx context.Context, in *RerankRequest, opts ...grpc.CallOption) (*RerankResponse, error)
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
type generatorServiceClient struct {
|
| 48 |
+
cc grpc.ClientConnInterface
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
func NewGeneratorServiceClient(cc grpc.ClientConnInterface) GeneratorServiceClient {
|
| 52 |
+
return &generatorServiceClient{cc}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
func (c *generatorServiceClient) Generate(ctx context.Context, in *GenerateRequest, opts ...grpc.CallOption) (*GenerateResponse, error) {
|
| 56 |
+
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
| 57 |
+
out := new(GenerateResponse)
|
| 58 |
+
err := c.cc.Invoke(ctx, GeneratorService_Generate_FullMethodName, in, out, cOpts...)
|
| 59 |
+
if err != nil {
|
| 60 |
+
return nil, err
|
| 61 |
+
}
|
| 62 |
+
return out, nil
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
func (c *generatorServiceClient) GenerateStream(ctx context.Context, in *GenerateRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GenerateChunk], error) {
|
| 66 |
+
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
| 67 |
+
stream, err := c.cc.NewStream(ctx, &GeneratorService_ServiceDesc.Streams[0], GeneratorService_GenerateStream_FullMethodName, cOpts...)
|
| 68 |
+
if err != nil {
|
| 69 |
+
return nil, err
|
| 70 |
+
}
|
| 71 |
+
x := &grpc.GenericClientStream[GenerateRequest, GenerateChunk]{ClientStream: stream}
|
| 72 |
+
if err := x.ClientStream.SendMsg(in); err != nil {
|
| 73 |
+
return nil, err
|
| 74 |
+
}
|
| 75 |
+
if err := x.ClientStream.CloseSend(); err != nil {
|
| 76 |
+
return nil, err
|
| 77 |
+
}
|
| 78 |
+
return x, nil
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
| 82 |
+
type GeneratorService_GenerateStreamClient = grpc.ServerStreamingClient[GenerateChunk]
|
| 83 |
+
|
| 84 |
+
func (c *generatorServiceClient) GenerateEmbedding(ctx context.Context, in *EmbeddingRequest, opts ...grpc.CallOption) (*EmbeddingResponse, error) {
|
| 85 |
+
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
| 86 |
+
out := new(EmbeddingResponse)
|
| 87 |
+
err := c.cc.Invoke(ctx, GeneratorService_GenerateEmbedding_FullMethodName, in, out, cOpts...)
|
| 88 |
+
if err != nil {
|
| 89 |
+
return nil, err
|
| 90 |
+
}
|
| 91 |
+
return out, nil
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
func (c *generatorServiceClient) BatchGenerateEmbeddings(ctx context.Context, in *BatchEmbeddingRequest, opts ...grpc.CallOption) (*BatchEmbeddingResponse, error) {
|
| 95 |
+
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
| 96 |
+
out := new(BatchEmbeddingResponse)
|
| 97 |
+
err := c.cc.Invoke(ctx, GeneratorService_BatchGenerateEmbeddings_FullMethodName, in, out, cOpts...)
|
| 98 |
+
if err != nil {
|
| 99 |
+
return nil, err
|
| 100 |
+
}
|
| 101 |
+
return out, nil
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
func (c *generatorServiceClient) Rerank(ctx context.Context, in *RerankRequest, opts ...grpc.CallOption) (*RerankResponse, error) {
|
| 105 |
+
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
| 106 |
+
out := new(RerankResponse)
|
| 107 |
+
err := c.cc.Invoke(ctx, GeneratorService_Rerank_FullMethodName, in, out, cOpts...)
|
| 108 |
+
if err != nil {
|
| 109 |
+
return nil, err
|
| 110 |
+
}
|
| 111 |
+
return out, nil
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// GeneratorServiceServer is the server API for GeneratorService service.
|
| 115 |
+
// All implementations must embed UnimplementedGeneratorServiceServer
|
| 116 |
+
// for forward compatibility.
|
| 117 |
+
//
|
| 118 |
+
// GeneratorService handles LLM-based response generation
|
| 119 |
+
type GeneratorServiceServer interface {
|
| 120 |
+
// Generate a complete response
|
| 121 |
+
Generate(context.Context, *GenerateRequest) (*GenerateResponse, error)
|
| 122 |
+
// Generate with streaming output
|
| 123 |
+
GenerateStream(*GenerateRequest, grpc.ServerStreamingServer[GenerateChunk]) error
|
| 124 |
+
// Generate embeddings for text
|
| 125 |
+
GenerateEmbedding(context.Context, *EmbeddingRequest) (*EmbeddingResponse, error)
|
| 126 |
+
// Batch generate embeddings
|
| 127 |
+
BatchGenerateEmbeddings(context.Context, *BatchEmbeddingRequest) (*BatchEmbeddingResponse, error)
|
| 128 |
+
// Rerank documents based on query relevance
|
| 129 |
+
Rerank(context.Context, *RerankRequest) (*RerankResponse, error)
|
| 130 |
+
mustEmbedUnimplementedGeneratorServiceServer()
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// UnimplementedGeneratorServiceServer must be embedded to have
|
| 134 |
+
// forward compatible implementations.
|
| 135 |
+
//
|
| 136 |
+
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
| 137 |
+
// pointer dereference when methods are called.
|
| 138 |
+
type UnimplementedGeneratorServiceServer struct{}
|
| 139 |
+
|
| 140 |
+
func (UnimplementedGeneratorServiceServer) Generate(context.Context, *GenerateRequest) (*GenerateResponse, error) {
|
| 141 |
+
return nil, status.Error(codes.Unimplemented, "method Generate not implemented")
|
| 142 |
+
}
|
| 143 |
+
func (UnimplementedGeneratorServiceServer) GenerateStream(*GenerateRequest, grpc.ServerStreamingServer[GenerateChunk]) error {
|
| 144 |
+
return status.Error(codes.Unimplemented, "method GenerateStream not implemented")
|
| 145 |
+
}
|
| 146 |
+
func (UnimplementedGeneratorServiceServer) GenerateEmbedding(context.Context, *EmbeddingRequest) (*EmbeddingResponse, error) {
|
| 147 |
+
return nil, status.Error(codes.Unimplemented, "method GenerateEmbedding not implemented")
|
| 148 |
+
}
|
| 149 |
+
func (UnimplementedGeneratorServiceServer) BatchGenerateEmbeddings(context.Context, *BatchEmbeddingRequest) (*BatchEmbeddingResponse, error) {
|
| 150 |
+
return nil, status.Error(codes.Unimplemented, "method BatchGenerateEmbeddings not implemented")
|
| 151 |
+
}
|
| 152 |
+
func (UnimplementedGeneratorServiceServer) Rerank(context.Context, *RerankRequest) (*RerankResponse, error) {
|
| 153 |
+
return nil, status.Error(codes.Unimplemented, "method Rerank not implemented")
|
| 154 |
+
}
|
| 155 |
+
func (UnimplementedGeneratorServiceServer) mustEmbedUnimplementedGeneratorServiceServer() {}
|
| 156 |
+
func (UnimplementedGeneratorServiceServer) testEmbeddedByValue() {}
|
| 157 |
+
|
| 158 |
+
// UnsafeGeneratorServiceServer may be embedded to opt out of forward compatibility for this service.
|
| 159 |
+
// Use of this interface is not recommended, as added methods to GeneratorServiceServer will
|
| 160 |
+
// result in compilation errors.
|
| 161 |
+
type UnsafeGeneratorServiceServer interface {
|
| 162 |
+
mustEmbedUnimplementedGeneratorServiceServer()
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
func RegisterGeneratorServiceServer(s grpc.ServiceRegistrar, srv GeneratorServiceServer) {
|
| 166 |
+
// If the following call panics, it indicates UnimplementedGeneratorServiceServer was
|
| 167 |
+
// embedded by pointer and is nil. This will cause panics if an
|
| 168 |
+
// unimplemented method is ever invoked, so we test this at initialization
|
| 169 |
+
// time to prevent it from happening at runtime later due to I/O.
|
| 170 |
+
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
| 171 |
+
t.testEmbeddedByValue()
|
| 172 |
+
}
|
| 173 |
+
s.RegisterService(&GeneratorService_ServiceDesc, srv)
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
func _GeneratorService_Generate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
| 177 |
+
in := new(GenerateRequest)
|
| 178 |
+
if err := dec(in); err != nil {
|
| 179 |
+
return nil, err
|
| 180 |
+
}
|
| 181 |
+
if interceptor == nil {
|
| 182 |
+
return srv.(GeneratorServiceServer).Generate(ctx, in)
|
| 183 |
+
}
|
| 184 |
+
info := &grpc.UnaryServerInfo{
|
| 185 |
+
Server: srv,
|
| 186 |
+
FullMethod: GeneratorService_Generate_FullMethodName,
|
| 187 |
+
}
|
| 188 |
+
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
| 189 |
+
return srv.(GeneratorServiceServer).Generate(ctx, req.(*GenerateRequest))
|
| 190 |
+
}
|
| 191 |
+
return interceptor(ctx, in, info, handler)
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
func _GeneratorService_GenerateStream_Handler(srv interface{}, stream grpc.ServerStream) error {
|
| 195 |
+
m := new(GenerateRequest)
|
| 196 |
+
if err := stream.RecvMsg(m); err != nil {
|
| 197 |
+
return err
|
| 198 |
+
}
|
| 199 |
+
return srv.(GeneratorServiceServer).GenerateStream(m, &grpc.GenericServerStream[GenerateRequest, GenerateChunk]{ServerStream: stream})
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
| 203 |
+
type GeneratorService_GenerateStreamServer = grpc.ServerStreamingServer[GenerateChunk]
|
| 204 |
+
|
| 205 |
+
func _GeneratorService_GenerateEmbedding_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
| 206 |
+
in := new(EmbeddingRequest)
|
| 207 |
+
if err := dec(in); err != nil {
|
| 208 |
+
return nil, err
|
| 209 |
+
}
|
| 210 |
+
if interceptor == nil {
|
| 211 |
+
return srv.(GeneratorServiceServer).GenerateEmbedding(ctx, in)
|
| 212 |
+
}
|
| 213 |
+
info := &grpc.UnaryServerInfo{
|
| 214 |
+
Server: srv,
|
| 215 |
+
FullMethod: GeneratorService_GenerateEmbedding_FullMethodName,
|
| 216 |
+
}
|
| 217 |
+
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
| 218 |
+
return srv.(GeneratorServiceServer).GenerateEmbedding(ctx, req.(*EmbeddingRequest))
|
| 219 |
+
}
|
| 220 |
+
return interceptor(ctx, in, info, handler)
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
func _GeneratorService_BatchGenerateEmbeddings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
| 224 |
+
in := new(BatchEmbeddingRequest)
|
| 225 |
+
if err := dec(in); err != nil {
|
| 226 |
+
return nil, err
|
| 227 |
+
}
|
| 228 |
+
if interceptor == nil {
|
| 229 |
+
return srv.(GeneratorServiceServer).BatchGenerateEmbeddings(ctx, in)
|
| 230 |
+
}
|
| 231 |
+
info := &grpc.UnaryServerInfo{
|
| 232 |
+
Server: srv,
|
| 233 |
+
FullMethod: GeneratorService_BatchGenerateEmbeddings_FullMethodName,
|
| 234 |
+
}
|
| 235 |
+
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
| 236 |
+
return srv.(GeneratorServiceServer).BatchGenerateEmbeddings(ctx, req.(*BatchEmbeddingRequest))
|
| 237 |
+
}
|
| 238 |
+
return interceptor(ctx, in, info, handler)
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
func _GeneratorService_Rerank_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
| 242 |
+
in := new(RerankRequest)
|
| 243 |
+
if err := dec(in); err != nil {
|
| 244 |
+
return nil, err
|
| 245 |
+
}
|
| 246 |
+
if interceptor == nil {
|
| 247 |
+
return srv.(GeneratorServiceServer).Rerank(ctx, in)
|
| 248 |
+
}
|
| 249 |
+
info := &grpc.UnaryServerInfo{
|
| 250 |
+
Server: srv,
|
| 251 |
+
FullMethod: GeneratorService_Rerank_FullMethodName,
|
| 252 |
+
}
|
| 253 |
+
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
| 254 |
+
return srv.(GeneratorServiceServer).Rerank(ctx, req.(*RerankRequest))
|
| 255 |
+
}
|
| 256 |
+
return interceptor(ctx, in, info, handler)
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// GeneratorService_ServiceDesc is the grpc.ServiceDesc for GeneratorService service.
|
| 260 |
+
// It's only intended for direct use with grpc.RegisterService,
|
| 261 |
+
// and not to be introspected or modified (even as a copy)
|
| 262 |
+
var GeneratorService_ServiceDesc = grpc.ServiceDesc{
|
| 263 |
+
ServiceName: "rag.v1.GeneratorService",
|
| 264 |
+
HandlerType: (*GeneratorServiceServer)(nil),
|
| 265 |
+
Methods: []grpc.MethodDesc{
|
| 266 |
+
{
|
| 267 |
+
MethodName: "Generate",
|
| 268 |
+
Handler: _GeneratorService_Generate_Handler,
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
MethodName: "GenerateEmbedding",
|
| 272 |
+
Handler: _GeneratorService_GenerateEmbedding_Handler,
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
MethodName: "BatchGenerateEmbeddings",
|
| 276 |
+
Handler: _GeneratorService_BatchGenerateEmbeddings_Handler,
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
MethodName: "Rerank",
|
| 280 |
+
Handler: _GeneratorService_Rerank_Handler,
|
| 281 |
+
},
|
| 282 |
+
},
|
| 283 |
+
Streams: []grpc.StreamDesc{
|
| 284 |
+
{
|
| 285 |
+
StreamName: "GenerateStream",
|
| 286 |
+
Handler: _GeneratorService_GenerateStream_Handler,
|
| 287 |
+
ServerStreams: true,
|
| 288 |
+
},
|
| 289 |
+
},
|
| 290 |
+
Metadata: "generator.proto",
|
| 291 |
+
}
|
go.mod
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module github.com/AmaniQuery/amaniquery
|
| 2 |
+
|
| 3 |
+
go 1.24.0
|
| 4 |
+
|
| 5 |
+
require (
|
| 6 |
+
github.com/blevesearch/bleve/v2 v2.3.10
|
| 7 |
+
github.com/golang-jwt/jwt/v5 v5.2.0
|
| 8 |
+
github.com/google/uuid v1.6.0
|
| 9 |
+
github.com/gorilla/mux v1.8.1
|
| 10 |
+
github.com/gorilla/websocket v1.5.1
|
| 11 |
+
github.com/graph-gophers/graphql-go v1.5.0
|
| 12 |
+
github.com/hashicorp/consul/api v1.27.0
|
| 13 |
+
github.com/neo4j/neo4j-go-driver/v5 v5.17.0
|
| 14 |
+
github.com/pierrec/lz4/v4 v4.1.15
|
| 15 |
+
github.com/prometheus/client_golang v1.18.0
|
| 16 |
+
github.com/qdrant/go-client v1.7.0
|
| 17 |
+
github.com/redis/go-redis/v9 v9.4.0
|
| 18 |
+
github.com/sony/gobreaker v0.5.0
|
| 19 |
+
github.com/spf13/viper v1.18.2
|
| 20 |
+
go.opentelemetry.io/otel v1.38.0
|
| 21 |
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0
|
| 22 |
+
go.opentelemetry.io/otel/sdk v1.38.0
|
| 23 |
+
go.opentelemetry.io/otel/trace v1.38.0
|
| 24 |
+
go.temporal.io/sdk v1.26.0
|
| 25 |
+
go.uber.org/zap v1.26.0
|
| 26 |
+
golang.org/x/sync v0.19.0
|
| 27 |
+
golang.org/x/time v0.5.0
|
| 28 |
+
google.golang.org/grpc v1.77.0
|
| 29 |
+
google.golang.org/protobuf v1.36.11
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
require (
|
| 33 |
+
github.com/RoaringBitmap/roaring v1.2.3 // indirect
|
| 34 |
+
github.com/armon/go-metrics v0.4.1 // indirect
|
| 35 |
+
github.com/beorn7/perks v1.0.1 // indirect
|
| 36 |
+
github.com/bits-and-blooms/bitset v1.2.0 // indirect
|
| 37 |
+
github.com/blevesearch/bleve_index_api v1.0.6 // indirect
|
| 38 |
+
github.com/blevesearch/geo v0.1.18 // indirect
|
| 39 |
+
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
| 40 |
+
github.com/blevesearch/gtreap v0.1.1 // indirect
|
| 41 |
+
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
| 42 |
+
github.com/blevesearch/scorch_segment_api/v2 v2.1.6 // indirect
|
| 43 |
+
github.com/blevesearch/segment v0.9.1 // indirect
|
| 44 |
+
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
| 45 |
+
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
|
| 46 |
+
github.com/blevesearch/vellum v1.0.10 // indirect
|
| 47 |
+
github.com/blevesearch/zapx/v11 v11.3.10 // indirect
|
| 48 |
+
github.com/blevesearch/zapx/v12 v12.3.10 // indirect
|
| 49 |
+
github.com/blevesearch/zapx/v13 v13.3.10 // indirect
|
| 50 |
+
github.com/blevesearch/zapx/v14 v14.3.10 // indirect
|
| 51 |
+
github.com/blevesearch/zapx/v15 v15.3.13 // indirect
|
| 52 |
+
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
| 53 |
+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
| 54 |
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
| 55 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
| 56 |
+
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
|
| 57 |
+
github.com/fatih/color v1.14.1 // indirect
|
| 58 |
+
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
| 59 |
+
github.com/go-logr/logr v1.4.3 // indirect
|
| 60 |
+
github.com/go-logr/stdr v1.2.2 // indirect
|
| 61 |
+
github.com/gogo/protobuf v1.3.2 // indirect
|
| 62 |
+
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
| 63 |
+
github.com/golang/mock v1.6.0 // indirect
|
| 64 |
+
github.com/golang/protobuf v1.5.4 // indirect
|
| 65 |
+
github.com/golang/snappy v0.0.4 // indirect
|
| 66 |
+
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
| 67 |
+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
|
| 68 |
+
github.com/hashicorp/errwrap v1.1.0 // indirect
|
| 69 |
+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
| 70 |
+
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
| 71 |
+
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
| 72 |
+
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
| 73 |
+
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
| 74 |
+
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
| 75 |
+
github.com/hashicorp/hcl v1.0.0 // indirect
|
| 76 |
+
github.com/hashicorp/serf v0.10.1 // indirect
|
| 77 |
+
github.com/json-iterator/go v1.1.12 // indirect
|
| 78 |
+
github.com/magiconair/properties v1.8.7 // indirect
|
| 79 |
+
github.com/mattn/go-colorable v0.1.13 // indirect
|
| 80 |
+
github.com/mattn/go-isatty v0.0.17 // indirect
|
| 81 |
+
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
| 82 |
+
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
| 83 |
+
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
| 84 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 85 |
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 86 |
+
github.com/mschoch/smat v0.2.0 // indirect
|
| 87 |
+
github.com/pborman/uuid v1.2.1 // indirect
|
| 88 |
+
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
| 89 |
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
| 90 |
+
github.com/prometheus/client_model v0.5.0 // indirect
|
| 91 |
+
github.com/prometheus/common v0.45.0 // indirect
|
| 92 |
+
github.com/prometheus/procfs v0.12.0 // indirect
|
| 93 |
+
github.com/robfig/cron v1.2.0 // indirect
|
| 94 |
+
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
| 95 |
+
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
| 96 |
+
github.com/sourcegraph/conc v0.3.0 // indirect
|
| 97 |
+
github.com/spf13/afero v1.11.0 // indirect
|
| 98 |
+
github.com/spf13/cast v1.6.0 // indirect
|
| 99 |
+
github.com/spf13/pflag v1.0.5 // indirect
|
| 100 |
+
github.com/stretchr/objx v0.5.2 // indirect
|
| 101 |
+
github.com/stretchr/testify v1.11.1 // indirect
|
| 102 |
+
github.com/subosito/gotenv v1.6.0 // indirect
|
| 103 |
+
go.etcd.io/bbolt v1.3.7 // indirect
|
| 104 |
+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
| 105 |
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 // indirect
|
| 106 |
+
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
| 107 |
+
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
|
| 108 |
+
go.temporal.io/api v1.29.1 // indirect
|
| 109 |
+
go.uber.org/multierr v1.10.0 // indirect
|
| 110 |
+
golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
|
| 111 |
+
golang.org/x/net v0.48.0 // indirect
|
| 112 |
+
golang.org/x/sys v0.39.0 // indirect
|
| 113 |
+
golang.org/x/text v0.32.0 // indirect
|
| 114 |
+
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
| 115 |
+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
| 116 |
+
gopkg.in/ini.v1 v1.67.0 // indirect
|
| 117 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
| 118 |
+
)
|
go.sum
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
| 2 |
+
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
| 3 |
+
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
| 4 |
+
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
|
| 5 |
+
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
|
| 6 |
+
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
| 7 |
+
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
| 8 |
+
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
| 9 |
+
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
| 10 |
+
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
| 11 |
+
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
| 12 |
+
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
| 13 |
+
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
| 14 |
+
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
| 15 |
+
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
| 16 |
+
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
| 17 |
+
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
| 18 |
+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
| 19 |
+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
| 20 |
+
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
| 21 |
+
github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
|
| 22 |
+
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
| 23 |
+
github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg=
|
| 24 |
+
github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA=
|
| 25 |
+
github.com/blevesearch/bleve_index_api v1.0.6 h1:gyUUxdsrvmW3jVhhYdCVL6h9dCjNT/geNU7PxGn37p8=
|
| 26 |
+
github.com/blevesearch/bleve_index_api v1.0.6/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms=
|
| 27 |
+
github.com/blevesearch/geo v0.1.18 h1:Np8jycHTZ5scFe7VEPLrDoHnnb9C4j636ue/CGrhtDw=
|
| 28 |
+
github.com/blevesearch/geo v0.1.18/go.mod h1:uRMGWG0HJYfWfFJpK3zTdnnr1K+ksZTuWKhXeSokfnM=
|
| 29 |
+
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
| 30 |
+
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
| 31 |
+
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
| 32 |
+
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
| 33 |
+
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
| 34 |
+
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
| 35 |
+
github.com/blevesearch/scorch_segment_api/v2 v2.1.6 h1:CdekX/Ob6YCYmeHzD72cKpwzBjvkOGegHOqhAkXp6yA=
|
| 36 |
+
github.com/blevesearch/scorch_segment_api/v2 v2.1.6/go.mod h1:nQQYlp51XvoSVxcciBjtvuHPIVjlWrN1hX4qwK2cqdc=
|
| 37 |
+
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
| 38 |
+
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
| 39 |
+
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
| 40 |
+
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
| 41 |
+
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
|
| 42 |
+
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
|
| 43 |
+
github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI=
|
| 44 |
+
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
|
| 45 |
+
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=
|
| 46 |
+
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=
|
| 47 |
+
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=
|
| 48 |
+
github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs=
|
| 49 |
+
github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8=
|
| 50 |
+
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=
|
| 51 |
+
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=
|
| 52 |
+
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
|
| 53 |
+
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
|
| 54 |
+
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
|
| 55 |
+
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
| 56 |
+
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
| 57 |
+
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
| 58 |
+
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
| 59 |
+
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
| 60 |
+
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
| 61 |
+
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
| 62 |
+
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
| 63 |
+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
| 64 |
+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
| 65 |
+
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
| 66 |
+
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
| 67 |
+
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
| 68 |
+
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
| 69 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 70 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 71 |
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
| 72 |
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 73 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
| 74 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
| 75 |
+
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
| 76 |
+
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
| 77 |
+
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
| 78 |
+
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
| 79 |
+
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
|
| 80 |
+
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
|
| 81 |
+
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
| 82 |
+
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
| 83 |
+
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
| 84 |
+
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
| 85 |
+
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
| 86 |
+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
| 87 |
+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
| 88 |
+
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
| 89 |
+
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
| 90 |
+
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
| 91 |
+
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
| 92 |
+
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
| 93 |
+
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
| 94 |
+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
| 95 |
+
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
| 96 |
+
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
| 97 |
+
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
| 98 |
+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
| 99 |
+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
| 100 |
+
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
| 101 |
+
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
| 102 |
+
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
| 103 |
+
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
| 104 |
+
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
| 105 |
+
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
| 106 |
+
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
|
| 107 |
+
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
| 108 |
+
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
| 109 |
+
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
| 110 |
+
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
| 111 |
+
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
| 112 |
+
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
| 113 |
+
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
| 114 |
+
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
| 115 |
+
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
| 116 |
+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
| 117 |
+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
| 118 |
+
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
| 119 |
+
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
| 120 |
+
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
| 121 |
+
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
| 122 |
+
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
| 123 |
+
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
| 124 |
+
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
| 125 |
+
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 126 |
+
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
| 127 |
+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
| 128 |
+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
| 129 |
+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 130 |
+
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 131 |
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 132 |
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 133 |
+
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
| 134 |
+
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
| 135 |
+
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
| 136 |
+
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
| 137 |
+
github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc=
|
| 138 |
+
github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
|
| 139 |
+
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
| 140 |
+
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
|
| 141 |
+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
|
| 142 |
+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
|
| 143 |
+
github.com/hashicorp/consul/api v1.27.0 h1:gmJ6DPKQog1426xsdmgk5iqDyoRiNc+ipBdJOqKQFjc=
|
| 144 |
+
github.com/hashicorp/consul/api v1.27.0/go.mod h1:JkekNRSou9lANFdt+4IKx3Za7XY0JzzpQjEb4Ivo1c8=
|
| 145 |
+
github.com/hashicorp/consul/sdk v0.15.1 h1:kKIGxc7CZtflcF5DLfHeq7rOQmRq3vk7kwISN9bif8Q=
|
| 146 |
+
github.com/hashicorp/consul/sdk v0.15.1/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A=
|
| 147 |
+
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
| 148 |
+
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
| 149 |
+
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
| 150 |
+
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
| 151 |
+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
| 152 |
+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
| 153 |
+
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
| 154 |
+
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
| 155 |
+
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
| 156 |
+
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
| 157 |
+
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
| 158 |
+
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
| 159 |
+
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
|
| 160 |
+
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
| 161 |
+
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
| 162 |
+
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
| 163 |
+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
| 164 |
+
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
| 165 |
+
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
| 166 |
+
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
| 167 |
+
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
| 168 |
+
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
| 169 |
+
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
| 170 |
+
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
| 171 |
+
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
| 172 |
+
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
| 173 |
+
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
| 174 |
+
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
| 175 |
+
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
| 176 |
+
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
|
| 177 |
+
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
| 178 |
+
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
| 179 |
+
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
| 180 |
+
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
| 181 |
+
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
| 182 |
+
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
| 183 |
+
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
| 184 |
+
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
| 185 |
+
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
|
| 186 |
+
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
|
| 187 |
+
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
|
| 188 |
+
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
| 189 |
+
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
| 190 |
+
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
| 191 |
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
| 192 |
+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
| 193 |
+
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
| 194 |
+
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
| 195 |
+
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
| 196 |
+
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
| 197 |
+
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
| 198 |
+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
| 199 |
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
| 200 |
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
| 201 |
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
| 202 |
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
| 203 |
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
| 204 |
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
| 205 |
+
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
| 206 |
+
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
| 207 |
+
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
| 208 |
+
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
| 209 |
+
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
| 210 |
+
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
| 211 |
+
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
| 212 |
+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
| 213 |
+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
| 214 |
+
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
| 215 |
+
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
| 216 |
+
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
| 217 |
+
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
| 218 |
+
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
| 219 |
+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
| 220 |
+
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
| 221 |
+
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
| 222 |
+
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
| 223 |
+
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
| 224 |
+
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
| 225 |
+
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
| 226 |
+
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
|
| 227 |
+
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
| 228 |
+
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
| 229 |
+
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
| 230 |
+
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
| 231 |
+
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
| 232 |
+
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
| 233 |
+
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
| 234 |
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 235 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 236 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 237 |
+
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
| 238 |
+
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
| 239 |
+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
| 240 |
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 241 |
+
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
| 242 |
+
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
| 243 |
+
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
| 244 |
+
github.com/neo4j/neo4j-go-driver/v5 v5.17.0 h1:Bdqg1Y8Hd3uLYToXtBjysDYXTdMiP7zeUNUEwfbJkSo=
|
| 245 |
+
github.com/neo4j/neo4j-go-driver/v5 v5.17.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
|
| 246 |
+
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
| 247 |
+
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
| 248 |
+
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
| 249 |
+
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
| 250 |
+
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
| 251 |
+
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
|
| 252 |
+
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
| 253 |
+
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
| 254 |
+
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
| 255 |
+
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
|
| 256 |
+
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
| 257 |
+
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
| 258 |
+
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
| 259 |
+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
| 260 |
+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
| 261 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 262 |
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
| 263 |
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 264 |
+
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
| 265 |
+
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
| 266 |
+
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
| 267 |
+
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
| 268 |
+
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
| 269 |
+
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
| 270 |
+
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
| 271 |
+
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
| 272 |
+
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
| 273 |
+
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
| 274 |
+
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
| 275 |
+
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
| 276 |
+
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
| 277 |
+
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
| 278 |
+
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
| 279 |
+
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
| 280 |
+
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
| 281 |
+
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
| 282 |
+
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
| 283 |
+
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
| 284 |
+
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
| 285 |
+
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
| 286 |
+
github.com/qdrant/go-client v1.7.0 h1:2TeeWyZAWIup7vvD7Ne6aAvo0H+F5OUb1pB9Z8Y4pFk=
|
| 287 |
+
github.com/qdrant/go-client v1.7.0/go.mod h1:680gkxNAsVtre0Z8hAQmtPzJtz1xFAyCu2TUxULtnoE=
|
| 288 |
+
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
| 289 |
+
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
| 290 |
+
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
| 291 |
+
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
| 292 |
+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
| 293 |
+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
| 294 |
+
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
| 295 |
+
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
| 296 |
+
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
| 297 |
+
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
| 298 |
+
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
| 299 |
+
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
| 300 |
+
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
| 301 |
+
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
| 302 |
+
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
| 303 |
+
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
|
| 304 |
+
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
| 305 |
+
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
| 306 |
+
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
| 307 |
+
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
| 308 |
+
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
| 309 |
+
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
| 310 |
+
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
| 311 |
+
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
| 312 |
+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
| 313 |
+
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
| 314 |
+
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
| 315 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 316 |
+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 317 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 318 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
| 319 |
+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
| 320 |
+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
| 321 |
+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
| 322 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
| 323 |
+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
| 324 |
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 325 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 326 |
+
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
| 327 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
| 328 |
+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
| 329 |
+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
| 330 |
+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
| 331 |
+
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
| 332 |
+
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
| 333 |
+
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
| 334 |
+
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
| 335 |
+
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
| 336 |
+
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
| 337 |
+
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
| 338 |
+
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
| 339 |
+
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
| 340 |
+
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
| 341 |
+
go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
|
| 342 |
+
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
| 343 |
+
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
| 344 |
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
|
| 345 |
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
|
| 346 |
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 h1:H2JFgRcGiyHg7H7bwcwaQJYrNFqCqrbTQ8K4p1OvDu8=
|
| 347 |
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0/go.mod h1:WfCWp1bGoYK8MeULtI15MmQVczfR+bFkk0DF3h06QmQ=
|
| 348 |
+
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
| 349 |
+
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
| 350 |
+
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
| 351 |
+
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
| 352 |
+
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
| 353 |
+
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
| 354 |
+
go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
|
| 355 |
+
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
| 356 |
+
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
| 357 |
+
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
|
| 358 |
+
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
|
| 359 |
+
go.temporal.io/api v1.29.1 h1:L722DCy3xCzpTe3Rvh1sFC9kcSaMJXqvodCF+swHGtQ=
|
| 360 |
+
go.temporal.io/api v1.29.1/go.mod h1:wZtsUJ3PySASGWbpXBWYVKJ4aHB2ZODEn/xNcTr9HRs=
|
| 361 |
+
go.temporal.io/sdk v1.26.0 h1:QAi7irgKvJI+5cKmvy+1lkdCDJJDDNpIQAoXdr3dcyM=
|
| 362 |
+
go.temporal.io/sdk v1.26.0/go.mod h1:rcAf1YWlbWgMsjJEuz7XiQd6UYxTQDOk2AqRRIDwq/U=
|
| 363 |
+
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
| 364 |
+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
| 365 |
+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
| 366 |
+
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
| 367 |
+
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
| 368 |
+
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
| 369 |
+
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
| 370 |
+
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
| 371 |
+
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
| 372 |
+
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
| 373 |
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
| 374 |
+
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
| 375 |
+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
| 376 |
+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
| 377 |
+
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
| 378 |
+
golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
|
| 379 |
+
golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
| 380 |
+
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
| 381 |
+
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
| 382 |
+
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
| 383 |
+
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
| 384 |
+
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
| 385 |
+
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
| 386 |
+
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
| 387 |
+
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
| 388 |
+
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
| 389 |
+
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
| 390 |
+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
| 391 |
+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
| 392 |
+
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
| 393 |
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
| 394 |
+
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
| 395 |
+
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
| 396 |
+
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
| 397 |
+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
| 398 |
+
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
| 399 |
+
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
| 400 |
+
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
| 401 |
+
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
| 402 |
+
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
| 403 |
+
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 404 |
+
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 405 |
+
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 406 |
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 407 |
+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 408 |
+
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 409 |
+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 410 |
+
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
| 411 |
+
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
| 412 |
+
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 413 |
+
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 414 |
+
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 415 |
+
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 416 |
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 417 |
+
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 418 |
+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 419 |
+
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 420 |
+
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 421 |
+
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 422 |
+
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 423 |
+
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 424 |
+
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 425 |
+
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 426 |
+
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 427 |
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 428 |
+
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 429 |
+
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 430 |
+
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 431 |
+
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 432 |
+
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 433 |
+
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 434 |
+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 435 |
+
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 436 |
+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 437 |
+
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
| 438 |
+
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
| 439 |
+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
| 440 |
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
| 441 |
+
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
| 442 |
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
| 443 |
+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
| 444 |
+
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
| 445 |
+
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
| 446 |
+
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
| 447 |
+
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
| 448 |
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
| 449 |
+
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
| 450 |
+
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
| 451 |
+
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
| 452 |
+
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
| 453 |
+
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
| 454 |
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
| 455 |
+
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
| 456 |
+
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
| 457 |
+
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
| 458 |
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 459 |
+
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 460 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 461 |
+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 462 |
+
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
| 463 |
+
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
| 464 |
+
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
| 465 |
+
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
| 466 |
+
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
| 467 |
+
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
| 468 |
+
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
| 469 |
+
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
| 470 |
+
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
| 471 |
+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
| 472 |
+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
| 473 |
+
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
| 474 |
+
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
| 475 |
+
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
| 476 |
+
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
| 477 |
+
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
| 478 |
+
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
| 479 |
+
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
| 480 |
+
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
| 481 |
+
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
| 482 |
+
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
| 483 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 484 |
+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 485 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
| 486 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
| 487 |
+
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
| 488 |
+
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
| 489 |
+
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
| 490 |
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
| 491 |
+
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
| 492 |
+
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
| 493 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 494 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 495 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 496 |
+
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
| 497 |
+
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
go.work
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
go 1.24.0
|
| 2 |
+
|
| 3 |
+
use (
|
| 4 |
+
.
|
| 5 |
+
./services/files
|
| 6 |
+
./services/ingestion
|
| 7 |
+
./services/notifications
|
| 8 |
+
./services/portal
|
| 9 |
+
./services/voice
|
| 10 |
+
)
|
internal/agent/interceptors.go
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package agent provides gRPC interceptors for logging, recovery, and authentication
|
| 2 |
+
package agent
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"context"
|
| 6 |
+
"runtime/debug"
|
| 7 |
+
|
| 8 |
+
"go.uber.org/zap"
|
| 9 |
+
"google.golang.org/grpc"
|
| 10 |
+
"google.golang.org/grpc/codes"
|
| 11 |
+
"google.golang.org/grpc/status"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
// LoggingInterceptor returns a gRPC unary server interceptor for logging requests
|
| 15 |
+
func LoggingInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor {
|
| 16 |
+
return func(
|
| 17 |
+
ctx context.Context,
|
| 18 |
+
req interface{},
|
| 19 |
+
info *grpc.UnaryServerInfo,
|
| 20 |
+
handler grpc.UnaryHandler,
|
| 21 |
+
) (interface{}, error) {
|
| 22 |
+
logger.Debug("handling request",
|
| 23 |
+
zap.String("method", info.FullMethod),
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
resp, err := handler(ctx, req)
|
| 27 |
+
|
| 28 |
+
if err != nil {
|
| 29 |
+
logger.Error("request failed",
|
| 30 |
+
zap.String("method", info.FullMethod),
|
| 31 |
+
zap.Error(err),
|
| 32 |
+
)
|
| 33 |
+
} else {
|
| 34 |
+
logger.Debug("request completed",
|
| 35 |
+
zap.String("method", info.FullMethod),
|
| 36 |
+
)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
return resp, err
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// RecoveryInterceptor returns a gRPC unary server interceptor that recovers from panics
|
| 44 |
+
func RecoveryInterceptor() grpc.UnaryServerInterceptor {
|
| 45 |
+
return func(
|
| 46 |
+
ctx context.Context,
|
| 47 |
+
req interface{},
|
| 48 |
+
info *grpc.UnaryServerInfo,
|
| 49 |
+
handler grpc.UnaryHandler,
|
| 50 |
+
) (resp interface{}, err error) {
|
| 51 |
+
defer func() {
|
| 52 |
+
if r := recover(); r != nil {
|
| 53 |
+
stack := debug.Stack()
|
| 54 |
+
err = status.Errorf(codes.Internal, "panic recovered: %v\n%s", r, string(stack))
|
| 55 |
+
}
|
| 56 |
+
}()
|
| 57 |
+
|
| 58 |
+
return handler(ctx, req)
|
| 59 |
+
}
|
| 60 |
+
}
|
internal/agent/orchestrator.go
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package agent implements the Agent Orchestrator service that handles
|
| 2 |
+
// query processing, tool orchestration, and conversation state management.
|
| 3 |
+
package agent
|
| 4 |
+
|
| 5 |
+
import (
|
| 6 |
+
"context"
|
| 7 |
+
"fmt"
|
| 8 |
+
"sync"
|
| 9 |
+
"time"
|
| 10 |
+
|
| 11 |
+
llmclient "github.com/AmaniQuery/amaniquery/internal/generator/llm"
|
| 12 |
+
"github.com/AmaniQuery/amaniquery/internal/retriever"
|
| 13 |
+
"github.com/AmaniQuery/amaniquery/internal/router"
|
| 14 |
+
"github.com/AmaniQuery/amaniquery/pkg/observability"
|
| 15 |
+
|
| 16 |
+
"github.com/sony/gobreaker"
|
| 17 |
+
"go.opentelemetry.io/otel"
|
| 18 |
+
"go.opentelemetry.io/otel/attribute"
|
| 19 |
+
"go.opentelemetry.io/otel/trace"
|
| 20 |
+
"go.uber.org/zap"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
var tracer = otel.Tracer("agent-orchestrator")
|
| 24 |
+
|
| 25 |
+
// Default system prompt for Kenya Law and News Intelligence
|
| 26 |
+
const defaultSystemPrompt = `You are AmaniQuery, an AI assistant specialized in Kenya Law and News Intelligence.
|
| 27 |
+
Your mission is to democratize access to legal information and civil education in Kenya.
|
| 28 |
+
|
| 29 |
+
When answering questions:
|
| 30 |
+
1. Provide accurate information based on the context provided
|
| 31 |
+
2. Cite specific laws, sections, or articles when relevant
|
| 32 |
+
3. Explain legal concepts in plain, understandable language
|
| 33 |
+
4. If the context doesn't contain enough information, say so clearly
|
| 34 |
+
5. For news-related queries, provide balanced and factual information
|
| 35 |
+
|
| 36 |
+
Always maintain a helpful, educational tone while being precise about legal matters.`
|
| 37 |
+
|
| 38 |
+
// Dependencies contains all service dependencies
|
| 39 |
+
type Dependencies struct {
|
| 40 |
+
VectorStore VectorStore
|
| 41 |
+
Cache CacheClient
|
| 42 |
+
LLMClient LLMClient
|
| 43 |
+
EmbeddingClient EmbeddingClient
|
| 44 |
+
KeywordEngine KeywordEngine
|
| 45 |
+
Router *router.Router
|
| 46 |
+
Retriever *retriever.HybridRetriever
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Close closes all dependencies
|
| 50 |
+
func (d *Dependencies) Close() error {
|
| 51 |
+
var errs []error
|
| 52 |
+
if d.VectorStore != nil {
|
| 53 |
+
if closer, ok := d.VectorStore.(interface{ Close() error }); ok {
|
| 54 |
+
if err := closer.Close(); err != nil {
|
| 55 |
+
errs = append(errs, err)
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
if d.Cache != nil {
|
| 60 |
+
if closer, ok := d.Cache.(interface{ Close() error }); ok {
|
| 61 |
+
if err := closer.Close(); err != nil {
|
| 62 |
+
errs = append(errs, err)
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
if d.KeywordEngine != nil {
|
| 67 |
+
if closer, ok := d.KeywordEngine.(interface{ Close() error }); ok {
|
| 68 |
+
if err := closer.Close(); err != nil {
|
| 69 |
+
errs = append(errs, err)
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
if len(errs) > 0 {
|
| 74 |
+
return errs[0]
|
| 75 |
+
}
|
| 76 |
+
return nil
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Interface definitions
|
| 80 |
+
type VectorStore interface {
|
| 81 |
+
Search(ctx context.Context, embedding []float32, topK int, filters map[string]interface{}) ([]retriever.SearchResult, error)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
type CacheClient interface {
|
| 85 |
+
Get(ctx context.Context, key string) ([]byte, error)
|
| 86 |
+
Set(ctx context.Context, key string, value []byte, ttlSeconds int) error
|
| 87 |
+
GetJSON(ctx context.Context, key string, v interface{}) error
|
| 88 |
+
SetJSON(ctx context.Context, key string, v interface{}, ttlSeconds int) error
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
type LLMClient interface {
|
| 92 |
+
Generate(ctx context.Context, messages []llmclient.Message, opts llmclient.Options) (*llmclient.Response, error)
|
| 93 |
+
GenerateStream(ctx context.Context, messages []llmclient.Message, opts llmclient.Options, callback llmclient.StreamCallback) error
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
type EmbeddingClient interface {
|
| 97 |
+
Generate(ctx context.Context, text string) ([]float32, error)
|
| 98 |
+
GenerateBatch(ctx context.Context, texts []string) ([][]float32, error)
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
type KeywordEngine interface {
|
| 102 |
+
Search(ctx context.Context, query string, topK int) ([]retriever.SearchResult, error)
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// QueryRequest represents an incoming query
|
| 106 |
+
type QueryRequest struct {
|
| 107 |
+
Query string
|
| 108 |
+
SessionID string
|
| 109 |
+
UserID string
|
| 110 |
+
ConversationHistory []Message
|
| 111 |
+
Metadata map[string]string
|
| 112 |
+
Options QueryOptions
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// QueryOptions configures query processing
|
| 116 |
+
type QueryOptions struct {
|
| 117 |
+
MaxSources int
|
| 118 |
+
UseCache bool
|
| 119 |
+
EnableAgentic bool
|
| 120 |
+
KnowledgeBases []string
|
| 121 |
+
Temperature float32
|
| 122 |
+
MaxTokens int
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// QueryResponse contains the query result
|
| 126 |
+
type QueryResponse struct {
|
| 127 |
+
Answer string
|
| 128 |
+
Sources []Source
|
| 129 |
+
Confidence float32
|
| 130 |
+
Metadata QueryMetadata
|
| 131 |
+
FollowUpQuestions []string
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// QueryMetadata contains processing information
|
| 135 |
+
type QueryMetadata struct {
|
| 136 |
+
ProcessingTimeMs int64
|
| 137 |
+
ChunksRetrieved int
|
| 138 |
+
TokensUsed int
|
| 139 |
+
CacheHit bool
|
| 140 |
+
RoutingStrategy string
|
| 141 |
+
TraceID string
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// Source represents a retrieved source document
|
| 145 |
+
type Source struct {
|
| 146 |
+
ID string
|
| 147 |
+
Title string
|
| 148 |
+
Content string
|
| 149 |
+
URL string
|
| 150 |
+
Score float32
|
| 151 |
+
Metadata map[string]string
|
| 152 |
+
Location string
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// Message represents a conversation message
|
| 156 |
+
type Message struct {
|
| 157 |
+
Role string
|
| 158 |
+
Content string
|
| 159 |
+
Timestamp time.Time
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// Tool represents an executable tool for agentic workflows
|
| 163 |
+
type Tool interface {
|
| 164 |
+
Name() string
|
| 165 |
+
Description() string
|
| 166 |
+
Execute(ctx context.Context, input string) (string, error)
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// ResponseStream for streaming responses
|
| 170 |
+
type ResponseStream interface {
|
| 171 |
+
Send(chunk *ResponseChunk) error
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// ResponseChunk for streaming
|
| 175 |
+
type ResponseChunk struct {
|
| 176 |
+
Type string
|
| 177 |
+
Content string
|
| 178 |
+
Sources []Source
|
| 179 |
+
IsFinal bool
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// Orchestrator implements the main query processing logic
|
| 183 |
+
type Orchestrator struct {
|
| 184 |
+
deps *Dependencies
|
| 185 |
+
logger *zap.Logger
|
| 186 |
+
tools map[string]Tool
|
| 187 |
+
toolsMu sync.RWMutex
|
| 188 |
+
circuitBreaker *gobreaker.CircuitBreaker
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// NewOrchestrator creates a new agent orchestrator
|
| 192 |
+
func NewOrchestrator(deps *Dependencies, logger *zap.Logger) *Orchestrator {
|
| 193 |
+
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
|
| 194 |
+
Name: "agent-orchestrator",
|
| 195 |
+
MaxRequests: 5,
|
| 196 |
+
Interval: 10 * time.Second,
|
| 197 |
+
Timeout: 60 * time.Second,
|
| 198 |
+
ReadyToTrip: func(counts gobreaker.Counts) bool {
|
| 199 |
+
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
|
| 200 |
+
return counts.Requests >= 3 && failureRatio >= 0.6
|
| 201 |
+
},
|
| 202 |
+
OnStateChange: func(name string, from, to gobreaker.State) {
|
| 203 |
+
logger.Warn("circuit breaker state changed",
|
| 204 |
+
zap.String("name", name),
|
| 205 |
+
zap.String("from", from.String()),
|
| 206 |
+
zap.String("to", to.String()),
|
| 207 |
+
)
|
| 208 |
+
},
|
| 209 |
+
})
|
| 210 |
+
|
| 211 |
+
return &Orchestrator{
|
| 212 |
+
deps: deps,
|
| 213 |
+
logger: logger,
|
| 214 |
+
tools: make(map[string]Tool),
|
| 215 |
+
circuitBreaker: cb,
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
// ProcessQuery handles a single query through the full RAG pipeline
|
| 220 |
+
func (o *Orchestrator) ProcessQuery(ctx context.Context, req *QueryRequest) (*QueryResponse, error) {
|
| 221 |
+
ctx, span := tracer.Start(ctx, "ProcessQuery",
|
| 222 |
+
trace.WithAttributes(
|
| 223 |
+
attribute.String("query", req.Query),
|
| 224 |
+
attribute.String("session_id", req.SessionID),
|
| 225 |
+
attribute.String("user_id", req.UserID),
|
| 226 |
+
),
|
| 227 |
+
)
|
| 228 |
+
defer span.End()
|
| 229 |
+
|
| 230 |
+
startTime := time.Now()
|
| 231 |
+
traceID := span.SpanContext().TraceID().String()
|
| 232 |
+
|
| 233 |
+
// Set default options
|
| 234 |
+
if req.Options.MaxSources == 0 {
|
| 235 |
+
req.Options.MaxSources = 10
|
| 236 |
+
}
|
| 237 |
+
if req.Options.Temperature == 0 {
|
| 238 |
+
req.Options.Temperature = 0.7
|
| 239 |
+
}
|
| 240 |
+
if req.Options.MaxTokens == 0 {
|
| 241 |
+
req.Options.MaxTokens = 4096
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// 1. Check cache first
|
| 245 |
+
if req.Options.UseCache && o.deps.Cache != nil {
|
| 246 |
+
cacheKey := o.buildCacheKey(req.Query)
|
| 247 |
+
var cached QueryResponse
|
| 248 |
+
if err := o.deps.Cache.GetJSON(ctx, cacheKey, &cached); err == nil {
|
| 249 |
+
span.SetAttributes(attribute.Bool("cache_hit", true))
|
| 250 |
+
cached.Metadata.CacheHit = true
|
| 251 |
+
cached.Metadata.TraceID = traceID
|
| 252 |
+
observability.RecordCacheHit("query")
|
| 253 |
+
observability.RecordQuery("success", true, cached.Metadata.RoutingStrategy, time.Since(startTime))
|
| 254 |
+
return &cached, nil
|
| 255 |
+
}
|
| 256 |
+
observability.RecordCacheMiss("query")
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// 2. Route the query to determine retrieval strategy
|
| 260 |
+
var routingDecision *router.RoutingDecision
|
| 261 |
+
if o.deps.Router != nil {
|
| 262 |
+
routingDecision = o.deps.Router.Route(ctx, req.Query)
|
| 263 |
+
span.SetAttributes(
|
| 264 |
+
attribute.String("routing_strategy", string(routingDecision.Strategy)),
|
| 265 |
+
attribute.String("query_type", string(routingDecision.QueryType)),
|
| 266 |
+
attribute.Float64("routing_confidence", float64(routingDecision.Confidence)),
|
| 267 |
+
)
|
| 268 |
+
} else {
|
| 269 |
+
routingDecision = &router.RoutingDecision{
|
| 270 |
+
Strategy: router.StrategyHybrid,
|
| 271 |
+
SuggestedTopK: req.Options.MaxSources,
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
// 3. Retrieve relevant documents
|
| 276 |
+
retrieveStart := time.Now()
|
| 277 |
+
var documents []retriever.SearchResult
|
| 278 |
+
var retrieveErr error
|
| 279 |
+
|
| 280 |
+
if o.deps.Retriever != nil {
|
| 281 |
+
searchResp, err := o.deps.Retriever.Search(ctx, retriever.SearchRequest{
|
| 282 |
+
Query: req.Query,
|
| 283 |
+
TopK: routingDecision.SuggestedTopK,
|
| 284 |
+
UseVector: routingDecision.Strategy != router.StrategyKeyword,
|
| 285 |
+
UseKeyword: routingDecision.Strategy != router.StrategyVector,
|
| 286 |
+
})
|
| 287 |
+
if err != nil {
|
| 288 |
+
o.logger.Error("retrieval failed", zap.Error(err))
|
| 289 |
+
retrieveErr = err
|
| 290 |
+
} else {
|
| 291 |
+
documents = searchResp.Results
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
observability.RecordRetrieval("hybrid", time.Since(retrieveStart))
|
| 296 |
+
span.SetAttributes(attribute.Int("documents_retrieved", len(documents)))
|
| 297 |
+
|
| 298 |
+
if retrieveErr != nil && len(documents) == 0 {
|
| 299 |
+
return nil, fmt.Errorf("retrieval failed: %w", retrieveErr)
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
// 4. Generate response using LLM
|
| 303 |
+
generateStart := time.Now()
|
| 304 |
+
answer, tokensUsed, err := o.generate(ctx, req, documents)
|
| 305 |
+
if err != nil {
|
| 306 |
+
observability.RecordError("generation", "orchestrator")
|
| 307 |
+
return nil, fmt.Errorf("generation failed: %w", err)
|
| 308 |
+
}
|
| 309 |
+
observability.RecordGeneration(time.Since(generateStart))
|
| 310 |
+
observability.RecordTokens("total", tokensUsed)
|
| 311 |
+
|
| 312 |
+
// 5. Build response
|
| 313 |
+
response := &QueryResponse{
|
| 314 |
+
Answer: answer,
|
| 315 |
+
Sources: o.buildSources(documents),
|
| 316 |
+
Confidence: o.calculateConfidence(documents),
|
| 317 |
+
Metadata: QueryMetadata{
|
| 318 |
+
ProcessingTimeMs: time.Since(startTime).Milliseconds(),
|
| 319 |
+
ChunksRetrieved: len(documents),
|
| 320 |
+
TokensUsed: tokensUsed,
|
| 321 |
+
CacheHit: false,
|
| 322 |
+
RoutingStrategy: string(routingDecision.Strategy),
|
| 323 |
+
TraceID: traceID,
|
| 324 |
+
},
|
| 325 |
+
FollowUpQuestions: o.generateFollowUps(ctx, req.Query, documents),
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
// 6. Cache the response
|
| 329 |
+
if req.Options.UseCache && o.deps.Cache != nil {
|
| 330 |
+
cacheKey := o.buildCacheKey(req.Query)
|
| 331 |
+
if err := o.deps.Cache.SetJSON(ctx, cacheKey, response, 3600); err != nil {
|
| 332 |
+
o.logger.Warn("failed to cache response", zap.Error(err))
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
observability.RecordQuery("success", false, string(routingDecision.Strategy), time.Since(startTime))
|
| 337 |
+
return response, nil
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
// ProcessQueryStream handles a query with streaming response
|
| 341 |
+
func (o *Orchestrator) ProcessQueryStream(ctx context.Context, req *QueryRequest, stream ResponseStream) error {
|
| 342 |
+
ctx, span := tracer.Start(ctx, "ProcessQueryStream")
|
| 343 |
+
defer span.End()
|
| 344 |
+
|
| 345 |
+
// Send thinking status
|
| 346 |
+
if err := stream.Send(&ResponseChunk{Type: "thinking", Content: "Analyzing your query..."}); err != nil {
|
| 347 |
+
return err
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
// Route query
|
| 351 |
+
var routingDecision *router.RoutingDecision
|
| 352 |
+
if o.deps.Router != nil {
|
| 353 |
+
routingDecision = o.deps.Router.Route(ctx, req.Query)
|
| 354 |
+
} else {
|
| 355 |
+
routingDecision = &router.RoutingDecision{
|
| 356 |
+
Strategy: router.StrategyHybrid,
|
| 357 |
+
SuggestedTopK: 10,
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
// Send retrieval status
|
| 362 |
+
if err := stream.Send(&ResponseChunk{Type: "retrieval", Content: "Searching knowledge base..."}); err != nil {
|
| 363 |
+
return err
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
// Retrieve documents
|
| 367 |
+
var documents []retriever.SearchResult
|
| 368 |
+
if o.deps.Retriever != nil {
|
| 369 |
+
searchResp, err := o.deps.Retriever.Search(ctx, retriever.SearchRequest{
|
| 370 |
+
Query: req.Query,
|
| 371 |
+
TopK: routingDecision.SuggestedTopK,
|
| 372 |
+
UseVector: true,
|
| 373 |
+
UseKeyword: true,
|
| 374 |
+
})
|
| 375 |
+
if err != nil {
|
| 376 |
+
o.logger.Warn("retrieval error in stream", zap.Error(err))
|
| 377 |
+
} else {
|
| 378 |
+
documents = searchResp.Results
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
// Send sources
|
| 383 |
+
sources := o.buildSources(documents)
|
| 384 |
+
if err := stream.Send(&ResponseChunk{
|
| 385 |
+
Type: "retrieval",
|
| 386 |
+
Content: fmt.Sprintf("Found %d relevant sources", len(documents)),
|
| 387 |
+
Sources: sources,
|
| 388 |
+
}); err != nil {
|
| 389 |
+
return err
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
// Stream generation
|
| 393 |
+
if o.deps.LLMClient == nil {
|
| 394 |
+
return stream.Send(&ResponseChunk{
|
| 395 |
+
Type: "complete",
|
| 396 |
+
Content: "LLM client not configured",
|
| 397 |
+
IsFinal: true,
|
| 398 |
+
})
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
messages := o.buildMessages(req, documents)
|
| 402 |
+
opts := llmclient.Options{
|
| 403 |
+
Temperature: req.Options.Temperature,
|
| 404 |
+
MaxTokens: req.Options.MaxTokens,
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
var fullContent string
|
| 408 |
+
err := o.deps.LLMClient.GenerateStream(ctx, messages, opts, func(chunk string) error {
|
| 409 |
+
fullContent += chunk
|
| 410 |
+
return stream.Send(&ResponseChunk{
|
| 411 |
+
Type: "generation",
|
| 412 |
+
Content: chunk,
|
| 413 |
+
})
|
| 414 |
+
})
|
| 415 |
+
|
| 416 |
+
if err != nil {
|
| 417 |
+
return err
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
// Send final response
|
| 421 |
+
return stream.Send(&ResponseChunk{
|
| 422 |
+
Type: "complete",
|
| 423 |
+
Content: fullContent,
|
| 424 |
+
Sources: sources,
|
| 425 |
+
IsFinal: true,
|
| 426 |
+
})
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
// RegisterTool registers a tool for agentic execution
|
| 430 |
+
func (o *Orchestrator) RegisterTool(name string, tool Tool) error {
|
| 431 |
+
o.toolsMu.Lock()
|
| 432 |
+
defer o.toolsMu.Unlock()
|
| 433 |
+
|
| 434 |
+
if _, exists := o.tools[name]; exists {
|
| 435 |
+
return fmt.Errorf("tool %s already registered", name)
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
o.tools[name] = tool
|
| 439 |
+
o.logger.Info("registered tool", zap.String("name", name))
|
| 440 |
+
return nil
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
func (o *Orchestrator) buildCacheKey(query string) string {
|
| 444 |
+
return "query:" + query
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
func (o *Orchestrator) generate(ctx context.Context, req *QueryRequest, docs []retriever.SearchResult) (string, int, error) {
|
| 448 |
+
if o.deps.LLMClient == nil {
|
| 449 |
+
return "LLM client not configured. Please set OPENAI_API_KEY environment variable.", 0, nil
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
messages := o.buildMessages(req, docs)
|
| 453 |
+
opts := llmclient.Options{
|
| 454 |
+
Temperature: req.Options.Temperature,
|
| 455 |
+
MaxTokens: req.Options.MaxTokens,
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
resp, err := o.deps.LLMClient.Generate(ctx, messages, opts)
|
| 459 |
+
if err != nil {
|
| 460 |
+
return "", 0, err
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
return resp.Content, resp.Usage.TotalTokens, nil
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
func (o *Orchestrator) buildMessages(req *QueryRequest, docs []retriever.SearchResult) []llmclient.Message {
|
| 467 |
+
// Build context from documents
|
| 468 |
+
contextDocs := make([]llmclient.ContextDoc, len(docs))
|
| 469 |
+
for i, doc := range docs {
|
| 470 |
+
title := doc.Title
|
| 471 |
+
if title == "" {
|
| 472 |
+
if t, ok := doc.Metadata["title"].(string); ok {
|
| 473 |
+
title = t
|
| 474 |
+
} else {
|
| 475 |
+
title = fmt.Sprintf("Source %d", i+1)
|
| 476 |
+
}
|
| 477 |
+
}
|
| 478 |
+
contextDocs[i] = llmclient.ContextDoc{
|
| 479 |
+
Title: title,
|
| 480 |
+
Content: doc.Content,
|
| 481 |
+
Source: doc.Source,
|
| 482 |
+
Score: doc.Score,
|
| 483 |
+
}
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
return llmclient.BuildPrompt(defaultSystemPrompt, req.Query, contextDocs)
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
func (o *Orchestrator) buildSources(docs []retriever.SearchResult) []Source {
|
| 490 |
+
sources := make([]Source, len(docs))
|
| 491 |
+
for i, doc := range docs {
|
| 492 |
+
metadata := make(map[string]string)
|
| 493 |
+
for k, v := range doc.Metadata {
|
| 494 |
+
if s, ok := v.(string); ok {
|
| 495 |
+
metadata[k] = s
|
| 496 |
+
}
|
| 497 |
+
}
|
| 498 |
+
sources[i] = Source{
|
| 499 |
+
ID: doc.ID,
|
| 500 |
+
Title: doc.Title,
|
| 501 |
+
Content: doc.Content,
|
| 502 |
+
URL: doc.Source,
|
| 503 |
+
Score: doc.Score,
|
| 504 |
+
Metadata: metadata,
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
return sources
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
func (o *Orchestrator) calculateConfidence(docs []retriever.SearchResult) float32 {
|
| 511 |
+
if len(docs) == 0 {
|
| 512 |
+
return 0.0
|
| 513 |
+
}
|
| 514 |
+
var totalScore float32
|
| 515 |
+
for _, doc := range docs {
|
| 516 |
+
totalScore += doc.Score
|
| 517 |
+
}
|
| 518 |
+
return totalScore / float32(len(docs))
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
func (o *Orchestrator) generateFollowUps(ctx context.Context, query string, docs []retriever.SearchResult) []string {
|
| 522 |
+
// Generate simple follow-up suggestions based on query patterns
|
| 523 |
+
followUps := []string{}
|
| 524 |
+
|
| 525 |
+
// Check for legal terms to suggest related questions
|
| 526 |
+
if containsLegalTerms(query) {
|
| 527 |
+
followUps = append(followUps, "What are the penalties for violating this law?")
|
| 528 |
+
followUps = append(followUps, "Are there any recent amendments to this law?")
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
// Suggest comparative questions
|
| 532 |
+
if len(docs) > 0 {
|
| 533 |
+
followUps = append(followUps, "How does this compare to similar laws in other East African countries?")
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
if len(followUps) > 3 {
|
| 537 |
+
followUps = followUps[:3]
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
return followUps
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
func containsLegalTerms(query string) bool {
|
| 544 |
+
legalTerms := []string{"constitution", "law", "act", "section", "article", "court", "case", "rights"}
|
| 545 |
+
queryLower := query
|
| 546 |
+
for _, term := range legalTerms {
|
| 547 |
+
if contains(queryLower, term) {
|
| 548 |
+
return true
|
| 549 |
+
}
|
| 550 |
+
}
|
| 551 |
+
return false
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
func contains(s, substr string) bool {
|
| 555 |
+
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
func containsHelper(s, substr string) bool {
|
| 559 |
+
for i := 0; i <= len(s)-len(substr); i++ {
|
| 560 |
+
if s[i:i+len(substr)] == substr {
|
| 561 |
+
return true
|
| 562 |
+
}
|
| 563 |
+
}
|
| 564 |
+
return false
|
| 565 |
+
}
|
internal/agent/server.go
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package agent provides gRPC server implementation for the Agent service
|
| 2 |
+
package agent
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"context"
|
| 6 |
+
"crypto/rand"
|
| 7 |
+
"encoding/hex"
|
| 8 |
+
"sync"
|
| 9 |
+
"time"
|
| 10 |
+
|
| 11 |
+
ragv1 "github.com/AmaniQuery/amaniquery/pkg/proto/gen/ragv1"
|
| 12 |
+
|
| 13 |
+
"go.uber.org/zap"
|
| 14 |
+
"google.golang.org/grpc"
|
| 15 |
+
"google.golang.org/grpc/codes"
|
| 16 |
+
"google.golang.org/grpc/status"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
// Server implements the AgentService gRPC server
|
| 20 |
+
type Server struct {
|
| 21 |
+
ragv1.UnimplementedAgentServiceServer
|
| 22 |
+
orchestrator *Orchestrator
|
| 23 |
+
logger *zap.Logger
|
| 24 |
+
mu sync.RWMutex
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// NewServer creates a new Agent gRPC server
|
| 28 |
+
func NewServer(deps *Dependencies, logger *zap.Logger) *Server {
|
| 29 |
+
return &Server{
|
| 30 |
+
orchestrator: NewOrchestrator(deps, logger),
|
| 31 |
+
logger: logger,
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// ProcessQuery handles a single query via gRPC (unary RPC)
|
| 36 |
+
func (s *Server) ProcessQuery(ctx context.Context, req *ragv1.QueryRequest) (*ragv1.QueryResponse, error) {
|
| 37 |
+
if req == nil || req.Query == "" {
|
| 38 |
+
return nil, status.Error(codes.InvalidArgument, "query is required")
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
s.logger.Info("processing query",
|
| 42 |
+
zap.String("query", req.Query),
|
| 43 |
+
zap.String("session_id", req.SessionId),
|
| 44 |
+
zap.String("user_id", req.UserId),
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
// Convert proto request to internal type
|
| 48 |
+
internalReq := s.protoToQueryRequest(req)
|
| 49 |
+
|
| 50 |
+
// Process query through orchestrator
|
| 51 |
+
response, err := s.orchestrator.ProcessQuery(ctx, internalReq)
|
| 52 |
+
if err != nil {
|
| 53 |
+
s.logger.Error("query processing failed",
|
| 54 |
+
zap.Error(err),
|
| 55 |
+
zap.String("query", req.Query),
|
| 56 |
+
)
|
| 57 |
+
return nil, status.Error(codes.Internal, "query processing failed: "+err.Error())
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Convert internal response to proto
|
| 61 |
+
return s.queryResponseToProto(response), nil
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// ProcessQueryStream handles a query with streaming response via gRPC (server streaming RPC)
|
| 65 |
+
func (s *Server) ProcessQueryStream(req *ragv1.QueryRequest, stream ragv1.AgentService_ProcessQueryStreamServer) error {
|
| 66 |
+
if req == nil || req.Query == "" {
|
| 67 |
+
return status.Error(codes.InvalidArgument, "query is required")
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
s.logger.Info("processing streaming query",
|
| 71 |
+
zap.String("query", req.Query),
|
| 72 |
+
zap.String("session_id", req.SessionId),
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
// Convert proto request to internal type
|
| 76 |
+
internalReq := s.protoToQueryRequest(req)
|
| 77 |
+
|
| 78 |
+
// Create a wrapper that implements ResponseStream
|
| 79 |
+
streamWrapper := &grpcStreamWrapper{
|
| 80 |
+
stream: stream,
|
| 81 |
+
logger: s.logger,
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
return s.orchestrator.ProcessQueryStream(stream.Context(), internalReq, streamWrapper)
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// CreateAgent creates a new agent with specific configuration
|
| 88 |
+
func (s *Server) CreateAgent(ctx context.Context, req *ragv1.CreateAgentRequest) (*ragv1.Agent, error) {
|
| 89 |
+
if req == nil || req.Name == "" {
|
| 90 |
+
return nil, status.Error(codes.InvalidArgument, "agent name is required")
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
s.logger.Info("creating agent", zap.String("name", req.Name))
|
| 94 |
+
|
| 95 |
+
// TODO: Implement agent creation logic with persistence
|
| 96 |
+
// For now, return a mock agent
|
| 97 |
+
return &ragv1.Agent{
|
| 98 |
+
Id: generateAgentID(),
|
| 99 |
+
Name: req.Name,
|
| 100 |
+
Description: req.Description,
|
| 101 |
+
CreatedAt: currentTimestamp(),
|
| 102 |
+
Config: req.Config,
|
| 103 |
+
}, nil
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// ExecutePlan executes a pre-defined execution plan
|
| 107 |
+
func (s *Server) ExecutePlan(ctx context.Context, req *ragv1.ExecutionPlan) (*ragv1.PlanResult, error) {
|
| 108 |
+
if req == nil || req.Id == "" {
|
| 109 |
+
return nil, status.Error(codes.InvalidArgument, "plan ID is required")
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
s.logger.Info("executing plan",
|
| 113 |
+
zap.String("plan_id", req.Id),
|
| 114 |
+
zap.Int("steps", len(req.Steps)),
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
// TODO: Implement multi-step plan execution
|
| 118 |
+
return &ragv1.PlanResult{
|
| 119 |
+
PlanId: req.Id,
|
| 120 |
+
Status: ragv1.ExecutionStatus_EXECUTION_STATUS_COMPLETED,
|
| 121 |
+
StepResults: make([]*ragv1.StepResult, 0),
|
| 122 |
+
FinalAnswer: "Plan execution not yet implemented",
|
| 123 |
+
ExecutionTimeMs: 0,
|
| 124 |
+
}, nil
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// GetQueryStatus returns the status of an ongoing query
|
| 128 |
+
func (s *Server) GetQueryStatus(ctx context.Context, req *ragv1.QueryStatusRequest) (*ragv1.QueryStatus, error) {
|
| 129 |
+
if req == nil || req.QueryId == "" {
|
| 130 |
+
return nil, status.Error(codes.InvalidArgument, "query_id is required")
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// TODO: Implement query status tracking
|
| 134 |
+
return &ragv1.QueryStatus{
|
| 135 |
+
QueryId: req.QueryId,
|
| 136 |
+
Status: ragv1.ExecutionStatus_EXECUTION_STATUS_COMPLETED,
|
| 137 |
+
Progress: 100,
|
| 138 |
+
CurrentStep: "completed",
|
| 139 |
+
}, nil
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// RegisterTool registers a tool with the orchestrator
|
| 143 |
+
func (s *Server) RegisterTool(name string, tool Tool) error {
|
| 144 |
+
return s.orchestrator.RegisterTool(name, tool)
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// grpcStreamWrapper wraps gRPC stream to implement ResponseStream interface
|
| 148 |
+
type grpcStreamWrapper struct {
|
| 149 |
+
stream ragv1.AgentService_ProcessQueryStreamServer
|
| 150 |
+
logger *zap.Logger
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
// Send implements ResponseStream.Send by converting internal chunk to protobuf
|
| 154 |
+
func (w *grpcStreamWrapper) Send(chunk *ResponseChunk) error {
|
| 155 |
+
w.logger.Debug("sending stream chunk",
|
| 156 |
+
zap.String("type", chunk.Type),
|
| 157 |
+
zap.Bool("is_final", chunk.IsFinal),
|
| 158 |
+
zap.Int("content_length", len(chunk.Content)),
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
// Convert chunk type string to proto enum
|
| 162 |
+
chunkType := w.stringToChunkType(chunk.Type)
|
| 163 |
+
|
| 164 |
+
// Convert sources to proto format
|
| 165 |
+
protoSources := make([]*ragv1.Source, len(chunk.Sources))
|
| 166 |
+
for i, src := range chunk.Sources {
|
| 167 |
+
protoSources[i] = &ragv1.Source{
|
| 168 |
+
Id: src.ID,
|
| 169 |
+
Title: src.Title,
|
| 170 |
+
Content: src.Content,
|
| 171 |
+
Url: src.URL,
|
| 172 |
+
Score: src.Score,
|
| 173 |
+
Metadata: src.Metadata,
|
| 174 |
+
Location: src.Location,
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// Create proto chunk and send
|
| 179 |
+
protoChunk := &ragv1.QueryResponseChunk{
|
| 180 |
+
Type: chunkType,
|
| 181 |
+
Content: chunk.Content,
|
| 182 |
+
Sources: protoSources,
|
| 183 |
+
IsFinal: chunk.IsFinal,
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
return w.stream.Send(protoChunk)
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// stringToChunkType converts string chunk type to proto enum
|
| 190 |
+
func (w *grpcStreamWrapper) stringToChunkType(t string) ragv1.ChunkType {
|
| 191 |
+
switch t {
|
| 192 |
+
case "thinking":
|
| 193 |
+
return ragv1.ChunkType_CHUNK_TYPE_THINKING
|
| 194 |
+
case "retrieval":
|
| 195 |
+
return ragv1.ChunkType_CHUNK_TYPE_RETRIEVAL
|
| 196 |
+
case "generation":
|
| 197 |
+
return ragv1.ChunkType_CHUNK_TYPE_GENERATION
|
| 198 |
+
case "complete":
|
| 199 |
+
return ragv1.ChunkType_CHUNK_TYPE_COMPLETE
|
| 200 |
+
case "error":
|
| 201 |
+
return ragv1.ChunkType_CHUNK_TYPE_ERROR
|
| 202 |
+
default:
|
| 203 |
+
return ragv1.ChunkType_CHUNK_TYPE_UNSPECIFIED
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// protoToQueryRequest converts proto QueryRequest to internal type
|
| 208 |
+
func (s *Server) protoToQueryRequest(req *ragv1.QueryRequest) *QueryRequest {
|
| 209 |
+
// Convert conversation history
|
| 210 |
+
history := make([]Message, len(req.ConversationHistory))
|
| 211 |
+
for i, msg := range req.ConversationHistory {
|
| 212 |
+
history[i] = Message{
|
| 213 |
+
Role: s.protoRoleToString(msg.Role),
|
| 214 |
+
Content: msg.Content,
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// Set default options if not provided
|
| 219 |
+
maxSources := int(req.Options.GetMaxSources())
|
| 220 |
+
if maxSources == 0 {
|
| 221 |
+
maxSources = 10
|
| 222 |
+
}
|
| 223 |
+
maxTokens := int(req.Options.GetMaxTokens())
|
| 224 |
+
if maxTokens == 0 {
|
| 225 |
+
maxTokens = 4096
|
| 226 |
+
}
|
| 227 |
+
temperature := req.Options.GetTemperature()
|
| 228 |
+
if temperature == 0 {
|
| 229 |
+
temperature = 0.7
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
return &QueryRequest{
|
| 233 |
+
Query: req.Query,
|
| 234 |
+
SessionID: req.SessionId,
|
| 235 |
+
UserID: req.UserId,
|
| 236 |
+
ConversationHistory: history,
|
| 237 |
+
Metadata: req.Metadata,
|
| 238 |
+
Options: QueryOptions{
|
| 239 |
+
MaxSources: maxSources,
|
| 240 |
+
UseCache: req.Options.GetUseCache(),
|
| 241 |
+
EnableAgentic: req.Options.GetEnableAgentic(),
|
| 242 |
+
KnowledgeBases: req.Options.GetKnowledgeBases(),
|
| 243 |
+
Temperature: temperature,
|
| 244 |
+
MaxTokens: maxTokens,
|
| 245 |
+
},
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
// queryResponseToProto converts internal QueryResponse to proto type
|
| 250 |
+
func (s *Server) queryResponseToProto(resp *QueryResponse) *ragv1.QueryResponse {
|
| 251 |
+
// Convert sources
|
| 252 |
+
protoSources := make([]*ragv1.Source, len(resp.Sources))
|
| 253 |
+
for i, src := range resp.Sources {
|
| 254 |
+
protoSources[i] = &ragv1.Source{
|
| 255 |
+
Id: src.ID,
|
| 256 |
+
Title: src.Title,
|
| 257 |
+
Content: src.Content,
|
| 258 |
+
Url: src.URL,
|
| 259 |
+
Score: src.Score,
|
| 260 |
+
Metadata: src.Metadata,
|
| 261 |
+
Location: src.Location,
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
return &ragv1.QueryResponse{
|
| 266 |
+
Answer: resp.Answer,
|
| 267 |
+
Sources: protoSources,
|
| 268 |
+
Confidence: resp.Confidence,
|
| 269 |
+
Metadata: &ragv1.QueryMetadata{
|
| 270 |
+
ProcessingTimeMs: resp.Metadata.ProcessingTimeMs,
|
| 271 |
+
ChunksRetrieved: int32(resp.Metadata.ChunksRetrieved),
|
| 272 |
+
TokensUsed: int32(resp.Metadata.TokensUsed),
|
| 273 |
+
CacheHit: resp.Metadata.CacheHit,
|
| 274 |
+
RoutingStrategy: resp.Metadata.RoutingStrategy,
|
| 275 |
+
TraceId: resp.Metadata.TraceID,
|
| 276 |
+
},
|
| 277 |
+
FollowUpQuestions: resp.FollowUpQuestions,
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// protoRoleToString converts proto MessageRole to string
|
| 282 |
+
func (s *Server) protoRoleToString(role ragv1.MessageRole) string {
|
| 283 |
+
switch role {
|
| 284 |
+
case ragv1.MessageRole_MESSAGE_ROLE_USER:
|
| 285 |
+
return "user"
|
| 286 |
+
case ragv1.MessageRole_MESSAGE_ROLE_ASSISTANT:
|
| 287 |
+
return "assistant"
|
| 288 |
+
case ragv1.MessageRole_MESSAGE_ROLE_SYSTEM:
|
| 289 |
+
return "system"
|
| 290 |
+
default:
|
| 291 |
+
return "user"
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// RegisterAgentServiceServer registers the server with gRPC using generated code
|
| 296 |
+
func RegisterAgentServiceServer(s *grpc.Server, srv *Server) {
|
| 297 |
+
ragv1.RegisterAgentServiceServer(s, srv)
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
// Helper functions
|
| 301 |
+
|
| 302 |
+
func generateAgentID() string {
|
| 303 |
+
b := make([]byte, 16)
|
| 304 |
+
rand.Read(b)
|
| 305 |
+
return "agent_" + hex.EncodeToString(b)
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
func currentTimestamp() int64 {
|
| 309 |
+
return time.Now().Unix()
|
| 310 |
+
}
|
| 311 |
+
|
internal/cache/cache.go
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package cache provides multi-tier caching implementation with local LRU and Redis
|
| 2 |
+
package cache
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"context"
|
| 6 |
+
"crypto/sha256"
|
| 7 |
+
"encoding/hex"
|
| 8 |
+
"encoding/json"
|
| 9 |
+
"sync"
|
| 10 |
+
"time"
|
| 11 |
+
|
| 12 |
+
"github.com/redis/go-redis/v9"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
// MultiTierCache implements a two-tier caching system with local LRU and Redis
|
| 16 |
+
type MultiTierCache struct {
|
| 17 |
+
local *LRUCache
|
| 18 |
+
redis *redis.Client
|
| 19 |
+
ttl time.Duration
|
| 20 |
+
metrics *CacheMetrics
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// CacheMetrics tracks cache performance
|
| 24 |
+
type CacheMetrics struct {
|
| 25 |
+
mu sync.RWMutex
|
| 26 |
+
LocalHits int64
|
| 27 |
+
LocalMisses int64
|
| 28 |
+
RedisHits int64
|
| 29 |
+
RedisMisses int64
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// Config for cache initialization
|
| 33 |
+
type Config struct {
|
| 34 |
+
RedisURL string
|
| 35 |
+
LocalSize int
|
| 36 |
+
TTL time.Duration
|
| 37 |
+
MaxRetries int
|
| 38 |
+
PoolSize int
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// New creates a new multi-tier cache
|
| 42 |
+
func New(cfg Config) (*MultiTierCache, error) {
|
| 43 |
+
// Parse Redis URL
|
| 44 |
+
opt, err := redis.ParseURL(cfg.RedisURL)
|
| 45 |
+
if err != nil {
|
| 46 |
+
return nil, err
|
| 47 |
+
}
|
| 48 |
+
opt.MaxRetries = cfg.MaxRetries
|
| 49 |
+
opt.PoolSize = cfg.PoolSize
|
| 50 |
+
|
| 51 |
+
redisClient := redis.NewClient(opt)
|
| 52 |
+
|
| 53 |
+
// Test Redis connection
|
| 54 |
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
| 55 |
+
defer cancel()
|
| 56 |
+
if err := redisClient.Ping(ctx).Err(); err != nil {
|
| 57 |
+
// Redis not available, continue with local cache only
|
| 58 |
+
redisClient = nil
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
return &MultiTierCache{
|
| 62 |
+
local: NewLRUCache(cfg.LocalSize),
|
| 63 |
+
redis: redisClient,
|
| 64 |
+
ttl: cfg.TTL,
|
| 65 |
+
metrics: &CacheMetrics{},
|
| 66 |
+
}, nil
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// Get retrieves a value from cache, checking local first then Redis
|
| 70 |
+
func (c *MultiTierCache) Get(ctx context.Context, key string) ([]byte, error) {
|
| 71 |
+
hashedKey := c.hashKey(key)
|
| 72 |
+
|
| 73 |
+
// Check local cache first (sub-millisecond)
|
| 74 |
+
if value, found := c.local.Get(hashedKey); found {
|
| 75 |
+
c.metrics.mu.Lock()
|
| 76 |
+
c.metrics.LocalHits++
|
| 77 |
+
c.metrics.mu.Unlock()
|
| 78 |
+
return value, nil
|
| 79 |
+
}
|
| 80 |
+
c.metrics.mu.Lock()
|
| 81 |
+
c.metrics.LocalMisses++
|
| 82 |
+
c.metrics.mu.Unlock()
|
| 83 |
+
|
| 84 |
+
// Check Redis if available (1-5ms)
|
| 85 |
+
if c.redis != nil {
|
| 86 |
+
value, err := c.redis.Get(ctx, hashedKey).Bytes()
|
| 87 |
+
if err == nil {
|
| 88 |
+
c.metrics.mu.Lock()
|
| 89 |
+
c.metrics.RedisHits++
|
| 90 |
+
c.metrics.mu.Unlock()
|
| 91 |
+
// Backfill local cache
|
| 92 |
+
c.local.Set(hashedKey, value)
|
| 93 |
+
return value, nil
|
| 94 |
+
}
|
| 95 |
+
if err != redis.Nil {
|
| 96 |
+
// Log error but continue
|
| 97 |
+
}
|
| 98 |
+
c.metrics.mu.Lock()
|
| 99 |
+
c.metrics.RedisMisses++
|
| 100 |
+
c.metrics.mu.Unlock()
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
return nil, ErrCacheMiss
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Set stores a value in both cache tiers
|
| 107 |
+
func (c *MultiTierCache) Set(ctx context.Context, key string, value []byte, ttlSeconds int) error {
|
| 108 |
+
hashedKey := c.hashKey(key)
|
| 109 |
+
ttl := time.Duration(ttlSeconds) * time.Second
|
| 110 |
+
if ttlSeconds == 0 {
|
| 111 |
+
ttl = c.ttl
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// Set in local cache
|
| 115 |
+
c.local.Set(hashedKey, value)
|
| 116 |
+
|
| 117 |
+
// Set in Redis if available
|
| 118 |
+
if c.redis != nil {
|
| 119 |
+
if err := c.redis.Set(ctx, hashedKey, value, ttl).Err(); err != nil {
|
| 120 |
+
// Log error but don't fail - local cache is still valid
|
| 121 |
+
return nil
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
return nil
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// Delete removes a value from both cache tiers
|
| 129 |
+
func (c *MultiTierCache) Delete(ctx context.Context, key string) error {
|
| 130 |
+
hashedKey := c.hashKey(key)
|
| 131 |
+
|
| 132 |
+
// Delete from local cache
|
| 133 |
+
c.local.Delete(hashedKey)
|
| 134 |
+
|
| 135 |
+
// Delete from Redis if available
|
| 136 |
+
if c.redis != nil {
|
| 137 |
+
if err := c.redis.Del(ctx, hashedKey).Err(); err != nil {
|
| 138 |
+
return err
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
return nil
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// GetJSON retrieves and unmarshals a JSON value from cache
|
| 146 |
+
func (c *MultiTierCache) GetJSON(ctx context.Context, key string, v interface{}) error {
|
| 147 |
+
data, err := c.Get(ctx, key)
|
| 148 |
+
if err != nil {
|
| 149 |
+
return err
|
| 150 |
+
}
|
| 151 |
+
return json.Unmarshal(data, v)
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
// SetJSON marshals and stores a JSON value in cache
|
| 155 |
+
func (c *MultiTierCache) SetJSON(ctx context.Context, key string, v interface{}, ttlSeconds int) error {
|
| 156 |
+
data, err := json.Marshal(v)
|
| 157 |
+
if err != nil {
|
| 158 |
+
return err
|
| 159 |
+
}
|
| 160 |
+
return c.Set(ctx, key, data, ttlSeconds)
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// GetMetrics returns cache performance metrics
|
| 164 |
+
func (c *MultiTierCache) GetMetrics() CacheMetrics {
|
| 165 |
+
c.metrics.mu.RLock()
|
| 166 |
+
defer c.metrics.mu.RUnlock()
|
| 167 |
+
return CacheMetrics{
|
| 168 |
+
LocalHits: c.metrics.LocalHits,
|
| 169 |
+
LocalMisses: c.metrics.LocalMisses,
|
| 170 |
+
RedisHits: c.metrics.RedisHits,
|
| 171 |
+
RedisMisses: c.metrics.RedisMisses,
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// Close closes the cache connections
|
| 176 |
+
func (c *MultiTierCache) Close() error {
|
| 177 |
+
if c.redis != nil {
|
| 178 |
+
return c.redis.Close()
|
| 179 |
+
}
|
| 180 |
+
return nil
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
func (c *MultiTierCache) hashKey(key string) string {
|
| 184 |
+
hash := sha256.Sum256([]byte(key))
|
| 185 |
+
return "amani:" + hex.EncodeToString(hash[:16])
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// ErrCacheMiss indicates the key was not found in cache
|
| 189 |
+
var ErrCacheMiss = &CacheMissError{}
|
| 190 |
+
|
| 191 |
+
// CacheMissError represents a cache miss
|
| 192 |
+
type CacheMissError struct{}
|
| 193 |
+
|
| 194 |
+
func (e *CacheMissError) Error() string {
|
| 195 |
+
return "cache miss"
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// LRUCache implements a simple LRU cache
|
| 199 |
+
type LRUCache struct {
|
| 200 |
+
mu sync.RWMutex
|
| 201 |
+
capacity int
|
| 202 |
+
items map[string]*lruItem
|
| 203 |
+
head *lruItem
|
| 204 |
+
tail *lruItem
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
type lruItem struct {
|
| 208 |
+
key string
|
| 209 |
+
value []byte
|
| 210 |
+
prev *lruItem
|
| 211 |
+
next *lruItem
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// NewLRUCache creates a new LRU cache with the given capacity
|
| 215 |
+
func NewLRUCache(capacity int) *LRUCache {
|
| 216 |
+
return &LRUCache{
|
| 217 |
+
capacity: capacity,
|
| 218 |
+
items: make(map[string]*lruItem),
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// Get retrieves a value from the LRU cache
|
| 223 |
+
func (c *LRUCache) Get(key string) ([]byte, bool) {
|
| 224 |
+
c.mu.Lock()
|
| 225 |
+
defer c.mu.Unlock()
|
| 226 |
+
|
| 227 |
+
item, found := c.items[key]
|
| 228 |
+
if !found {
|
| 229 |
+
return nil, false
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
// Move to front (most recently used)
|
| 233 |
+
c.moveToFront(item)
|
| 234 |
+
return item.value, true
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// Set stores a value in the LRU cache
|
| 238 |
+
func (c *LRUCache) Set(key string, value []byte) {
|
| 239 |
+
c.mu.Lock()
|
| 240 |
+
defer c.mu.Unlock()
|
| 241 |
+
|
| 242 |
+
// Check if item exists
|
| 243 |
+
if item, found := c.items[key]; found {
|
| 244 |
+
item.value = value
|
| 245 |
+
c.moveToFront(item)
|
| 246 |
+
return
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
// Create new item
|
| 250 |
+
item := &lruItem{key: key, value: value}
|
| 251 |
+
c.items[key] = item
|
| 252 |
+
c.addToFront(item)
|
| 253 |
+
|
| 254 |
+
// Evict if over capacity
|
| 255 |
+
if len(c.items) > c.capacity {
|
| 256 |
+
c.evictLRU()
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
// Delete removes a value from the LRU cache
|
| 261 |
+
func (c *LRUCache) Delete(key string) {
|
| 262 |
+
c.mu.Lock()
|
| 263 |
+
defer c.mu.Unlock()
|
| 264 |
+
|
| 265 |
+
if item, found := c.items[key]; found {
|
| 266 |
+
c.removeItem(item)
|
| 267 |
+
delete(c.items, key)
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
func (c *LRUCache) moveToFront(item *lruItem) {
|
| 272 |
+
if item == c.head {
|
| 273 |
+
return
|
| 274 |
+
}
|
| 275 |
+
c.removeItem(item)
|
| 276 |
+
c.addToFront(item)
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
func (c *LRUCache) addToFront(item *lruItem) {
|
| 280 |
+
item.prev = nil
|
| 281 |
+
item.next = c.head
|
| 282 |
+
if c.head != nil {
|
| 283 |
+
c.head.prev = item
|
| 284 |
+
}
|
| 285 |
+
c.head = item
|
| 286 |
+
if c.tail == nil {
|
| 287 |
+
c.tail = item
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
func (c *LRUCache) removeItem(item *lruItem) {
|
| 292 |
+
if item.prev != nil {
|
| 293 |
+
item.prev.next = item.next
|
| 294 |
+
} else {
|
| 295 |
+
c.head = item.next
|
| 296 |
+
}
|
| 297 |
+
if item.next != nil {
|
| 298 |
+
item.next.prev = item.prev
|
| 299 |
+
} else {
|
| 300 |
+
c.tail = item.prev
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
func (c *LRUCache) evictLRU() {
|
| 305 |
+
if c.tail == nil {
|
| 306 |
+
return
|
| 307 |
+
}
|
| 308 |
+
delete(c.items, c.tail.key)
|
| 309 |
+
c.removeItem(c.tail)
|
| 310 |
+
}
|
internal/cache/cache_test.go
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cache_test
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"testing"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
"github.com/AmaniQuery/amaniquery/internal/cache"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
// TestLRUCache_SetGet verifies basic set/get operations
|
| 12 |
+
func TestLRUCache_SetGet(t *testing.T) {
|
| 13 |
+
lru := cache.NewLRUCache(100)
|
| 14 |
+
|
| 15 |
+
// Test set and get
|
| 16 |
+
lru.Set("key1", []byte("value1"))
|
| 17 |
+
|
| 18 |
+
value, found := lru.Get("key1")
|
| 19 |
+
if !found {
|
| 20 |
+
t.Fatal("Expected to find key1")
|
| 21 |
+
}
|
| 22 |
+
if string(value) != "value1" {
|
| 23 |
+
t.Errorf("Expected 'value1', got '%s'", string(value))
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Test missing key
|
| 27 |
+
_, found = lru.Get("nonexistent")
|
| 28 |
+
if found {
|
| 29 |
+
t.Error("Expected not to find nonexistent key")
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// TestLRUCache_Update verifies updating existing keys
|
| 34 |
+
func TestLRUCache_Update(t *testing.T) {
|
| 35 |
+
lru := cache.NewLRUCache(100)
|
| 36 |
+
|
| 37 |
+
lru.Set("key1", []byte("original"))
|
| 38 |
+
lru.Set("key1", []byte("updated"))
|
| 39 |
+
|
| 40 |
+
value, found := lru.Get("key1")
|
| 41 |
+
if !found {
|
| 42 |
+
t.Fatal("Expected to find key1")
|
| 43 |
+
}
|
| 44 |
+
if string(value) != "updated" {
|
| 45 |
+
t.Errorf("Expected 'updated', got '%s'", string(value))
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// TestLRUCache_Eviction verifies LRU eviction when over capacity
|
| 50 |
+
func TestLRUCache_Eviction(t *testing.T) {
|
| 51 |
+
lru := cache.NewLRUCache(3)
|
| 52 |
+
|
| 53 |
+
// Fill cache to capacity
|
| 54 |
+
lru.Set("key1", []byte("value1"))
|
| 55 |
+
lru.Set("key2", []byte("value2"))
|
| 56 |
+
lru.Set("key3", []byte("value3"))
|
| 57 |
+
|
| 58 |
+
// Access key1 to make it recently used
|
| 59 |
+
lru.Get("key1")
|
| 60 |
+
|
| 61 |
+
// Add new key, should evict key2 (least recently used)
|
| 62 |
+
lru.Set("key4", []byte("value4"))
|
| 63 |
+
|
| 64 |
+
// key2 should be evicted
|
| 65 |
+
_, found := lru.Get("key2")
|
| 66 |
+
if found {
|
| 67 |
+
t.Error("Expected key2 to be evicted")
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// key1 should still exist (was accessed recently)
|
| 71 |
+
_, found = lru.Get("key1")
|
| 72 |
+
if !found {
|
| 73 |
+
t.Error("Expected key1 to still exist")
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// key3 and key4 should exist
|
| 77 |
+
_, found = lru.Get("key3")
|
| 78 |
+
if !found {
|
| 79 |
+
t.Error("Expected key3 to exist")
|
| 80 |
+
}
|
| 81 |
+
_, found = lru.Get("key4")
|
| 82 |
+
if !found {
|
| 83 |
+
t.Error("Expected key4 to exist")
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// TestLRUCache_Delete verifies deletion operations
|
| 88 |
+
func TestLRUCache_Delete(t *testing.T) {
|
| 89 |
+
lru := cache.NewLRUCache(100)
|
| 90 |
+
|
| 91 |
+
lru.Set("key1", []byte("value1"))
|
| 92 |
+
lru.Set("key2", []byte("value2"))
|
| 93 |
+
|
| 94 |
+
lru.Delete("key1")
|
| 95 |
+
|
| 96 |
+
_, found := lru.Get("key1")
|
| 97 |
+
if found {
|
| 98 |
+
t.Error("Expected key1 to be deleted")
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// key2 should still exist
|
| 102 |
+
_, found = lru.Get("key2")
|
| 103 |
+
if !found {
|
| 104 |
+
t.Error("Expected key2 to still exist")
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// TestLRUCache_DeleteNonexistent verifies deleting nonexistent keys doesn't panic
|
| 109 |
+
func TestLRUCache_DeleteNonexistent(t *testing.T) {
|
| 110 |
+
lru := cache.NewLRUCache(100)
|
| 111 |
+
|
| 112 |
+
// Should not panic
|
| 113 |
+
lru.Delete("nonexistent")
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// TestCacheMissError tests the error type
|
| 117 |
+
func TestCacheMissError(t *testing.T) {
|
| 118 |
+
err := cache.ErrCacheMiss
|
| 119 |
+
if err.Error() != "cache miss" {
|
| 120 |
+
t.Errorf("Expected 'cache miss', got '%s'", err.Error())
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// TestMultiTierCache_LocalOnly tests cache without Redis connection
|
| 125 |
+
func TestMultiTierCache_LocalOnly(t *testing.T) {
|
| 126 |
+
// Create cache with invalid Redis URL to ensure Redis is not used
|
| 127 |
+
cfg := cache.Config{
|
| 128 |
+
RedisURL: "redis://invalid:6379", // Will fail connection
|
| 129 |
+
LocalSize: 100,
|
| 130 |
+
TTL: time.Hour,
|
| 131 |
+
MaxRetries: 1,
|
| 132 |
+
PoolSize: 1,
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
c, err := cache.New(cfg)
|
| 136 |
+
if err != nil {
|
| 137 |
+
t.Fatalf("Failed to create cache: %v", err)
|
| 138 |
+
}
|
| 139 |
+
defer c.Close()
|
| 140 |
+
|
| 141 |
+
ctx := context.Background()
|
| 142 |
+
|
| 143 |
+
// Set and get should work with local cache
|
| 144 |
+
err = c.Set(ctx, "key1", []byte("value1"), 0)
|
| 145 |
+
if err != nil {
|
| 146 |
+
t.Fatalf("Set failed: %v", err)
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
value, err := c.Get(ctx, "key1")
|
| 150 |
+
if err != nil {
|
| 151 |
+
t.Fatalf("Get failed: %v", err)
|
| 152 |
+
}
|
| 153 |
+
if string(value) != "value1" {
|
| 154 |
+
t.Errorf("Expected 'value1', got '%s'", string(value))
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// TestMultiTierCache_CacheMiss tests cache miss behavior
|
| 159 |
+
func TestMultiTierCache_CacheMiss(t *testing.T) {
|
| 160 |
+
cfg := cache.Config{
|
| 161 |
+
RedisURL: "redis://invalid:6379",
|
| 162 |
+
LocalSize: 100,
|
| 163 |
+
TTL: time.Hour,
|
| 164 |
+
MaxRetries: 1,
|
| 165 |
+
PoolSize: 1,
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
c, err := cache.New(cfg)
|
| 169 |
+
if err != nil {
|
| 170 |
+
t.Fatalf("Failed to create cache: %v", err)
|
| 171 |
+
}
|
| 172 |
+
defer c.Close()
|
| 173 |
+
|
| 174 |
+
ctx := context.Background()
|
| 175 |
+
|
| 176 |
+
_, err = c.Get(ctx, "nonexistent")
|
| 177 |
+
if err == nil {
|
| 178 |
+
t.Error("Expected cache miss error")
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// TestMultiTierCache_Delete tests deletion
|
| 183 |
+
func TestMultiTierCache_Delete(t *testing.T) {
|
| 184 |
+
cfg := cache.Config{
|
| 185 |
+
RedisURL: "redis://invalid:6379",
|
| 186 |
+
LocalSize: 100,
|
| 187 |
+
TTL: time.Hour,
|
| 188 |
+
MaxRetries: 1,
|
| 189 |
+
PoolSize: 1,
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
c, err := cache.New(cfg)
|
| 193 |
+
if err != nil {
|
| 194 |
+
t.Fatalf("Failed to create cache: %v", err)
|
| 195 |
+
}
|
| 196 |
+
defer c.Close()
|
| 197 |
+
|
| 198 |
+
ctx := context.Background()
|
| 199 |
+
|
| 200 |
+
// Set then delete
|
| 201 |
+
c.Set(ctx, "key1", []byte("value1"), 0)
|
| 202 |
+
err = c.Delete(ctx, "key1")
|
| 203 |
+
if err != nil {
|
| 204 |
+
t.Fatalf("Delete failed: %v", err)
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// Should be gone
|
| 208 |
+
_, err = c.Get(ctx, "key1")
|
| 209 |
+
if err == nil {
|
| 210 |
+
t.Error("Expected cache miss after delete")
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// TestMultiTierCache_JSON tests JSON operations
|
| 215 |
+
func TestMultiTierCache_JSON(t *testing.T) {
|
| 216 |
+
cfg := cache.Config{
|
| 217 |
+
RedisURL: "redis://invalid:6379",
|
| 218 |
+
LocalSize: 100,
|
| 219 |
+
TTL: time.Hour,
|
| 220 |
+
MaxRetries: 1,
|
| 221 |
+
PoolSize: 1,
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
c, err := cache.New(cfg)
|
| 225 |
+
if err != nil {
|
| 226 |
+
t.Fatalf("Failed to create cache: %v", err)
|
| 227 |
+
}
|
| 228 |
+
defer c.Close()
|
| 229 |
+
|
| 230 |
+
ctx := context.Background()
|
| 231 |
+
|
| 232 |
+
type testData struct {
|
| 233 |
+
Name string `json:"name"`
|
| 234 |
+
Value int `json:"value"`
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
original := testData{Name: "test", Value: 42}
|
| 238 |
+
|
| 239 |
+
err = c.SetJSON(ctx, "json-key", original, 0)
|
| 240 |
+
if err != nil {
|
| 241 |
+
t.Fatalf("SetJSON failed: %v", err)
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
var result testData
|
| 245 |
+
err = c.GetJSON(ctx, "json-key", &result)
|
| 246 |
+
if err != nil {
|
| 247 |
+
t.Fatalf("GetJSON failed: %v", err)
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
if result.Name != original.Name || result.Value != original.Value {
|
| 251 |
+
t.Errorf("JSON mismatch: expected %+v, got %+v", original, result)
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
// TestMultiTierCache_Metrics tests metrics tracking
|
| 256 |
+
func TestMultiTierCache_Metrics(t *testing.T) {
|
| 257 |
+
cfg := cache.Config{
|
| 258 |
+
RedisURL: "redis://invalid:6379",
|
| 259 |
+
LocalSize: 100,
|
| 260 |
+
TTL: time.Hour,
|
| 261 |
+
MaxRetries: 1,
|
| 262 |
+
PoolSize: 1,
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
c, err := cache.New(cfg)
|
| 266 |
+
if err != nil {
|
| 267 |
+
t.Fatalf("Failed to create cache: %v", err)
|
| 268 |
+
}
|
| 269 |
+
defer c.Close()
|
| 270 |
+
|
| 271 |
+
ctx := context.Background()
|
| 272 |
+
|
| 273 |
+
// Set and get to generate metrics
|
| 274 |
+
c.Set(ctx, "key1", []byte("value1"), 0)
|
| 275 |
+
c.Get(ctx, "key1") // Hit
|
| 276 |
+
c.Get(ctx, "key2") // Miss
|
| 277 |
+
|
| 278 |
+
metrics := c.GetMetrics()
|
| 279 |
+
if metrics.LocalHits != 1 {
|
| 280 |
+
t.Errorf("Expected 1 local hit, got %d", metrics.LocalHits)
|
| 281 |
+
}
|
| 282 |
+
if metrics.LocalMisses != 1 {
|
| 283 |
+
t.Errorf("Expected 1 local miss, got %d", metrics.LocalMisses)
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// BenchmarkLRUCache_Set benchmarks LRU set operations
|
| 288 |
+
func BenchmarkLRUCache_Set(b *testing.B) {
|
| 289 |
+
lru := cache.NewLRUCache(10000)
|
| 290 |
+
|
| 291 |
+
b.ResetTimer()
|
| 292 |
+
for i := 0; i < b.N; i++ {
|
| 293 |
+
lru.Set("key"+string(rune(i%1000)), []byte("value"))
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
// BenchmarkLRUCache_Get benchmarks LRU get operations
|
| 298 |
+
func BenchmarkLRUCache_Get(b *testing.B) {
|
| 299 |
+
lru := cache.NewLRUCache(10000)
|
| 300 |
+
|
| 301 |
+
// Pre-populate
|
| 302 |
+
for i := 0; i < 1000; i++ {
|
| 303 |
+
lru.Set("key"+string(rune(i)), []byte("value"))
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
b.ResetTimer()
|
| 307 |
+
for i := 0; i < b.N; i++ {
|
| 308 |
+
lru.Get("key" + string(rune(i%1000)))
|
| 309 |
+
}
|
| 310 |
+
}
|
internal/gateway/cache/cache.go
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package cache provides caching for the API Gateway.
|
| 2 |
+
package cache
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"context"
|
| 6 |
+
"crypto/sha256"
|
| 7 |
+
"encoding/hex"
|
| 8 |
+
"encoding/json"
|
| 9 |
+
"time"
|
| 10 |
+
|
| 11 |
+
"github.com/redis/go-redis/v9"
|
| 12 |
+
"go.uber.org/zap"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
// Config for cache manager
|
| 16 |
+
type Config struct {
|
| 17 |
+
RedisAddr string
|
| 18 |
+
RedisPassword string
|
| 19 |
+
RedisDB int
|
| 20 |
+
DefaultTTL time.Duration
|
| 21 |
+
MaxEntrySize int
|
| 22 |
+
KeyPrefix string
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Manager handles caching operations
|
| 26 |
+
type Manager struct {
|
| 27 |
+
client *redis.Client
|
| 28 |
+
config Config
|
| 29 |
+
logger *zap.Logger
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// NewManager creates a cache manager
|
| 33 |
+
func NewManager(cfg Config, logger *zap.Logger) (*Manager, error) {
|
| 34 |
+
client := redis.NewClient(&redis.Options{
|
| 35 |
+
Addr: cfg.RedisAddr,
|
| 36 |
+
Password: cfg.RedisPassword,
|
| 37 |
+
DB: cfg.RedisDB,
|
| 38 |
+
})
|
| 39 |
+
|
| 40 |
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
| 41 |
+
defer cancel()
|
| 42 |
+
|
| 43 |
+
if err := client.Ping(ctx).Err(); err != nil {
|
| 44 |
+
return nil, err
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return &Manager{
|
| 48 |
+
client: client,
|
| 49 |
+
config: cfg,
|
| 50 |
+
logger: logger,
|
| 51 |
+
}, nil
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// GenerateKey creates cache key from request
|
| 55 |
+
func (m *Manager) GenerateKey(parts ...string) string {
|
| 56 |
+
h := sha256.New()
|
| 57 |
+
for _, p := range parts {
|
| 58 |
+
h.Write([]byte(p))
|
| 59 |
+
}
|
| 60 |
+
return m.config.KeyPrefix + hex.EncodeToString(h.Sum(nil))
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Get retrieves cached value
|
| 64 |
+
func (m *Manager) Get(ctx context.Context, key string) (interface{}, bool) {
|
| 65 |
+
data, err := m.client.Get(ctx, key).Bytes()
|
| 66 |
+
if err != nil {
|
| 67 |
+
return nil, false
|
| 68 |
+
}
|
| 69 |
+
var result interface{}
|
| 70 |
+
if err := json.Unmarshal(data, &result); err != nil {
|
| 71 |
+
return nil, false
|
| 72 |
+
}
|
| 73 |
+
return result, true
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Set stores value in cache
|
| 77 |
+
func (m *Manager) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
|
| 78 |
+
data, err := json.Marshal(value)
|
| 79 |
+
if err != nil {
|
| 80 |
+
return err
|
| 81 |
+
}
|
| 82 |
+
if len(data) > m.config.MaxEntrySize {
|
| 83 |
+
m.logger.Warn("Cache entry exceeds max size", zap.String("key", key))
|
| 84 |
+
return nil
|
| 85 |
+
}
|
| 86 |
+
if ttl == 0 {
|
| 87 |
+
ttl = m.config.DefaultTTL
|
| 88 |
+
}
|
| 89 |
+
return m.client.Set(ctx, key, data, ttl).Err()
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Delete removes value from cache
|
| 93 |
+
func (m *Manager) Delete(ctx context.Context, key string) error {
|
| 94 |
+
return m.client.Del(ctx, key).Err()
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// Close closes Redis connection
|
| 98 |
+
func (m *Manager) Close() error {
|
| 99 |
+
return m.client.Close()
|
| 100 |
+
}
|
internal/gateway/cmd/main.go
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Gateway entry point
|
| 2 |
+
package main
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"flag"
|
| 6 |
+
"os"
|
| 7 |
+
|
| 8 |
+
"github.com/spf13/viper"
|
| 9 |
+
"go.uber.org/zap"
|
| 10 |
+
|
| 11 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func main() {
|
| 15 |
+
configPath := flag.String("config", "gateway.yaml", "Path to config file")
|
| 16 |
+
flag.Parse()
|
| 17 |
+
|
| 18 |
+
// Initialize logger
|
| 19 |
+
logger, _ := zap.NewProduction()
|
| 20 |
+
defer logger.Sync()
|
| 21 |
+
|
| 22 |
+
// Load configuration
|
| 23 |
+
cfg, err := loadConfig(*configPath)
|
| 24 |
+
if err != nil {
|
| 25 |
+
logger.Fatal("Failed to load config", zap.Error(err))
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// Create and start gateway
|
| 29 |
+
gw, err := gateway.New(cfg, logger)
|
| 30 |
+
if err != nil {
|
| 31 |
+
logger.Fatal("Failed to create gateway", zap.Error(err))
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
logger.Info("Starting API Gateway", zap.String("addr", cfg.Server.BindAddr))
|
| 35 |
+
if err := gw.Start(); err != nil {
|
| 36 |
+
logger.Fatal("Gateway error", zap.Error(err))
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
func loadConfig(path string) (*gateway.Config, error) {
|
| 41 |
+
v := viper.New()
|
| 42 |
+
v.SetConfigFile(path)
|
| 43 |
+
v.SetConfigType("yaml")
|
| 44 |
+
|
| 45 |
+
// Environment variable overrides
|
| 46 |
+
v.AutomaticEnv()
|
| 47 |
+
v.SetEnvPrefix("GATEWAY")
|
| 48 |
+
|
| 49 |
+
// Set defaults
|
| 50 |
+
cfg := gateway.DefaultConfig()
|
| 51 |
+
|
| 52 |
+
if err := v.ReadInConfig(); err != nil {
|
| 53 |
+
if !os.IsNotExist(err) {
|
| 54 |
+
return nil, err
|
| 55 |
+
}
|
| 56 |
+
// Use defaults if config file doesn't exist
|
| 57 |
+
return cfg, nil
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if err := v.Unmarshal(cfg); err != nil {
|
| 61 |
+
return nil, err
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
return cfg, nil
|
| 65 |
+
}
|
internal/gateway/config.go
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package gateway provides the API Gateway for the RAG Agent Framework.
|
| 2 |
+
// It implements a multi-protocol gateway supporting REST, GraphQL, WebSocket, and SSE,
|
| 3 |
+
// with comprehensive middleware for security, observability, and performance.
|
| 4 |
+
package gateway
|
| 5 |
+
|
| 6 |
+
import (
|
| 7 |
+
"time"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
// Config holds all gateway configuration
|
| 11 |
+
type Config struct {
|
| 12 |
+
Server ServerConfig `yaml:"server"`
|
| 13 |
+
TLS TLSConfig `yaml:"tls"`
|
| 14 |
+
RateLimit RateLimitConfig `yaml:"rateLimit"`
|
| 15 |
+
CORS CORSConfig `yaml:"cors"`
|
| 16 |
+
Auth AuthConfig `yaml:"auth"`
|
| 17 |
+
Cache CacheConfig `yaml:"cache"`
|
| 18 |
+
ServiceDiscovery ServiceDiscoveryConfig `yaml:"serviceDiscovery"`
|
| 19 |
+
CircuitBreaker CircuitBreakerConfig `yaml:"circuitBreaker"`
|
| 20 |
+
Observability ObservabilityConfig `yaml:"observability"`
|
| 21 |
+
WebSocket WebSocketConfig `yaml:"websocket"`
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// ServerConfig contains HTTP server settings
|
| 25 |
+
type ServerConfig struct {
|
| 26 |
+
BindAddr string `yaml:"bindAddr"`
|
| 27 |
+
ReadTimeout time.Duration `yaml:"readTimeout"`
|
| 28 |
+
WriteTimeout time.Duration `yaml:"writeTimeout"`
|
| 29 |
+
IdleTimeout time.Duration `yaml:"idleTimeout"`
|
| 30 |
+
ShutdownTimeout time.Duration `yaml:"shutdownTimeout"`
|
| 31 |
+
MaxHeaderBytes int `yaml:"maxHeaderBytes"`
|
| 32 |
+
EnableHTTP2 bool `yaml:"enableHttp2"`
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// TLSConfig holds TLS certificate settings
|
| 36 |
+
type TLSConfig struct {
|
| 37 |
+
Enabled bool `yaml:"enabled"`
|
| 38 |
+
CertFile string `yaml:"certFile"`
|
| 39 |
+
KeyFile string `yaml:"keyFile"`
|
| 40 |
+
MinVersion string `yaml:"minVersion"`
|
| 41 |
+
ClientCAFile string `yaml:"clientCaFile"`
|
| 42 |
+
RequireClientCert bool `yaml:"requireClientCert"`
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// RateLimitConfig specifies rate limiting parameters
|
| 46 |
+
type RateLimitConfig struct {
|
| 47 |
+
Enabled bool `yaml:"enabled"`
|
| 48 |
+
RequestsPerSec float64 `yaml:"requestsPerSec"`
|
| 49 |
+
BurstSize int `yaml:"burstSize"`
|
| 50 |
+
PerTenant bool `yaml:"perTenant"`
|
| 51 |
+
PerUser bool `yaml:"perUser"`
|
| 52 |
+
RedisEnabled bool `yaml:"redisEnabled"`
|
| 53 |
+
CleanupInterval time.Duration `yaml:"cleanupInterval"`
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// CORSConfig specifies CORS settings
|
| 57 |
+
type CORSConfig struct {
|
| 58 |
+
AllowedOrigins []string `yaml:"allowedOrigins"`
|
| 59 |
+
AllowedMethods []string `yaml:"allowedMethods"`
|
| 60 |
+
AllowedHeaders []string `yaml:"allowedHeaders"`
|
| 61 |
+
ExposedHeaders []string `yaml:"exposedHeaders"`
|
| 62 |
+
AllowCredentials bool `yaml:"allowCredentials"`
|
| 63 |
+
MaxAge time.Duration `yaml:"maxAge"`
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// AuthConfig contains authentication settings
|
| 67 |
+
type AuthConfig struct {
|
| 68 |
+
JWTSecret string `yaml:"jwtSecret"`
|
| 69 |
+
JWTPublicKeyFile string `yaml:"jwtPublicKeyFile"`
|
| 70 |
+
JWTIssuer string `yaml:"jwtIssuer"`
|
| 71 |
+
JWTAudience string `yaml:"jwtAudience"`
|
| 72 |
+
TokenDuration time.Duration `yaml:"tokenDuration"`
|
| 73 |
+
OPAEnabled bool `yaml:"opaEnabled"`
|
| 74 |
+
OPAAddr string `yaml:"opaAddr"`
|
| 75 |
+
OPAPolicy string `yaml:"opaPolicy"`
|
| 76 |
+
SkipPaths []string `yaml:"skipPaths"`
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// CacheConfig specifies caching settings
|
| 80 |
+
type CacheConfig struct {
|
| 81 |
+
Enabled bool `yaml:"enabled"`
|
| 82 |
+
RedisAddr string `yaml:"redisAddr"`
|
| 83 |
+
RedisPassword string `yaml:"redisPassword"`
|
| 84 |
+
RedisDB int `yaml:"redisDb"`
|
| 85 |
+
DefaultTTL time.Duration `yaml:"defaultTtl"`
|
| 86 |
+
MaxEntrySize int `yaml:"maxEntrySize"`
|
| 87 |
+
KeyPrefix string `yaml:"keyPrefix"`
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// ServiceDiscoveryConfig for Consul-based service discovery
|
| 91 |
+
type ServiceDiscoveryConfig struct {
|
| 92 |
+
Enabled bool `yaml:"enabled"`
|
| 93 |
+
ConsulAddr string `yaml:"consulAddr"`
|
| 94 |
+
ConsulToken string `yaml:"consulToken"`
|
| 95 |
+
ConsulDatacenter string `yaml:"consulDatacenter"`
|
| 96 |
+
ServiceRefreshInterval time.Duration `yaml:"serviceRefreshInterval"`
|
| 97 |
+
// Direct service addresses (when Consul is disabled)
|
| 98 |
+
AgentServiceAddr string `yaml:"agentServiceAddr"`
|
| 99 |
+
RetrieverServiceAddr string `yaml:"retrieverServiceAddr"`
|
| 100 |
+
GeneratorServiceAddr string `yaml:"generatorServiceAddr"`
|
| 101 |
+
MemoryServiceAddr string `yaml:"memoryServiceAddr"`
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// CircuitBreakerConfig for per-service circuit breakers
|
| 105 |
+
type CircuitBreakerConfig struct {
|
| 106 |
+
MaxRequests uint32 `yaml:"maxRequests"`
|
| 107 |
+
Interval time.Duration `yaml:"interval"`
|
| 108 |
+
Timeout time.Duration `yaml:"timeout"`
|
| 109 |
+
FailureThreshold uint32 `yaml:"failureThreshold"`
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// ObservabilityConfig for tracing and metrics
|
| 113 |
+
type ObservabilityConfig struct {
|
| 114 |
+
MetricsEnabled bool `yaml:"metricsEnabled"`
|
| 115 |
+
MetricsPath string `yaml:"metricsPath"`
|
| 116 |
+
TracingEnabled bool `yaml:"tracingEnabled"`
|
| 117 |
+
TracingEndpoint string `yaml:"tracingEndpoint"`
|
| 118 |
+
ServiceName string `yaml:"serviceName"`
|
| 119 |
+
AuditLogEnabled bool `yaml:"auditLogEnabled"`
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// WebSocketConfig for WebSocket connections
|
| 123 |
+
type WebSocketConfig struct {
|
| 124 |
+
ReadBufferSize int `yaml:"readBufferSize"`
|
| 125 |
+
WriteBufferSize int `yaml:"writeBufferSize"`
|
| 126 |
+
PingInterval time.Duration `yaml:"pingInterval"`
|
| 127 |
+
PongWait time.Duration `yaml:"pongWait"`
|
| 128 |
+
WriteWait time.Duration `yaml:"writeWait"`
|
| 129 |
+
MaxMessageSize int64 `yaml:"maxMessageSize"`
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// DefaultConfig returns a configuration with sensible defaults
|
| 133 |
+
func DefaultConfig() *Config {
|
| 134 |
+
return &Config{
|
| 135 |
+
Server: ServerConfig{
|
| 136 |
+
BindAddr: ":8443",
|
| 137 |
+
ReadTimeout: 30 * time.Second,
|
| 138 |
+
WriteTimeout: 60 * time.Second,
|
| 139 |
+
IdleTimeout: 120 * time.Second,
|
| 140 |
+
ShutdownTimeout: 30 * time.Second,
|
| 141 |
+
MaxHeaderBytes: 1 << 20, // 1MB
|
| 142 |
+
EnableHTTP2: true,
|
| 143 |
+
},
|
| 144 |
+
TLS: TLSConfig{
|
| 145 |
+
Enabled: false,
|
| 146 |
+
MinVersion: "1.2",
|
| 147 |
+
},
|
| 148 |
+
RateLimit: RateLimitConfig{
|
| 149 |
+
Enabled: true,
|
| 150 |
+
RequestsPerSec: 100,
|
| 151 |
+
BurstSize: 200,
|
| 152 |
+
PerTenant: true,
|
| 153 |
+
PerUser: true,
|
| 154 |
+
CleanupInterval: 10 * time.Minute,
|
| 155 |
+
},
|
| 156 |
+
CORS: CORSConfig{
|
| 157 |
+
AllowedOrigins: []string{"*"},
|
| 158 |
+
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
| 159 |
+
AllowedHeaders: []string{"Authorization", "Content-Type", "X-Request-ID", "X-Client-Version"},
|
| 160 |
+
ExposedHeaders: []string{"X-Request-ID", "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"},
|
| 161 |
+
AllowCredentials: true,
|
| 162 |
+
MaxAge: 3600 * time.Second,
|
| 163 |
+
},
|
| 164 |
+
Auth: AuthConfig{
|
| 165 |
+
TokenDuration: 24 * time.Hour,
|
| 166 |
+
OPAPolicy: "authz/allow",
|
| 167 |
+
SkipPaths: []string{"/admin/health", "/admin/metrics"},
|
| 168 |
+
},
|
| 169 |
+
Cache: CacheConfig{
|
| 170 |
+
Enabled: true,
|
| 171 |
+
DefaultTTL: 5 * time.Minute,
|
| 172 |
+
MaxEntrySize: 1 << 20, // 1MB
|
| 173 |
+
KeyPrefix: "rag:gateway:",
|
| 174 |
+
},
|
| 175 |
+
ServiceDiscovery: ServiceDiscoveryConfig{
|
| 176 |
+
ServiceRefreshInterval: 30 * time.Second,
|
| 177 |
+
AgentServiceAddr: "localhost:9090",
|
| 178 |
+
RetrieverServiceAddr: "localhost:9091",
|
| 179 |
+
GeneratorServiceAddr: "localhost:9092",
|
| 180 |
+
MemoryServiceAddr: "localhost:9093",
|
| 181 |
+
},
|
| 182 |
+
CircuitBreaker: CircuitBreakerConfig{
|
| 183 |
+
MaxRequests: 5,
|
| 184 |
+
Interval: 60 * time.Second,
|
| 185 |
+
Timeout: 30 * time.Second,
|
| 186 |
+
FailureThreshold: 3,
|
| 187 |
+
},
|
| 188 |
+
Observability: ObservabilityConfig{
|
| 189 |
+
MetricsEnabled: true,
|
| 190 |
+
MetricsPath: "/admin/metrics",
|
| 191 |
+
TracingEnabled: true,
|
| 192 |
+
ServiceName: "api-gateway",
|
| 193 |
+
AuditLogEnabled: true,
|
| 194 |
+
},
|
| 195 |
+
WebSocket: WebSocketConfig{
|
| 196 |
+
ReadBufferSize: 1024,
|
| 197 |
+
WriteBufferSize: 1024,
|
| 198 |
+
PingInterval: 30 * time.Second,
|
| 199 |
+
PongWait: 60 * time.Second,
|
| 200 |
+
WriteWait: 10 * time.Second,
|
| 201 |
+
MaxMessageSize: 512 * 1024, // 512KB
|
| 202 |
+
},
|
| 203 |
+
}
|
| 204 |
+
}
|
internal/gateway/gateway.go
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package gateway
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"crypto/tls"
|
| 6 |
+
"crypto/x509"
|
| 7 |
+
"errors"
|
| 8 |
+
"fmt"
|
| 9 |
+
"net/http"
|
| 10 |
+
"os"
|
| 11 |
+
"os/signal"
|
| 12 |
+
"sync"
|
| 13 |
+
"syscall"
|
| 14 |
+
|
| 15 |
+
"github.com/gorilla/mux"
|
| 16 |
+
"github.com/prometheus/client_golang/prometheus/promhttp"
|
| 17 |
+
"go.opentelemetry.io/otel"
|
| 18 |
+
"go.opentelemetry.io/otel/trace"
|
| 19 |
+
"go.uber.org/zap"
|
| 20 |
+
|
| 21 |
+
gatewaycache "github.com/AmaniQuery/amaniquery/internal/gateway/cache"
|
| 22 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/gwtypes"
|
| 23 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/handlers"
|
| 24 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/middleware"
|
| 25 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/services"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
// Gateway is the main API Gateway server
|
| 29 |
+
type Gateway struct {
|
| 30 |
+
config *Config
|
| 31 |
+
router *mux.Router
|
| 32 |
+
server *http.Server
|
| 33 |
+
logger *zap.Logger
|
| 34 |
+
tracer trace.Tracer
|
| 35 |
+
|
| 36 |
+
// Middleware
|
| 37 |
+
corsMiddleware *middleware.CORSMiddleware
|
| 38 |
+
rateLimitMiddleware *middleware.RateLimitMiddleware
|
| 39 |
+
authMiddleware *middleware.AuthMiddleware
|
| 40 |
+
auditMiddleware *middleware.AuditMiddleware
|
| 41 |
+
tracingMiddleware *middleware.TracingMiddleware
|
| 42 |
+
|
| 43 |
+
// Services
|
| 44 |
+
serviceRegistry *services.Registry
|
| 45 |
+
cacheManager *gatewaycache.Manager
|
| 46 |
+
|
| 47 |
+
// Handlers
|
| 48 |
+
queryHandler *handlers.QueryHandler
|
| 49 |
+
memoryHandler *handlers.MemoryHandler
|
| 50 |
+
agentHandler *handlers.AgentHandler
|
| 51 |
+
wsHandler *handlers.WebSocketHandler
|
| 52 |
+
adminHandler *handlers.AdminHandler
|
| 53 |
+
authHandler *handlers.AuthHandler
|
| 54 |
+
graphqlHandler *handlers.GraphQLHandler
|
| 55 |
+
|
| 56 |
+
// Lifecycle
|
| 57 |
+
wg sync.WaitGroup
|
| 58 |
+
shutdown chan struct{}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// New creates a new Gateway instance
|
| 62 |
+
func New(cfg *Config, logger *zap.Logger) (*Gateway, error) {
|
| 63 |
+
if cfg == nil {
|
| 64 |
+
cfg = DefaultConfig()
|
| 65 |
+
}
|
| 66 |
+
if logger == nil {
|
| 67 |
+
var err error
|
| 68 |
+
logger, err = zap.NewProduction()
|
| 69 |
+
if err != nil {
|
| 70 |
+
return nil, fmt.Errorf("failed to create logger: %w", err)
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
g := &Gateway{
|
| 75 |
+
config: cfg,
|
| 76 |
+
router: mux.NewRouter(),
|
| 77 |
+
logger: logger,
|
| 78 |
+
tracer: otel.Tracer(cfg.Observability.ServiceName),
|
| 79 |
+
shutdown: make(chan struct{}),
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Initialize components
|
| 83 |
+
if err := g.initializeMiddleware(); err != nil {
|
| 84 |
+
return nil, fmt.Errorf("failed to initialize middleware: %w", err)
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
if err := g.initializeServices(); err != nil {
|
| 88 |
+
return nil, fmt.Errorf("failed to initialize services: %w", err)
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
if err := g.initializeHandlers(); err != nil {
|
| 92 |
+
return nil, fmt.Errorf("failed to initialize handlers: %w", err)
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
g.setupRoutes()
|
| 96 |
+
|
| 97 |
+
return g, nil
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// initializeMiddleware sets up all middleware components
|
| 101 |
+
func (g *Gateway) initializeMiddleware() error {
|
| 102 |
+
// CORS middleware
|
| 103 |
+
g.corsMiddleware = middleware.NewCORSMiddleware(middleware.CORSConfig{
|
| 104 |
+
AllowedOrigins: g.config.CORS.AllowedOrigins,
|
| 105 |
+
AllowedMethods: g.config.CORS.AllowedMethods,
|
| 106 |
+
AllowedHeaders: g.config.CORS.AllowedHeaders,
|
| 107 |
+
ExposedHeaders: g.config.CORS.ExposedHeaders,
|
| 108 |
+
AllowCredentials: g.config.CORS.AllowCredentials,
|
| 109 |
+
MaxAge: int(g.config.CORS.MaxAge.Seconds()),
|
| 110 |
+
})
|
| 111 |
+
|
| 112 |
+
// Rate limiting middleware
|
| 113 |
+
if g.config.RateLimit.Enabled {
|
| 114 |
+
var err error
|
| 115 |
+
g.rateLimitMiddleware, err = middleware.NewRateLimitMiddleware(middleware.RateLimitConfig{
|
| 116 |
+
RequestsPerSec: g.config.RateLimit.RequestsPerSec,
|
| 117 |
+
BurstSize: g.config.RateLimit.BurstSize,
|
| 118 |
+
PerTenant: g.config.RateLimit.PerTenant,
|
| 119 |
+
PerUser: g.config.RateLimit.PerUser,
|
| 120 |
+
RedisAddr: g.config.Cache.RedisAddr,
|
| 121 |
+
RedisEnabled: g.config.RateLimit.RedisEnabled,
|
| 122 |
+
CleanupInterval: g.config.RateLimit.CleanupInterval,
|
| 123 |
+
}, g.logger)
|
| 124 |
+
if err != nil {
|
| 125 |
+
return fmt.Errorf("failed to create rate limit middleware: %w", err)
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Auth middleware
|
| 130 |
+
g.authMiddleware = middleware.NewAuthMiddleware(middleware.AuthConfig{
|
| 131 |
+
JWTSecret: g.config.Auth.JWTSecret,
|
| 132 |
+
JWTIssuer: g.config.Auth.JWTIssuer,
|
| 133 |
+
JWTAudience: g.config.Auth.JWTAudience,
|
| 134 |
+
OPAEnabled: g.config.Auth.OPAEnabled,
|
| 135 |
+
OPAAddr: g.config.Auth.OPAAddr,
|
| 136 |
+
OPAPolicy: g.config.Auth.OPAPolicy,
|
| 137 |
+
SkipPaths: g.config.Auth.SkipPaths,
|
| 138 |
+
}, g.logger)
|
| 139 |
+
|
| 140 |
+
// Audit middleware
|
| 141 |
+
if g.config.Observability.AuditLogEnabled {
|
| 142 |
+
g.auditMiddleware = middleware.NewAuditMiddleware(g.logger)
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Tracing middleware
|
| 146 |
+
if g.config.Observability.TracingEnabled {
|
| 147 |
+
g.tracingMiddleware = middleware.NewTracingMiddleware(g.tracer)
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
return nil
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
// initializeServices sets up backend service connections
|
| 154 |
+
func (g *Gateway) initializeServices() error {
|
| 155 |
+
var err error
|
| 156 |
+
|
| 157 |
+
// Service registry
|
| 158 |
+
g.serviceRegistry, err = services.NewRegistry(services.RegistryConfig{
|
| 159 |
+
ConsulEnabled: g.config.ServiceDiscovery.Enabled,
|
| 160 |
+
ConsulAddr: g.config.ServiceDiscovery.ConsulAddr,
|
| 161 |
+
ConsulToken: g.config.ServiceDiscovery.ConsulToken,
|
| 162 |
+
RefreshInterval: g.config.ServiceDiscovery.ServiceRefreshInterval,
|
| 163 |
+
AgentAddr: g.config.ServiceDiscovery.AgentServiceAddr,
|
| 164 |
+
RetrieverAddr: g.config.ServiceDiscovery.RetrieverServiceAddr,
|
| 165 |
+
GeneratorAddr: g.config.ServiceDiscovery.GeneratorServiceAddr,
|
| 166 |
+
MemoryAddr: g.config.ServiceDiscovery.MemoryServiceAddr,
|
| 167 |
+
CircuitBreakerCfg: services.CircuitBreakerConfig{
|
| 168 |
+
MaxRequests: g.config.CircuitBreaker.MaxRequests,
|
| 169 |
+
Interval: g.config.CircuitBreaker.Interval,
|
| 170 |
+
Timeout: g.config.CircuitBreaker.Timeout,
|
| 171 |
+
FailureThreshold: g.config.CircuitBreaker.FailureThreshold,
|
| 172 |
+
},
|
| 173 |
+
}, g.logger)
|
| 174 |
+
if err != nil {
|
| 175 |
+
return fmt.Errorf("failed to create service registry: %w", err)
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// Cache manager
|
| 179 |
+
if g.config.Cache.Enabled {
|
| 180 |
+
g.cacheManager, err = gatewaycache.NewManager(gatewaycache.Config{
|
| 181 |
+
RedisAddr: g.config.Cache.RedisAddr,
|
| 182 |
+
RedisPassword: g.config.Cache.RedisPassword,
|
| 183 |
+
RedisDB: g.config.Cache.RedisDB,
|
| 184 |
+
DefaultTTL: g.config.Cache.DefaultTTL,
|
| 185 |
+
MaxEntrySize: g.config.Cache.MaxEntrySize,
|
| 186 |
+
KeyPrefix: g.config.Cache.KeyPrefix,
|
| 187 |
+
}, g.logger)
|
| 188 |
+
if err != nil {
|
| 189 |
+
g.logger.Warn("Failed to create cache manager, caching disabled", zap.Error(err))
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
return nil
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
// initializeHandlers sets up all request handlers
|
| 197 |
+
func (g *Gateway) initializeHandlers() error {
|
| 198 |
+
// Query handler
|
| 199 |
+
g.queryHandler = handlers.NewQueryHandler(
|
| 200 |
+
g.serviceRegistry,
|
| 201 |
+
g.cacheManager,
|
| 202 |
+
g.logger,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
// Memory handler
|
| 206 |
+
g.memoryHandler = handlers.NewMemoryHandler(
|
| 207 |
+
g.serviceRegistry,
|
| 208 |
+
g.logger,
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
// Agent handler
|
| 212 |
+
g.agentHandler = handlers.NewAgentHandler(
|
| 213 |
+
g.serviceRegistry,
|
| 214 |
+
g.logger,
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
// WebSocket handler
|
| 218 |
+
g.wsHandler = handlers.NewWebSocketHandler(
|
| 219 |
+
g.serviceRegistry,
|
| 220 |
+
gwtypes.WebSocketConfig{
|
| 221 |
+
ReadBufferSize: g.config.WebSocket.ReadBufferSize,
|
| 222 |
+
WriteBufferSize: g.config.WebSocket.WriteBufferSize,
|
| 223 |
+
PingInterval: g.config.WebSocket.PingInterval,
|
| 224 |
+
PongWait: g.config.WebSocket.PongWait,
|
| 225 |
+
WriteWait: g.config.WebSocket.WriteWait,
|
| 226 |
+
MaxMessageSize: g.config.WebSocket.MaxMessageSize,
|
| 227 |
+
},
|
| 228 |
+
g.logger,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
// Admin handler
|
| 232 |
+
g.adminHandler = handlers.NewAdminHandler(
|
| 233 |
+
g.serviceRegistry,
|
| 234 |
+
g.logger,
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
// Auth handler
|
| 238 |
+
g.authHandler = handlers.NewAuthHandler(
|
| 239 |
+
middleware.AuthConfig{
|
| 240 |
+
JWTSecret: g.config.Auth.JWTSecret,
|
| 241 |
+
JWTIssuer: g.config.Auth.JWTIssuer,
|
| 242 |
+
JWTAudience: g.config.Auth.JWTAudience,
|
| 243 |
+
},
|
| 244 |
+
g.logger,
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
// GraphQL handler
|
| 248 |
+
g.graphqlHandler = handlers.NewGraphQLHandler(
|
| 249 |
+
g.serviceRegistry,
|
| 250 |
+
g.logger,
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
return nil
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
// setupRoutes configures all API routes
|
| 257 |
+
func (g *Gateway) setupRoutes() {
|
| 258 |
+
// Global middleware
|
| 259 |
+
g.router.Use(middleware.Recovery(g.logger))
|
| 260 |
+
g.router.Use(middleware.RequestID())
|
| 261 |
+
g.router.Use(g.corsMiddleware.Handler)
|
| 262 |
+
|
| 263 |
+
if g.tracingMiddleware != nil {
|
| 264 |
+
g.router.Use(g.tracingMiddleware.Handler)
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
// Public routes (no auth required)
|
| 268 |
+
g.router.HandleFunc("/admin/health", g.adminHandler.HealthCheck).Methods("GET")
|
| 269 |
+
g.router.Handle("/admin/metrics", promhttp.Handler()).Methods("GET")
|
| 270 |
+
|
| 271 |
+
// API v2 routes
|
| 272 |
+
v2 := g.router.PathPrefix("/v2").Subrouter()
|
| 273 |
+
|
| 274 |
+
// Apply rate limiting
|
| 275 |
+
if g.rateLimitMiddleware != nil {
|
| 276 |
+
v2.Use(g.rateLimitMiddleware.Handler)
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// Apply authentication
|
| 280 |
+
v2.Use(g.authMiddleware.Handler)
|
| 281 |
+
|
| 282 |
+
// Apply audit logging
|
| 283 |
+
if g.auditMiddleware != nil {
|
| 284 |
+
v2.Use(g.auditMiddleware.Handler)
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// Query routes
|
| 288 |
+
queries := v2.PathPrefix("/queries").Subrouter()
|
| 289 |
+
queries.HandleFunc("", g.queryHandler.ExecuteQuery).Methods("POST")
|
| 290 |
+
queries.HandleFunc("/stream", g.wsHandler.HandleStream)
|
| 291 |
+
queries.HandleFunc("/{queryId}", g.queryHandler.GetQueryResult).Methods("GET")
|
| 292 |
+
|
| 293 |
+
// Memory routes
|
| 294 |
+
memory := v2.PathPrefix("/memory").Subrouter()
|
| 295 |
+
memory.HandleFunc("/context", g.memoryHandler.GetContextWindow).Methods("GET")
|
| 296 |
+
memory.HandleFunc("/sessions/{sessionId}/consolidate", g.memoryHandler.ConsolidateMemory).Methods("POST")
|
| 297 |
+
|
| 298 |
+
// Agent routes
|
| 299 |
+
agents := v2.PathPrefix("/agents").Subrouter()
|
| 300 |
+
agents.HandleFunc("", g.agentHandler.CreateAgent).Methods("POST")
|
| 301 |
+
agents.HandleFunc("/{agentId}", g.agentHandler.GetAgent).Methods("GET")
|
| 302 |
+
agents.HandleFunc("/{agentId}", g.agentHandler.DeleteAgent).Methods("DELETE")
|
| 303 |
+
agents.HandleFunc("/{agentId}/execute", g.agentHandler.ExecutePlan).Methods("POST")
|
| 304 |
+
|
| 305 |
+
// Auth routes
|
| 306 |
+
v2.HandleFunc("/auth/token", g.authHandler.Token).Methods("POST")
|
| 307 |
+
|
| 308 |
+
// GraphQL route
|
| 309 |
+
v2.Handle("/graphql", g.graphqlHandler).Methods("POST", "GET")
|
| 310 |
+
|
| 311 |
+
// CORS preflight handler
|
| 312 |
+
g.router.Methods("OPTIONS").HandlerFunc(g.corsMiddleware.HandlePreflight)
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
// Start starts the gateway server
|
| 316 |
+
func (g *Gateway) Start() error {
|
| 317 |
+
// Build TLS config if enabled
|
| 318 |
+
var tlsConfig *tls.Config
|
| 319 |
+
if g.config.TLS.Enabled {
|
| 320 |
+
var err error
|
| 321 |
+
tlsConfig, err = g.buildTLSConfig()
|
| 322 |
+
if err != nil {
|
| 323 |
+
return fmt.Errorf("failed to build TLS config: %w", err)
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
// Create HTTP server
|
| 328 |
+
g.server = &http.Server{
|
| 329 |
+
Addr: g.config.Server.BindAddr,
|
| 330 |
+
Handler: g.router,
|
| 331 |
+
ReadTimeout: g.config.Server.ReadTimeout,
|
| 332 |
+
WriteTimeout: g.config.Server.WriteTimeout,
|
| 333 |
+
IdleTimeout: g.config.Server.IdleTimeout,
|
| 334 |
+
MaxHeaderBytes: g.config.Server.MaxHeaderBytes,
|
| 335 |
+
TLSConfig: tlsConfig,
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
// Start server in goroutine
|
| 339 |
+
g.wg.Add(1)
|
| 340 |
+
go func() {
|
| 341 |
+
defer g.wg.Done()
|
| 342 |
+
|
| 343 |
+
g.logger.Info("Gateway starting",
|
| 344 |
+
zap.String("addr", g.config.Server.BindAddr),
|
| 345 |
+
zap.Bool("tls", g.config.TLS.Enabled),
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
var err error
|
| 349 |
+
if g.config.TLS.Enabled {
|
| 350 |
+
err = g.server.ListenAndServeTLS(g.config.TLS.CertFile, g.config.TLS.KeyFile)
|
| 351 |
+
} else {
|
| 352 |
+
err = g.server.ListenAndServe()
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
| 356 |
+
g.logger.Error("Server error", zap.Error(err))
|
| 357 |
+
}
|
| 358 |
+
}()
|
| 359 |
+
|
| 360 |
+
// Wait for shutdown signal
|
| 361 |
+
g.handleShutdown()
|
| 362 |
+
|
| 363 |
+
return nil
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
// handleShutdown gracefully stops the server on interrupt signals
|
| 367 |
+
func (g *Gateway) handleShutdown() {
|
| 368 |
+
sigChan := make(chan os.Signal, 1)
|
| 369 |
+
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
| 370 |
+
|
| 371 |
+
select {
|
| 372 |
+
case sig := <-sigChan:
|
| 373 |
+
g.logger.Info("Received shutdown signal", zap.String("signal", sig.String()))
|
| 374 |
+
case <-g.shutdown:
|
| 375 |
+
g.logger.Info("Shutdown requested")
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
g.Stop()
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
// Stop gracefully stops the gateway
|
| 382 |
+
func (g *Gateway) Stop() {
|
| 383 |
+
ctx, cancel := context.WithTimeout(context.Background(), g.config.Server.ShutdownTimeout)
|
| 384 |
+
defer cancel()
|
| 385 |
+
|
| 386 |
+
g.logger.Info("Shutting down gateway...")
|
| 387 |
+
|
| 388 |
+
// Shutdown HTTP server
|
| 389 |
+
if g.server != nil {
|
| 390 |
+
if err := g.server.Shutdown(ctx); err != nil {
|
| 391 |
+
g.logger.Error("Server shutdown error", zap.Error(err))
|
| 392 |
+
}
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
// Close service connections
|
| 396 |
+
if g.serviceRegistry != nil {
|
| 397 |
+
g.serviceRegistry.Close()
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// Close cache manager
|
| 401 |
+
if g.cacheManager != nil {
|
| 402 |
+
g.cacheManager.Close()
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
// Close WebSocket connections
|
| 406 |
+
if g.wsHandler != nil {
|
| 407 |
+
g.wsHandler.CloseAll()
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// Wait for goroutines
|
| 411 |
+
g.wg.Wait()
|
| 412 |
+
|
| 413 |
+
g.logger.Info("Gateway stopped")
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
// Shutdown requests a graceful shutdown
|
| 417 |
+
func (g *Gateway) Shutdown() {
|
| 418 |
+
close(g.shutdown)
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// buildTLSConfig creates TLS configuration
|
| 422 |
+
func (g *Gateway) buildTLSConfig() (*tls.Config, error) {
|
| 423 |
+
tlsConfig := &tls.Config{
|
| 424 |
+
MinVersion: tls.VersionTLS12,
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
// Set minimum TLS version
|
| 428 |
+
switch g.config.TLS.MinVersion {
|
| 429 |
+
case "1.2":
|
| 430 |
+
tlsConfig.MinVersion = tls.VersionTLS12
|
| 431 |
+
case "1.3":
|
| 432 |
+
tlsConfig.MinVersion = tls.VersionTLS13
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
// Load client CA if mTLS is required
|
| 436 |
+
if g.config.TLS.RequireClientCert && g.config.TLS.ClientCAFile != "" {
|
| 437 |
+
caCert, err := os.ReadFile(g.config.TLS.ClientCAFile)
|
| 438 |
+
if err != nil {
|
| 439 |
+
return nil, fmt.Errorf("failed to read client CA file: %w", err)
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
caCertPool := x509.NewCertPool()
|
| 443 |
+
if !caCertPool.AppendCertsFromPEM(caCert) {
|
| 444 |
+
return nil, errors.New("failed to parse client CA certificate")
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
tlsConfig.ClientCAs = caCertPool
|
| 448 |
+
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
return tlsConfig, nil
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
// Router returns the underlying router for testing
|
| 455 |
+
func (g *Gateway) Router() *mux.Router {
|
| 456 |
+
return g.router
|
| 457 |
+
}
|
internal/gateway/gwtypes/types.go
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package gwtypes provides shared types for the gateway and handlers.
|
| 2 |
+
package gwtypes
|
| 3 |
+
|
| 4 |
+
import "time"
|
| 5 |
+
|
| 6 |
+
// WebSocketConfig for WebSocket connections
|
| 7 |
+
type WebSocketConfig struct {
|
| 8 |
+
ReadBufferSize int `yaml:"readBufferSize"`
|
| 9 |
+
WriteBufferSize int `yaml:"writeBufferSize"`
|
| 10 |
+
PingInterval time.Duration `yaml:"pingInterval"`
|
| 11 |
+
PongWait time.Duration `yaml:"pongWait"`
|
| 12 |
+
WriteWait time.Duration `yaml:"writeWait"`
|
| 13 |
+
MaxMessageSize int64 `yaml:"maxMessageSize"`
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// REST API request/response types matching OpenAPI spec
|
| 17 |
+
|
| 18 |
+
// QueryRequest represents a RAG query request
|
| 19 |
+
type QueryRequest struct {
|
| 20 |
+
Query string `json:"query"`
|
| 21 |
+
UserID string `json:"userId"`
|
| 22 |
+
SessionID string `json:"sessionId,omitempty"`
|
| 23 |
+
AgentID string `json:"agentId,omitempty"`
|
| 24 |
+
Context *QueryContext `json:"context,omitempty"`
|
| 25 |
+
Streaming bool `json:"streaming,omitempty"`
|
| 26 |
+
Options *QueryOptions `json:"options,omitempty"`
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// QueryContext specifies context retrieval options
|
| 30 |
+
type QueryContext struct {
|
| 31 |
+
MaxTurns int `json:"maxTurns,omitempty"`
|
| 32 |
+
MemoryTypes []string `json:"memoryTypes,omitempty"`
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// QueryOptions specifies query processing options
|
| 36 |
+
type QueryOptions struct {
|
| 37 |
+
Temperature float64 `json:"temperature,omitempty"`
|
| 38 |
+
MaxTokens int `json:"maxTokens,omitempty"`
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// QueryResponse contains the query result
|
| 42 |
+
type QueryResponse struct {
|
| 43 |
+
QueryID string `json:"queryId"`
|
| 44 |
+
Answer string `json:"answer"`
|
| 45 |
+
Sources []Source `json:"sources,omitempty"`
|
| 46 |
+
Metadata *ResponseMetadata `json:"metadata,omitempty"`
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Source represents a retrieved document source
|
| 50 |
+
type Source struct {
|
| 51 |
+
DocumentID string `json:"documentId"`
|
| 52 |
+
Score float64 `json:"score"`
|
| 53 |
+
Content string `json:"content"`
|
| 54 |
+
Metadata map[string]string `json:"metadata,omitempty"`
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// ResponseMetadata contains query processing info
|
| 58 |
+
type ResponseMetadata struct {
|
| 59 |
+
LatencyMs int64 `json:"latencyMs"`
|
| 60 |
+
TokensUsed int `json:"tokensUsed"`
|
| 61 |
+
CacheHit bool `json:"cacheHit"`
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// StreamChunk represents a streaming response chunk
|
| 65 |
+
type StreamChunk struct {
|
| 66 |
+
Type string `json:"type"` // chunk, sources, metadata, done, error
|
| 67 |
+
Data string `json:"data,omitempty"`
|
| 68 |
+
Timestamp string `json:"timestamp"`
|
| 69 |
+
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// CreateAgentRequest for creating new agents
|
| 73 |
+
type CreateAgentRequest struct {
|
| 74 |
+
Name string `json:"name"`
|
| 75 |
+
Type string `json:"type"` // rag, agentic, custom
|
| 76 |
+
Config map[string]interface{} `json:"config,omitempty"`
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Agent represents an agent instance
|
| 80 |
+
type Agent struct {
|
| 81 |
+
ID string `json:"id"`
|
| 82 |
+
Name string `json:"name"`
|
| 83 |
+
Status string `json:"status"` // active, idle, error
|
| 84 |
+
CreatedAt string `json:"createdAt"`
|
| 85 |
+
Config map[string]interface{} `json:"config,omitempty"`
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// ExecutionPlan for multi-step execution
|
| 89 |
+
type ExecutionPlan struct {
|
| 90 |
+
ID string `json:"id"`
|
| 91 |
+
Query string `json:"query"`
|
| 92 |
+
Steps []ExecutionStep `json:"steps"`
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// ExecutionStep represents a single execution step
|
| 96 |
+
type ExecutionStep struct {
|
| 97 |
+
ID string `json:"id"`
|
| 98 |
+
Type string `json:"type"`
|
| 99 |
+
Description string `json:"description"`
|
| 100 |
+
Tool string `json:"tool"`
|
| 101 |
+
Input string `json:"input"`
|
| 102 |
+
Dependencies []string `json:"dependencies,omitempty"`
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// PlanResult contains execution results
|
| 106 |
+
type PlanResult struct {
|
| 107 |
+
PlanID string `json:"planId"`
|
| 108 |
+
Status string `json:"status"`
|
| 109 |
+
StepResults []StepResult `json:"stepResults"`
|
| 110 |
+
FinalAnswer string `json:"finalAnswer"`
|
| 111 |
+
ExecutionTimeMs int64 `json:"executionTimeMs"`
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// StepResult contains individual step results
|
| 115 |
+
type StepResult struct {
|
| 116 |
+
StepID string `json:"stepId"`
|
| 117 |
+
Status string `json:"status"`
|
| 118 |
+
Output string `json:"output"`
|
| 119 |
+
Error string `json:"error,omitempty"`
|
| 120 |
+
ExecutionTimeMs int64 `json:"executionTimeMs"`
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// ContextWindow represents memory context
|
| 124 |
+
type ContextWindow struct {
|
| 125 |
+
SessionID string `json:"sessionId"`
|
| 126 |
+
Entries []ContextEntry `json:"entries"`
|
| 127 |
+
TotalTokens int `json:"totalTokens"`
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// ContextEntry represents a single context entry
|
| 131 |
+
type ContextEntry struct {
|
| 132 |
+
ID string `json:"id"`
|
| 133 |
+
Type string `json:"type"`
|
| 134 |
+
Content string `json:"content"`
|
| 135 |
+
Timestamp string `json:"timestamp"`
|
| 136 |
+
Score float64 `json:"score,omitempty"`
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// TokenRequest for OAuth2 token endpoint
|
| 140 |
+
type TokenRequest struct {
|
| 141 |
+
GrantType string `json:"grant_type" form:"grant_type"`
|
| 142 |
+
ClientID string `json:"client_id" form:"client_id"`
|
| 143 |
+
ClientSecret string `json:"client_secret" form:"client_secret"`
|
| 144 |
+
Username string `json:"username,omitempty" form:"username"`
|
| 145 |
+
Password string `json:"password,omitempty" form:"password"`
|
| 146 |
+
RefreshToken string `json:"refresh_token,omitempty" form:"refresh_token"`
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// TokenResponse for OAuth2 token response
|
| 150 |
+
type TokenResponse struct {
|
| 151 |
+
AccessToken string `json:"access_token"`
|
| 152 |
+
TokenType string `json:"token_type"`
|
| 153 |
+
ExpiresIn int `json:"expires_in"`
|
| 154 |
+
RefreshToken string `json:"refresh_token,omitempty"`
|
| 155 |
+
Scope string `json:"scope,omitempty"`
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// HealthResponse for health check
|
| 159 |
+
type HealthResponse struct {
|
| 160 |
+
Status string `json:"status"`
|
| 161 |
+
Timestamp string `json:"timestamp"`
|
| 162 |
+
Services map[string]string `json:"services"`
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// ErrorResponse for API errors
|
| 166 |
+
type ErrorResponse struct {
|
| 167 |
+
Error string `json:"error"`
|
| 168 |
+
Message string `json:"message"`
|
| 169 |
+
Code string `json:"code,omitempty"`
|
| 170 |
+
RequestID string `json:"requestId,omitempty"`
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// WebSocketMessage represents client WebSocket messages
|
| 174 |
+
type WebSocketMessage struct {
|
| 175 |
+
Type string `json:"type"` // query, ping, cancel
|
| 176 |
+
Payload interface{} `json:"payload,omitempty"`
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// WebSocketQueryPayload for WebSocket query messages
|
| 180 |
+
type WebSocketQueryPayload struct {
|
| 181 |
+
QueryID string `json:"queryId,omitempty"`
|
| 182 |
+
Query string `json:"query"`
|
| 183 |
+
UserID string `json:"userId"`
|
| 184 |
+
SessionID string `json:"sessionId,omitempty"`
|
| 185 |
+
AgentID string `json:"agentId,omitempty"`
|
| 186 |
+
Streaming bool `json:"streaming"`
|
| 187 |
+
Options *QueryOptions `json:"options,omitempty"`
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// WebSocketServerMessage represents server WebSocket messages
|
| 191 |
+
type WebSocketServerMessage struct {
|
| 192 |
+
Type string `json:"type"`
|
| 193 |
+
Timestamp string `json:"timestamp"`
|
| 194 |
+
Data interface{} `json:"data,omitempty"`
|
| 195 |
+
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
| 196 |
+
}
|
internal/gateway/handlers/admin.go
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"net/http"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
"go.uber.org/zap"
|
| 9 |
+
|
| 10 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/gwtypes"
|
| 11 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/services"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
// AdminHandler handles admin endpoints
|
| 15 |
+
type AdminHandler struct {
|
| 16 |
+
registry *services.Registry
|
| 17 |
+
logger *zap.Logger
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// NewAdminHandler creates a new admin handler
|
| 21 |
+
func NewAdminHandler(registry *services.Registry, logger *zap.Logger) *AdminHandler {
|
| 22 |
+
return &AdminHandler{
|
| 23 |
+
registry: registry,
|
| 24 |
+
logger: logger,
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// HealthCheck handles GET /admin/health
|
| 29 |
+
func (h *AdminHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
|
| 30 |
+
ctx := r.Context()
|
| 31 |
+
|
| 32 |
+
// Check all backend services
|
| 33 |
+
serviceStatus := make(map[string]string)
|
| 34 |
+
|
| 35 |
+
// Check agent service
|
| 36 |
+
if h.registry != nil {
|
| 37 |
+
if _, err := h.registry.GetAgentClient(ctx); err != nil {
|
| 38 |
+
serviceStatus["agent"] = "unhealthy"
|
| 39 |
+
} else {
|
| 40 |
+
status := h.registry.HealthCheck(ctx, "agent")
|
| 41 |
+
serviceStatus["agent"] = status
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Check retriever service
|
| 45 |
+
if _, err := h.registry.GetRetrieverClient(ctx); err != nil {
|
| 46 |
+
serviceStatus["retriever"] = "unhealthy"
|
| 47 |
+
} else {
|
| 48 |
+
status := h.registry.HealthCheck(ctx, "retriever")
|
| 49 |
+
serviceStatus["retriever"] = status
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Check generator service
|
| 53 |
+
if _, err := h.registry.GetGeneratorClient(ctx); err != nil {
|
| 54 |
+
serviceStatus["generator"] = "unhealthy"
|
| 55 |
+
} else {
|
| 56 |
+
status := h.registry.HealthCheck(ctx, "generator")
|
| 57 |
+
serviceStatus["generator"] = status
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Check memory service
|
| 61 |
+
if _, err := h.registry.GetMemoryClient(ctx); err != nil {
|
| 62 |
+
serviceStatus["memory"] = "unhealthy"
|
| 63 |
+
} else {
|
| 64 |
+
status := h.registry.HealthCheck(ctx, "memory")
|
| 65 |
+
serviceStatus["memory"] = status
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// Determine overall status
|
| 70 |
+
overallStatus := "healthy"
|
| 71 |
+
for _, status := range serviceStatus {
|
| 72 |
+
if status != "healthy" && status != "SERVING" {
|
| 73 |
+
overallStatus = "degraded"
|
| 74 |
+
break
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
response := gwtypes.HealthResponse{
|
| 79 |
+
Status: overallStatus,
|
| 80 |
+
Timestamp: time.Now().Format(time.RFC3339),
|
| 81 |
+
Services: serviceStatus,
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Set status code based on health
|
| 85 |
+
statusCode := http.StatusOK
|
| 86 |
+
if overallStatus != "healthy" {
|
| 87 |
+
statusCode = http.StatusServiceUnavailable
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
w.Header().Set("Content-Type", "application/json")
|
| 91 |
+
w.WriteHeader(statusCode)
|
| 92 |
+
json.NewEncoder(w).Encode(response)
|
| 93 |
+
}
|
internal/gateway/handlers/agent.go
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"net/http"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
"github.com/google/uuid"
|
| 9 |
+
"github.com/gorilla/mux"
|
| 10 |
+
"go.uber.org/zap"
|
| 11 |
+
|
| 12 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/gwtypes"
|
| 13 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/middleware"
|
| 14 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/services"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
// AgentHandler handles agent-related endpoints
|
| 18 |
+
type AgentHandler struct {
|
| 19 |
+
registry *services.Registry
|
| 20 |
+
logger *zap.Logger
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// NewAgentHandler creates a new agent handler
|
| 24 |
+
func NewAgentHandler(registry *services.Registry, logger *zap.Logger) *AgentHandler {
|
| 25 |
+
return &AgentHandler{
|
| 26 |
+
registry: registry,
|
| 27 |
+
logger: logger,
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// CreateAgent handles POST /v2/agents
|
| 32 |
+
func (h *AgentHandler) CreateAgent(w http.ResponseWriter, r *http.Request) {
|
| 33 |
+
ctx := r.Context()
|
| 34 |
+
|
| 35 |
+
// Parse request
|
| 36 |
+
var req gwtypes.CreateAgentRequest
|
| 37 |
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
| 38 |
+
h.errorResponse(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
| 39 |
+
return
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Validate required fields
|
| 43 |
+
if req.Name == "" {
|
| 44 |
+
h.errorResponse(w, http.StatusBadRequest, "Agent name is required", "")
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
if req.Type == "" {
|
| 48 |
+
req.Type = "rag" // Default type
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Get user info from context
|
| 52 |
+
userID := middleware.UserIDFromContext(ctx)
|
| 53 |
+
tenantID := middleware.TenantIDFromContext(ctx)
|
| 54 |
+
|
| 55 |
+
// Get agent client
|
| 56 |
+
agentClient, err := h.registry.GetAgentClient(ctx)
|
| 57 |
+
if err != nil {
|
| 58 |
+
h.logger.Error("Failed to get agent client", zap.Error(err))
|
| 59 |
+
h.errorResponse(w, http.StatusServiceUnavailable, "Service unavailable", "")
|
| 60 |
+
return
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Generate agent ID
|
| 64 |
+
agentID := uuid.New().String()
|
| 65 |
+
|
| 66 |
+
// Create agent
|
| 67 |
+
agent, err := agentClient.CreateAgent(ctx, &services.CreateAgentRequest{
|
| 68 |
+
ID: agentID,
|
| 69 |
+
Name: req.Name,
|
| 70 |
+
Type: req.Type,
|
| 71 |
+
Config: req.Config,
|
| 72 |
+
UserID: userID,
|
| 73 |
+
TenantID: tenantID,
|
| 74 |
+
})
|
| 75 |
+
if err != nil {
|
| 76 |
+
h.logger.Error("Failed to create agent", zap.Error(err))
|
| 77 |
+
h.errorResponse(w, http.StatusInternalServerError, "Failed to create agent", err.Error())
|
| 78 |
+
return
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Build response
|
| 82 |
+
response := gwtypes.Agent{
|
| 83 |
+
ID: agent.ID,
|
| 84 |
+
Name: agent.Name,
|
| 85 |
+
Status: agent.Status,
|
| 86 |
+
CreatedAt: time.Now().Format(time.RFC3339),
|
| 87 |
+
Config: agent.Config,
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
h.jsonResponse(w, http.StatusCreated, response)
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// GetAgent handles GET /v2/agents/{agentId}
|
| 94 |
+
func (h *AgentHandler) GetAgent(w http.ResponseWriter, r *http.Request) {
|
| 95 |
+
ctx := r.Context()
|
| 96 |
+
vars := mux.Vars(r)
|
| 97 |
+
agentID := vars["agentId"]
|
| 98 |
+
|
| 99 |
+
if agentID == "" {
|
| 100 |
+
h.errorResponse(w, http.StatusBadRequest, "Agent ID is required", "")
|
| 101 |
+
return
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// Get agent client
|
| 105 |
+
agentClient, err := h.registry.GetAgentClient(ctx)
|
| 106 |
+
if err != nil {
|
| 107 |
+
h.logger.Error("Failed to get agent client", zap.Error(err))
|
| 108 |
+
h.errorResponse(w, http.StatusServiceUnavailable, "Service unavailable", "")
|
| 109 |
+
return
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Get agent
|
| 113 |
+
agent, err := agentClient.GetAgent(ctx, agentID)
|
| 114 |
+
if err != nil {
|
| 115 |
+
h.logger.Error("Failed to get agent", zap.Error(err), zap.String("agent_id", agentID))
|
| 116 |
+
h.errorResponse(w, http.StatusNotFound, "Agent not found", "")
|
| 117 |
+
return
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// Build response
|
| 121 |
+
response := gwtypes.Agent{
|
| 122 |
+
ID: agent.ID,
|
| 123 |
+
Name: agent.Name,
|
| 124 |
+
Status: agent.Status,
|
| 125 |
+
CreatedAt: agent.CreatedAt,
|
| 126 |
+
Config: agent.Config,
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
h.jsonResponse(w, http.StatusOK, response)
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// DeleteAgent handles DELETE /v2/agents/{agentId}
|
| 133 |
+
func (h *AgentHandler) DeleteAgent(w http.ResponseWriter, r *http.Request) {
|
| 134 |
+
ctx := r.Context()
|
| 135 |
+
vars := mux.Vars(r)
|
| 136 |
+
agentID := vars["agentId"]
|
| 137 |
+
|
| 138 |
+
if agentID == "" {
|
| 139 |
+
h.errorResponse(w, http.StatusBadRequest, "Agent ID is required", "")
|
| 140 |
+
return
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// Get agent client
|
| 144 |
+
agentClient, err := h.registry.GetAgentClient(ctx)
|
| 145 |
+
if err != nil {
|
| 146 |
+
h.logger.Error("Failed to get agent client", zap.Error(err))
|
| 147 |
+
h.errorResponse(w, http.StatusServiceUnavailable, "Service unavailable", "")
|
| 148 |
+
return
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// Delete agent
|
| 152 |
+
err = agentClient.DeleteAgent(ctx, agentID)
|
| 153 |
+
if err != nil {
|
| 154 |
+
h.logger.Error("Failed to delete agent", zap.Error(err), zap.String("agent_id", agentID))
|
| 155 |
+
h.errorResponse(w, http.StatusInternalServerError, "Failed to delete agent", err.Error())
|
| 156 |
+
return
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
w.WriteHeader(http.StatusNoContent)
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// ExecutePlan handles POST /v2/agents/{agentId}/execute
|
| 163 |
+
func (h *AgentHandler) ExecutePlan(w http.ResponseWriter, r *http.Request) {
|
| 164 |
+
ctx := r.Context()
|
| 165 |
+
vars := mux.Vars(r)
|
| 166 |
+
agentID := vars["agentId"]
|
| 167 |
+
|
| 168 |
+
if agentID == "" {
|
| 169 |
+
h.errorResponse(w, http.StatusBadRequest, "Agent ID is required", "")
|
| 170 |
+
return
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Parse request
|
| 174 |
+
var plan gwtypes.ExecutionPlan
|
| 175 |
+
if err := json.NewDecoder(r.Body).Decode(&plan); err != nil {
|
| 176 |
+
h.errorResponse(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
| 177 |
+
return
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// Get user info from context
|
| 181 |
+
userID := middleware.UserIDFromContext(ctx)
|
| 182 |
+
|
| 183 |
+
// Get agent client
|
| 184 |
+
agentClient, err := h.registry.GetAgentClient(ctx)
|
| 185 |
+
if err != nil {
|
| 186 |
+
h.logger.Error("Failed to get agent client", zap.Error(err))
|
| 187 |
+
h.errorResponse(w, http.StatusServiceUnavailable, "Service unavailable", "")
|
| 188 |
+
return
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// Generate plan ID if not provided
|
| 192 |
+
if plan.ID == "" {
|
| 193 |
+
plan.ID = uuid.New().String()
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
// Execute plan
|
| 197 |
+
start := time.Now()
|
| 198 |
+
result, err := agentClient.ExecutePlan(ctx, &services.ExecutePlanRequest{
|
| 199 |
+
AgentID: agentID,
|
| 200 |
+
PlanID: plan.ID,
|
| 201 |
+
Query: plan.Query,
|
| 202 |
+
Steps: h.convertSteps(plan.Steps),
|
| 203 |
+
UserID: userID,
|
| 204 |
+
})
|
| 205 |
+
if err != nil {
|
| 206 |
+
h.logger.Error("Failed to execute plan", zap.Error(err), zap.String("plan_id", plan.ID))
|
| 207 |
+
h.errorResponse(w, http.StatusInternalServerError, "Plan execution failed", err.Error())
|
| 208 |
+
return
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Build response
|
| 212 |
+
response := gwtypes.PlanResult{
|
| 213 |
+
PlanID: plan.ID,
|
| 214 |
+
Status: result.Status,
|
| 215 |
+
StepResults: h.convertStepResults(result.StepResults),
|
| 216 |
+
FinalAnswer: result.FinalAnswer,
|
| 217 |
+
ExecutionTimeMs: time.Since(start).Milliseconds(),
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
h.jsonResponse(w, http.StatusOK, response)
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
func (h *AgentHandler) convertSteps(steps []gwtypes.ExecutionStep) []*services.ExecutionStep {
|
| 224 |
+
result := make([]*services.ExecutionStep, len(steps))
|
| 225 |
+
for i, step := range steps {
|
| 226 |
+
result[i] = &services.ExecutionStep{
|
| 227 |
+
ID: step.ID,
|
| 228 |
+
Type: step.Type,
|
| 229 |
+
Description: step.Description,
|
| 230 |
+
Tool: step.Tool,
|
| 231 |
+
Input: step.Input,
|
| 232 |
+
Dependencies: step.Dependencies,
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
return result
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
func (h *AgentHandler) convertStepResults(results []*services.StepResult) []gwtypes.StepResult {
|
| 239 |
+
converted := make([]gwtypes.StepResult, len(results))
|
| 240 |
+
for i, r := range results {
|
| 241 |
+
converted[i] = gwtypes.StepResult{
|
| 242 |
+
StepID: r.StepID,
|
| 243 |
+
Status: r.Status,
|
| 244 |
+
Output: r.Output,
|
| 245 |
+
Error: r.Error,
|
| 246 |
+
ExecutionTimeMs: r.ExecutionTimeMs,
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
return converted
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
func (h *AgentHandler) jsonResponse(w http.ResponseWriter, status int, data interface{}) {
|
| 253 |
+
w.Header().Set("Content-Type", "application/json")
|
| 254 |
+
w.WriteHeader(status)
|
| 255 |
+
json.NewEncoder(w).Encode(data)
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
func (h *AgentHandler) errorResponse(w http.ResponseWriter, status int, message, detail string) {
|
| 259 |
+
w.Header().Set("Content-Type", "application/json")
|
| 260 |
+
w.WriteHeader(status)
|
| 261 |
+
resp := gwtypes.ErrorResponse{
|
| 262 |
+
Error: http.StatusText(status),
|
| 263 |
+
Message: message,
|
| 264 |
+
RequestID: w.Header().Get("X-Request-ID"),
|
| 265 |
+
}
|
| 266 |
+
if detail != "" {
|
| 267 |
+
resp.Code = detail
|
| 268 |
+
}
|
| 269 |
+
json.NewEncoder(w).Encode(resp)
|
| 270 |
+
}
|
internal/gateway/handlers/auth.go
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"net/http"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
"github.com/golang-jwt/jwt/v5"
|
| 9 |
+
"go.uber.org/zap"
|
| 10 |
+
|
| 11 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/gwtypes"
|
| 12 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/middleware"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
// AuthHandler handles authentication endpoints
|
| 16 |
+
type AuthHandler struct {
|
| 17 |
+
config middleware.AuthConfig
|
| 18 |
+
logger *zap.Logger
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// NewAuthHandler creates a new auth handler
|
| 22 |
+
func NewAuthHandler(cfg middleware.AuthConfig, logger *zap.Logger) *AuthHandler {
|
| 23 |
+
return &AuthHandler{
|
| 24 |
+
config: middleware.AuthConfig{
|
| 25 |
+
JWTSecret: cfg.JWTSecret,
|
| 26 |
+
JWTIssuer: cfg.JWTIssuer,
|
| 27 |
+
JWTAudience: cfg.JWTAudience,
|
| 28 |
+
},
|
| 29 |
+
logger: logger,
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Token handles POST /v2/auth/token
|
| 34 |
+
func (h *AuthHandler) Token(w http.ResponseWriter, r *http.Request) {
|
| 35 |
+
if err := r.ParseForm(); err != nil {
|
| 36 |
+
h.errorResponse(w, http.StatusBadRequest, "Invalid request")
|
| 37 |
+
return
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
grantType := r.FormValue("grant_type")
|
| 41 |
+
clientID := r.FormValue("client_id")
|
| 42 |
+
clientSecret := r.FormValue("client_secret")
|
| 43 |
+
|
| 44 |
+
if !h.validateClient(clientID, clientSecret) {
|
| 45 |
+
h.errorResponse(w, http.StatusUnauthorized, "Invalid client credentials")
|
| 46 |
+
return
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
switch grantType {
|
| 50 |
+
case "client_credentials":
|
| 51 |
+
token, expiresIn, err := h.generateToken(clientID, []string{"service"})
|
| 52 |
+
if err != nil {
|
| 53 |
+
h.errorResponse(w, http.StatusInternalServerError, "Token generation failed")
|
| 54 |
+
return
|
| 55 |
+
}
|
| 56 |
+
h.tokenResponse(w, token, "", expiresIn)
|
| 57 |
+
|
| 58 |
+
case "password":
|
| 59 |
+
username := r.FormValue("username")
|
| 60 |
+
password := r.FormValue("password")
|
| 61 |
+
if !h.validateUser(username, password) {
|
| 62 |
+
h.errorResponse(w, http.StatusUnauthorized, "Invalid credentials")
|
| 63 |
+
return
|
| 64 |
+
}
|
| 65 |
+
token, expiresIn, _ := h.generateToken(username, []string{"user"})
|
| 66 |
+
refresh, _, _ := h.generateToken(username, []string{"refresh"})
|
| 67 |
+
h.tokenResponse(w, token, refresh, expiresIn)
|
| 68 |
+
|
| 69 |
+
default:
|
| 70 |
+
h.errorResponse(w, http.StatusBadRequest, "Unsupported grant type")
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
func (h *AuthHandler) validateClient(id, secret string) bool {
|
| 75 |
+
return id != "" && secret != ""
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
func (h *AuthHandler) validateUser(username, password string) bool {
|
| 79 |
+
return username != "" && password != ""
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
func (h *AuthHandler) generateToken(subject string, roles []string) (string, int, error) {
|
| 83 |
+
now := time.Now()
|
| 84 |
+
expiresIn := 3600
|
| 85 |
+
claims := &middleware.JWTClaims{
|
| 86 |
+
RegisteredClaims: jwt.RegisteredClaims{
|
| 87 |
+
Issuer: h.config.JWTIssuer,
|
| 88 |
+
Subject: subject,
|
| 89 |
+
IssuedAt: jwt.NewNumericDate(now),
|
| 90 |
+
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(expiresIn) * time.Second)),
|
| 91 |
+
},
|
| 92 |
+
UserID: subject,
|
| 93 |
+
Roles: roles,
|
| 94 |
+
}
|
| 95 |
+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
| 96 |
+
tokenString, err := token.SignedString([]byte(h.config.JWTSecret))
|
| 97 |
+
return tokenString, expiresIn, err
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
func (h *AuthHandler) tokenResponse(w http.ResponseWriter, access, refresh string, expiresIn int) {
|
| 101 |
+
w.Header().Set("Content-Type", "application/json")
|
| 102 |
+
w.Header().Set("Cache-Control", "no-store")
|
| 103 |
+
json.NewEncoder(w).Encode(gwtypes.TokenResponse{
|
| 104 |
+
AccessToken: access,
|
| 105 |
+
TokenType: "Bearer",
|
| 106 |
+
ExpiresIn: expiresIn,
|
| 107 |
+
RefreshToken: refresh,
|
| 108 |
+
})
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
func (h *AuthHandler) errorResponse(w http.ResponseWriter, status int, message string) {
|
| 112 |
+
w.Header().Set("Content-Type", "application/json")
|
| 113 |
+
w.WriteHeader(status)
|
| 114 |
+
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
| 115 |
+
}
|
internal/gateway/handlers/graphql.go
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"net/http"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/google/uuid"
|
| 10 |
+
graphql "github.com/graph-gophers/graphql-go"
|
| 11 |
+
"go.uber.org/zap"
|
| 12 |
+
|
| 13 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/middleware"
|
| 14 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/services"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
// GraphQL Schema Definition
|
| 18 |
+
const schemaString = `
|
| 19 |
+
schema {
|
| 20 |
+
query: Query
|
| 21 |
+
mutation: Mutation
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
type Query {
|
| 25 |
+
# Execute a RAG query and get the response
|
| 26 |
+
query(input: QueryInput!): QueryResponse!
|
| 27 |
+
|
| 28 |
+
# Get a specific query result by ID
|
| 29 |
+
queryResult(queryId: ID!): QueryResponse
|
| 30 |
+
|
| 31 |
+
# Get an agent by ID
|
| 32 |
+
agent(id: ID!): Agent
|
| 33 |
+
|
| 34 |
+
# List all agents for the current user
|
| 35 |
+
agents: [Agent!]!
|
| 36 |
+
|
| 37 |
+
# Get memory context for a session
|
| 38 |
+
contextWindow(sessionId: ID!, maxTurns: Int): ContextWindow!
|
| 39 |
+
|
| 40 |
+
# Health check
|
| 41 |
+
health: HealthStatus!
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
type Mutation {
|
| 45 |
+
# Execute a RAG query
|
| 46 |
+
executeQuery(input: QueryInput!): QueryResponse!
|
| 47 |
+
|
| 48 |
+
# Create a new agent
|
| 49 |
+
createAgent(input: CreateAgentInput!): Agent!
|
| 50 |
+
|
| 51 |
+
# Delete an agent
|
| 52 |
+
deleteAgent(id: ID!): Boolean!
|
| 53 |
+
|
| 54 |
+
# Execute an agent plan
|
| 55 |
+
executePlan(agentId: ID!, input: ExecutionPlanInput!): PlanResult!
|
| 56 |
+
|
| 57 |
+
# Consolidate memory for a session
|
| 58 |
+
consolidateMemory(sessionId: ID!): Boolean!
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
input QueryInput {
|
| 62 |
+
query: String!
|
| 63 |
+
sessionId: String
|
| 64 |
+
agentId: String
|
| 65 |
+
temperature: Float
|
| 66 |
+
maxTokens: Int
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
input CreateAgentInput {
|
| 70 |
+
name: String!
|
| 71 |
+
type: AgentType!
|
| 72 |
+
systemPrompt: String
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
input ExecutionPlanInput {
|
| 76 |
+
query: String!
|
| 77 |
+
steps: [ExecutionStepInput!]
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
input ExecutionStepInput {
|
| 81 |
+
type: String!
|
| 82 |
+
description: String!
|
| 83 |
+
tool: String
|
| 84 |
+
input: String
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
type QueryResponse {
|
| 88 |
+
queryId: ID!
|
| 89 |
+
answer: String!
|
| 90 |
+
sources: [Source!]!
|
| 91 |
+
metadata: QueryMetadata!
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
type Source {
|
| 95 |
+
documentId: ID!
|
| 96 |
+
score: Float!
|
| 97 |
+
content: String!
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
type QueryMetadata {
|
| 101 |
+
latencyMs: Int!
|
| 102 |
+
tokensUsed: Int!
|
| 103 |
+
cacheHit: Boolean!
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
type Agent {
|
| 107 |
+
id: ID!
|
| 108 |
+
name: String!
|
| 109 |
+
type: AgentType!
|
| 110 |
+
status: AgentStatus!
|
| 111 |
+
createdAt: String!
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
enum AgentType {
|
| 115 |
+
RAG
|
| 116 |
+
AGENTIC
|
| 117 |
+
CUSTOM
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
enum AgentStatus {
|
| 121 |
+
ACTIVE
|
| 122 |
+
IDLE
|
| 123 |
+
ERROR
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
type PlanResult {
|
| 127 |
+
planId: ID!
|
| 128 |
+
status: String!
|
| 129 |
+
finalAnswer: String!
|
| 130 |
+
executionTimeMs: Int!
|
| 131 |
+
stepResults: [StepResult!]!
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
type StepResult {
|
| 135 |
+
stepId: ID!
|
| 136 |
+
status: String!
|
| 137 |
+
output: String!
|
| 138 |
+
error: String
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
type ContextWindow {
|
| 142 |
+
sessionId: ID!
|
| 143 |
+
entries: [ContextEntry!]!
|
| 144 |
+
totalTokens: Int!
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
type ContextEntry {
|
| 148 |
+
id: ID!
|
| 149 |
+
type: String!
|
| 150 |
+
content: String!
|
| 151 |
+
timestamp: String!
|
| 152 |
+
score: Float
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
type HealthStatus {
|
| 156 |
+
status: String!
|
| 157 |
+
timestamp: String!
|
| 158 |
+
services: [ServiceHealth!]!
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
type ServiceHealth {
|
| 162 |
+
name: String!
|
| 163 |
+
status: String!
|
| 164 |
+
}
|
| 165 |
+
`
|
| 166 |
+
|
| 167 |
+
// GraphQLHandler handles GraphQL requests
|
| 168 |
+
type GraphQLHandler struct {
|
| 169 |
+
schema *graphql.Schema
|
| 170 |
+
registry *services.Registry
|
| 171 |
+
logger *zap.Logger
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// NewGraphQLHandler creates a new GraphQL handler
|
| 175 |
+
func NewGraphQLHandler(registry *services.Registry, logger *zap.Logger) *GraphQLHandler {
|
| 176 |
+
resolver := &Resolver{
|
| 177 |
+
registry: registry,
|
| 178 |
+
logger: logger,
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
schema := graphql.MustParseSchema(schemaString, resolver,
|
| 182 |
+
graphql.UseFieldResolvers(),
|
| 183 |
+
graphql.MaxParallelism(20),
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
return &GraphQLHandler{
|
| 187 |
+
schema: schema,
|
| 188 |
+
registry: registry,
|
| 189 |
+
logger: logger,
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
// ServeHTTP implements http.Handler for GraphQL
|
| 194 |
+
func (h *GraphQLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
| 195 |
+
var params struct {
|
| 196 |
+
Query string `json:"query"`
|
| 197 |
+
OperationName string `json:"operationName"`
|
| 198 |
+
Variables map[string]interface{} `json:"variables"`
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
| 202 |
+
h.errorResponse(w, "Invalid request body", http.StatusBadRequest)
|
| 203 |
+
return
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
ctx := r.Context()
|
| 207 |
+
response := h.schema.Exec(ctx, params.Query, params.OperationName, params.Variables)
|
| 208 |
+
|
| 209 |
+
w.Header().Set("Content-Type", "application/json")
|
| 210 |
+
if err := json.NewEncoder(w).Encode(response); err != nil {
|
| 211 |
+
h.logger.Error("Failed to encode GraphQL response", zap.Error(err))
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
func (h *GraphQLHandler) errorResponse(w http.ResponseWriter, message string, status int) {
|
| 216 |
+
w.Header().Set("Content-Type", "application/json")
|
| 217 |
+
w.WriteHeader(status)
|
| 218 |
+
json.NewEncoder(w).Encode(map[string]interface{}{
|
| 219 |
+
"errors": []map[string]string{{"message": message}},
|
| 220 |
+
})
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// Resolver implements GraphQL resolvers
|
| 224 |
+
type Resolver struct {
|
| 225 |
+
registry *services.Registry
|
| 226 |
+
logger *zap.Logger
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
// Query resolvers
|
| 230 |
+
|
| 231 |
+
func (r *Resolver) Query(ctx context.Context, args struct{ Input QueryInputArgs }) (*QueryResponseResolver, error) {
|
| 232 |
+
return r.ExecuteQuery(ctx, args)
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
func (r *Resolver) QueryResult(ctx context.Context, args struct{ QueryId graphql.ID }) (*QueryResponseResolver, error) {
|
| 236 |
+
agentClient, err := r.registry.GetAgentClient(ctx)
|
| 237 |
+
if err != nil {
|
| 238 |
+
return nil, err
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
status, err := agentClient.GetQueryStatus(ctx, string(args.QueryId))
|
| 242 |
+
if err != nil {
|
| 243 |
+
return nil, err
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
return &QueryResponseResolver{
|
| 247 |
+
queryId: string(args.QueryId),
|
| 248 |
+
answer: status.Status,
|
| 249 |
+
}, nil
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
func (r *Resolver) Agent(ctx context.Context, args struct{ Id graphql.ID }) (*AgentResolver, error) {
|
| 253 |
+
agentClient, err := r.registry.GetAgentClient(ctx)
|
| 254 |
+
if err != nil {
|
| 255 |
+
return nil, err
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
agent, err := agentClient.GetAgent(ctx, string(args.Id))
|
| 259 |
+
if err != nil {
|
| 260 |
+
return nil, err
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
return &AgentResolver{agent: agent}, nil
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
func (r *Resolver) Agents(ctx context.Context) ([]*AgentResolver, error) {
|
| 267 |
+
// Return empty list - would query user's agents
|
| 268 |
+
return []*AgentResolver{}, nil
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
func (r *Resolver) ContextWindow(ctx context.Context, args struct {
|
| 272 |
+
SessionId graphql.ID
|
| 273 |
+
MaxTurns *int32
|
| 274 |
+
}) (*ContextWindowResolver, error) {
|
| 275 |
+
memoryClient, err := r.registry.GetMemoryClient(ctx)
|
| 276 |
+
if err != nil {
|
| 277 |
+
return nil, err
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
maxTurns := 50
|
| 281 |
+
if args.MaxTurns != nil {
|
| 282 |
+
maxTurns = int(*args.MaxTurns)
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
userID := middleware.UserIDFromContext(ctx)
|
| 286 |
+
window, err := memoryClient.GetContextWindow(ctx, &services.ContextWindowRequest{
|
| 287 |
+
SessionID: string(args.SessionId),
|
| 288 |
+
UserID: userID,
|
| 289 |
+
MaxTurns: maxTurns,
|
| 290 |
+
})
|
| 291 |
+
if err != nil {
|
| 292 |
+
return nil, err
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
return &ContextWindowResolver{
|
| 296 |
+
sessionId: string(args.SessionId),
|
| 297 |
+
window: window,
|
| 298 |
+
}, nil
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
func (r *Resolver) Health(ctx context.Context) (*HealthStatusResolver, error) {
|
| 302 |
+
serviceStatuses := []ServiceHealthResolver{}
|
| 303 |
+
|
| 304 |
+
for _, svc := range []string{"agent", "retriever", "generator", "memory"} {
|
| 305 |
+
status := r.registry.HealthCheck(ctx, svc)
|
| 306 |
+
serviceStatuses = append(serviceStatuses, ServiceHealthResolver{
|
| 307 |
+
name: svc,
|
| 308 |
+
status: status,
|
| 309 |
+
})
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
return &HealthStatusResolver{
|
| 313 |
+
status: "healthy",
|
| 314 |
+
timestamp: time.Now().Format(time.RFC3339),
|
| 315 |
+
services: serviceStatuses,
|
| 316 |
+
}, nil
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
// Mutation resolvers
|
| 320 |
+
|
| 321 |
+
func (r *Resolver) ExecuteQuery(ctx context.Context, args struct{ Input QueryInputArgs }) (*QueryResponseResolver, error) {
|
| 322 |
+
agentClient, err := r.registry.GetAgentClient(ctx)
|
| 323 |
+
if err != nil {
|
| 324 |
+
return nil, err
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
userID := middleware.UserIDFromContext(ctx)
|
| 328 |
+
queryID := uuid.New().String()
|
| 329 |
+
|
| 330 |
+
req := &services.QueryRequest{
|
| 331 |
+
QueryID: queryID,
|
| 332 |
+
Query: args.Input.Query,
|
| 333 |
+
UserID: userID,
|
| 334 |
+
SessionID: stringVal(args.Input.SessionId),
|
| 335 |
+
AgentID: stringVal(args.Input.AgentId),
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
if args.Input.Temperature != nil {
|
| 339 |
+
req.Temperature = *args.Input.Temperature
|
| 340 |
+
}
|
| 341 |
+
if args.Input.MaxTokens != nil {
|
| 342 |
+
req.MaxTokens = int(*args.Input.MaxTokens)
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
resp, err := agentClient.ProcessQuery(ctx, req)
|
| 346 |
+
if err != nil {
|
| 347 |
+
return nil, err
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
sources := make([]SourceResolver, len(resp.Sources))
|
| 351 |
+
for i, src := range resp.Sources {
|
| 352 |
+
sources[i] = SourceResolver{
|
| 353 |
+
documentId: src.ID,
|
| 354 |
+
score: src.Score,
|
| 355 |
+
content: src.Content,
|
| 356 |
+
}
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
return &QueryResponseResolver{
|
| 360 |
+
queryId: queryID,
|
| 361 |
+
answer: resp.Answer,
|
| 362 |
+
sources: sources,
|
| 363 |
+
tokensUsed: resp.TokensUsed,
|
| 364 |
+
}, nil
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
func (r *Resolver) CreateAgent(ctx context.Context, args struct{ Input CreateAgentInputArgs }) (*AgentResolver, error) {
|
| 368 |
+
agentClient, err := r.registry.GetAgentClient(ctx)
|
| 369 |
+
if err != nil {
|
| 370 |
+
return nil, err
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
userID := middleware.UserIDFromContext(ctx)
|
| 374 |
+
tenantID := middleware.TenantIDFromContext(ctx)
|
| 375 |
+
|
| 376 |
+
agent, err := agentClient.CreateAgent(ctx, &services.CreateAgentRequest{
|
| 377 |
+
ID: uuid.New().String(),
|
| 378 |
+
Name: args.Input.Name,
|
| 379 |
+
Type: string(args.Input.Type),
|
| 380 |
+
UserID: userID,
|
| 381 |
+
TenantID: tenantID,
|
| 382 |
+
})
|
| 383 |
+
if err != nil {
|
| 384 |
+
return nil, err
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
return &AgentResolver{agent: agent}, nil
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
func (r *Resolver) DeleteAgent(ctx context.Context, args struct{ Id graphql.ID }) (bool, error) {
|
| 391 |
+
agentClient, err := r.registry.GetAgentClient(ctx)
|
| 392 |
+
if err != nil {
|
| 393 |
+
return false, err
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
err = agentClient.DeleteAgent(ctx, string(args.Id))
|
| 397 |
+
return err == nil, err
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
func (r *Resolver) ExecutePlan(ctx context.Context, args struct {
|
| 401 |
+
AgentId graphql.ID
|
| 402 |
+
Input ExecutionPlanInputArgs
|
| 403 |
+
}) (*PlanResultResolver, error) {
|
| 404 |
+
agentClient, err := r.registry.GetAgentClient(ctx)
|
| 405 |
+
if err != nil {
|
| 406 |
+
return nil, err
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
userID := middleware.UserIDFromContext(ctx)
|
| 410 |
+
planID := uuid.New().String()
|
| 411 |
+
|
| 412 |
+
steps := make([]*services.ExecutionStep, len(args.Input.Steps))
|
| 413 |
+
for i, s := range args.Input.Steps {
|
| 414 |
+
steps[i] = &services.ExecutionStep{
|
| 415 |
+
ID: uuid.New().String(),
|
| 416 |
+
Type: s.Type,
|
| 417 |
+
Description: s.Description,
|
| 418 |
+
Tool: stringVal(s.Tool),
|
| 419 |
+
Input: stringVal(s.Input),
|
| 420 |
+
}
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
result, err := agentClient.ExecutePlan(ctx, &services.ExecutePlanRequest{
|
| 424 |
+
AgentID: string(args.AgentId),
|
| 425 |
+
PlanID: planID,
|
| 426 |
+
Query: args.Input.Query,
|
| 427 |
+
Steps: steps,
|
| 428 |
+
UserID: userID,
|
| 429 |
+
})
|
| 430 |
+
if err != nil {
|
| 431 |
+
return nil, err
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
stepResults := make([]StepResultResolver, len(result.StepResults))
|
| 435 |
+
for i, sr := range result.StepResults {
|
| 436 |
+
stepResults[i] = StepResultResolver{
|
| 437 |
+
stepId: sr.StepID,
|
| 438 |
+
status: sr.Status,
|
| 439 |
+
output: sr.Output,
|
| 440 |
+
err: sr.Error,
|
| 441 |
+
}
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
return &PlanResultResolver{
|
| 445 |
+
planId: planID,
|
| 446 |
+
status: result.Status,
|
| 447 |
+
finalAnswer: result.FinalAnswer,
|
| 448 |
+
stepResults: stepResults,
|
| 449 |
+
}, nil
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
func (r *Resolver) ConsolidateMemory(ctx context.Context, args struct{ SessionId graphql.ID }) (bool, error) {
|
| 453 |
+
memoryClient, err := r.registry.GetMemoryClient(ctx)
|
| 454 |
+
if err != nil {
|
| 455 |
+
return false, err
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
userID := middleware.UserIDFromContext(ctx)
|
| 459 |
+
err = memoryClient.ConsolidateMemory(ctx, &services.ConsolidateRequest{
|
| 460 |
+
SessionID: string(args.SessionId),
|
| 461 |
+
UserID: userID,
|
| 462 |
+
})
|
| 463 |
+
return err == nil, err
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
// Input args types
|
| 467 |
+
|
| 468 |
+
type QueryInputArgs struct {
|
| 469 |
+
Query string
|
| 470 |
+
SessionId *string
|
| 471 |
+
AgentId *string
|
| 472 |
+
Temperature *float64
|
| 473 |
+
MaxTokens *int32
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
type CreateAgentInputArgs struct {
|
| 477 |
+
Name string
|
| 478 |
+
Type string
|
| 479 |
+
SystemPrompt *string
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
type ExecutionPlanInputArgs struct {
|
| 483 |
+
Query string
|
| 484 |
+
Steps []ExecutionStepInputArgs
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
type ExecutionStepInputArgs struct {
|
| 488 |
+
Type string
|
| 489 |
+
Description string
|
| 490 |
+
Tool *string
|
| 491 |
+
Input *string
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
// Resolver types
|
| 495 |
+
|
| 496 |
+
type QueryResponseResolver struct {
|
| 497 |
+
queryId string
|
| 498 |
+
answer string
|
| 499 |
+
sources []SourceResolver
|
| 500 |
+
tokensUsed int
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
func (r *QueryResponseResolver) QueryId() graphql.ID { return graphql.ID(r.queryId) }
|
| 504 |
+
func (r *QueryResponseResolver) Answer() string { return r.answer }
|
| 505 |
+
func (r *QueryResponseResolver) Sources() []SourceResolver {
|
| 506 |
+
return r.sources
|
| 507 |
+
}
|
| 508 |
+
func (r *QueryResponseResolver) Metadata() *QueryMetadataResolver {
|
| 509 |
+
return &QueryMetadataResolver{latencyMs: 100, tokensUsed: r.tokensUsed}
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
type SourceResolver struct {
|
| 513 |
+
documentId string
|
| 514 |
+
score float64
|
| 515 |
+
content string
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
func (r SourceResolver) DocumentId() graphql.ID { return graphql.ID(r.documentId) }
|
| 519 |
+
func (r SourceResolver) Score() float64 { return r.score }
|
| 520 |
+
func (r SourceResolver) Content() string { return r.content }
|
| 521 |
+
|
| 522 |
+
type QueryMetadataResolver struct {
|
| 523 |
+
latencyMs int
|
| 524 |
+
tokensUsed int
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
func (r *QueryMetadataResolver) LatencyMs() int32 { return int32(r.latencyMs) }
|
| 528 |
+
func (r *QueryMetadataResolver) TokensUsed() int32 { return int32(r.tokensUsed) }
|
| 529 |
+
func (r *QueryMetadataResolver) CacheHit() bool { return false }
|
| 530 |
+
|
| 531 |
+
type AgentResolver struct {
|
| 532 |
+
agent *services.AgentInfo
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
func (r *AgentResolver) Id() graphql.ID { return graphql.ID(r.agent.ID) }
|
| 536 |
+
func (r *AgentResolver) Name() string { return r.agent.Name }
|
| 537 |
+
func (r *AgentResolver) Type() string { return "RAG" }
|
| 538 |
+
func (r *AgentResolver) Status() string { return r.agent.Status }
|
| 539 |
+
func (r *AgentResolver) CreatedAt() string { return r.agent.CreatedAt }
|
| 540 |
+
|
| 541 |
+
type PlanResultResolver struct {
|
| 542 |
+
planId string
|
| 543 |
+
status string
|
| 544 |
+
finalAnswer string
|
| 545 |
+
stepResults []StepResultResolver
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
func (r *PlanResultResolver) PlanId() graphql.ID { return graphql.ID(r.planId) }
|
| 549 |
+
func (r *PlanResultResolver) Status() string { return r.status }
|
| 550 |
+
func (r *PlanResultResolver) FinalAnswer() string { return r.finalAnswer }
|
| 551 |
+
func (r *PlanResultResolver) ExecutionTimeMs() int32 { return 1000 }
|
| 552 |
+
func (r *PlanResultResolver) StepResults() []StepResultResolver { return r.stepResults }
|
| 553 |
+
|
| 554 |
+
type StepResultResolver struct {
|
| 555 |
+
stepId string
|
| 556 |
+
status string
|
| 557 |
+
output string
|
| 558 |
+
err string
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
func (r StepResultResolver) StepId() graphql.ID { return graphql.ID(r.stepId) }
|
| 562 |
+
func (r StepResultResolver) Status() string { return r.status }
|
| 563 |
+
func (r StepResultResolver) Output() string { return r.output }
|
| 564 |
+
func (r StepResultResolver) Error() *string {
|
| 565 |
+
if r.err == "" {
|
| 566 |
+
return nil
|
| 567 |
+
}
|
| 568 |
+
return &r.err
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
type ContextWindowResolver struct {
|
| 572 |
+
sessionId string
|
| 573 |
+
window *services.ContextWindowResponse
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
func (r *ContextWindowResolver) SessionId() graphql.ID { return graphql.ID(r.sessionId) }
|
| 577 |
+
func (r *ContextWindowResolver) TotalTokens() int32 { return int32(r.window.TotalTokens) }
|
| 578 |
+
func (r *ContextWindowResolver) Entries() []ContextEntryResolver {
|
| 579 |
+
entries := make([]ContextEntryResolver, len(r.window.Entries))
|
| 580 |
+
for i, e := range r.window.Entries {
|
| 581 |
+
entries[i] = ContextEntryResolver{entry: e}
|
| 582 |
+
}
|
| 583 |
+
return entries
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
type ContextEntryResolver struct {
|
| 587 |
+
entry *services.ContextEntry
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
func (r ContextEntryResolver) Id() graphql.ID { return graphql.ID(r.entry.ID) }
|
| 591 |
+
func (r ContextEntryResolver) Type() string { return r.entry.Type }
|
| 592 |
+
func (r ContextEntryResolver) Content() string { return r.entry.Content }
|
| 593 |
+
func (r ContextEntryResolver) Timestamp() string { return r.entry.Timestamp }
|
| 594 |
+
func (r ContextEntryResolver) Score() *float64 { return &r.entry.Score }
|
| 595 |
+
|
| 596 |
+
type HealthStatusResolver struct {
|
| 597 |
+
status string
|
| 598 |
+
timestamp string
|
| 599 |
+
services []ServiceHealthResolver
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
func (r *HealthStatusResolver) Status() string { return r.status }
|
| 603 |
+
func (r *HealthStatusResolver) Timestamp() string { return r.timestamp }
|
| 604 |
+
func (r *HealthStatusResolver) Services() []ServiceHealthResolver { return r.services }
|
| 605 |
+
|
| 606 |
+
type ServiceHealthResolver struct {
|
| 607 |
+
name string
|
| 608 |
+
status string
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
func (r ServiceHealthResolver) Name() string { return r.name }
|
| 612 |
+
func (r ServiceHealthResolver) Status() string { return r.status }
|
| 613 |
+
|
| 614 |
+
// Helper functions
|
| 615 |
+
func stringVal(s *string) string {
|
| 616 |
+
if s == nil {
|
| 617 |
+
return ""
|
| 618 |
+
}
|
| 619 |
+
return *s
|
| 620 |
+
}
|
internal/gateway/handlers/handlers_test.go
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers_test
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"net/http"
|
| 7 |
+
"net/http/httptest"
|
| 8 |
+
"testing"
|
| 9 |
+
|
| 10 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/gwtypes"
|
| 11 |
+
"go.uber.org/zap"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
// TestQueryRequest_JSONParsing tests JSON parsing of query requests
|
| 15 |
+
func TestQueryRequest_JSONParsing(t *testing.T) {
|
| 16 |
+
jsonData := `{
|
| 17 |
+
"query": "What is negligence in tort law?",
|
| 18 |
+
"userId": "user-123",
|
| 19 |
+
"sessionId": "session-456"
|
| 20 |
+
}`
|
| 21 |
+
|
| 22 |
+
var req gwtypes.QueryRequest
|
| 23 |
+
err := json.Unmarshal([]byte(jsonData), &req)
|
| 24 |
+
if err != nil {
|
| 25 |
+
t.Fatalf("Failed to parse JSON: %v", err)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
if req.Query != "What is negligence in tort law?" {
|
| 29 |
+
t.Errorf("Query mismatch: got '%s'", req.Query)
|
| 30 |
+
}
|
| 31 |
+
if req.UserID != "user-123" {
|
| 32 |
+
t.Errorf("UserID mismatch: got '%s'", req.UserID)
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// TestQueryResponse_JSONSerialization tests JSON serialization of query responses
|
| 37 |
+
func TestQueryResponse_JSONSerialization(t *testing.T) {
|
| 38 |
+
resp := gwtypes.QueryResponse{
|
| 39 |
+
QueryID: "query-123",
|
| 40 |
+
Answer: "Negligence is a failure to exercise reasonable care.",
|
| 41 |
+
Sources: []gwtypes.Source{
|
| 42 |
+
{
|
| 43 |
+
DocumentID: "doc-1",
|
| 44 |
+
Score: 0.95,
|
| 45 |
+
Content: "Source content",
|
| 46 |
+
},
|
| 47 |
+
},
|
| 48 |
+
Metadata: &gwtypes.ResponseMetadata{
|
| 49 |
+
LatencyMs: 150,
|
| 50 |
+
TokensUsed: 200,
|
| 51 |
+
CacheHit: false,
|
| 52 |
+
},
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
data, err := json.Marshal(resp)
|
| 56 |
+
if err != nil {
|
| 57 |
+
t.Fatalf("Failed to serialize JSON: %v", err)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if len(data) == 0 {
|
| 61 |
+
t.Error("Expected non-empty JSON")
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Verify round-trip
|
| 65 |
+
var parsed gwtypes.QueryResponse
|
| 66 |
+
err = json.Unmarshal(data, &parsed)
|
| 67 |
+
if err != nil {
|
| 68 |
+
t.Fatalf("Failed to parse serialized JSON: %v", err)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
if parsed.QueryID != resp.QueryID {
|
| 72 |
+
t.Error("QueryID mismatch after round-trip")
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// TestErrorResponse_Structure tests error response structure
|
| 77 |
+
func TestErrorResponse_Structure(t *testing.T) {
|
| 78 |
+
resp := gwtypes.ErrorResponse{
|
| 79 |
+
Error: "Bad Request",
|
| 80 |
+
Message: "Query is required",
|
| 81 |
+
Code: "VALIDATION_ERROR",
|
| 82 |
+
RequestID: "req-123",
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
data, err := json.Marshal(resp)
|
| 86 |
+
if err != nil {
|
| 87 |
+
t.Fatalf("Failed to serialize: %v", err)
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
if len(data) == 0 {
|
| 91 |
+
t.Error("Expected non-empty JSON")
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// TestSource_Structure tests source structure
|
| 96 |
+
func TestSource_Structure(t *testing.T) {
|
| 97 |
+
source := gwtypes.Source{
|
| 98 |
+
DocumentID: "doc-123",
|
| 99 |
+
Score: 0.87,
|
| 100 |
+
Content: "This is the source content.",
|
| 101 |
+
Metadata: map[string]string{
|
| 102 |
+
"author": "John Doe",
|
| 103 |
+
"date": "2024-01-15",
|
| 104 |
+
},
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
if source.DocumentID != "doc-123" {
|
| 108 |
+
t.Error("DocumentID mismatch")
|
| 109 |
+
}
|
| 110 |
+
if source.Score != 0.87 {
|
| 111 |
+
t.Error("Score mismatch")
|
| 112 |
+
}
|
| 113 |
+
if source.Metadata["author"] != "John Doe" {
|
| 114 |
+
t.Error("Metadata mismatch")
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// TestQueryRequest_EmptyValidation tests that empty query is detected
|
| 119 |
+
func TestQueryRequest_EmptyValidation(t *testing.T) {
|
| 120 |
+
req := gwtypes.QueryRequest{
|
| 121 |
+
Query: "",
|
| 122 |
+
UserID: "user-123",
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
if req.Query != "" {
|
| 126 |
+
t.Error("Query should be empty for this test")
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// This simulates what the handler would check
|
| 130 |
+
isValid := req.Query != ""
|
| 131 |
+
if isValid {
|
| 132 |
+
t.Error("Empty query should be invalid")
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// TestResponseMetadata_Structure tests response metadata
|
| 137 |
+
func TestResponseMetadata_Structure(t *testing.T) {
|
| 138 |
+
meta := gwtypes.ResponseMetadata{
|
| 139 |
+
LatencyMs: 250,
|
| 140 |
+
TokensUsed: 1500,
|
| 141 |
+
CacheHit: true,
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
if meta.LatencyMs != 250 {
|
| 145 |
+
t.Error("LatencyMs mismatch")
|
| 146 |
+
}
|
| 147 |
+
if meta.TokensUsed != 1500 {
|
| 148 |
+
t.Error("TokensUsed mismatch")
|
| 149 |
+
}
|
| 150 |
+
if !meta.CacheHit {
|
| 151 |
+
t.Error("CacheHit should be true")
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// TestHTTPStatusCodes tests status codes are correctly used
|
| 156 |
+
func TestHTTPStatusCodes(t *testing.T) {
|
| 157 |
+
testCases := []struct {
|
| 158 |
+
name string
|
| 159 |
+
status int
|
| 160 |
+
expected string
|
| 161 |
+
}{
|
| 162 |
+
{"OK", http.StatusOK, "OK"},
|
| 163 |
+
{"BadRequest", http.StatusBadRequest, "Bad Request"},
|
| 164 |
+
{"NotFound", http.StatusNotFound, "Not Found"},
|
| 165 |
+
{"ServiceUnavailable", http.StatusServiceUnavailable, "Service Unavailable"},
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
for _, tc := range testCases {
|
| 169 |
+
t.Run(tc.name, func(t *testing.T) {
|
| 170 |
+
text := http.StatusText(tc.status)
|
| 171 |
+
if text != tc.expected {
|
| 172 |
+
t.Errorf("Expected '%s', got '%s'", tc.expected, text)
|
| 173 |
+
}
|
| 174 |
+
})
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// TestJSONContentType tests content type header
|
| 179 |
+
func TestJSONContentType(t *testing.T) {
|
| 180 |
+
rec := httptest.NewRecorder()
|
| 181 |
+
rec.Header().Set("Content-Type", "application/json")
|
| 182 |
+
|
| 183 |
+
if rec.Header().Get("Content-Type") != "application/json" {
|
| 184 |
+
t.Error("Content-Type header not set correctly")
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// TestQueryOptionsDefaults tests query options defaults
|
| 189 |
+
func TestQueryOptionsDefaults(t *testing.T) {
|
| 190 |
+
opts := gwtypes.QueryOptions{
|
| 191 |
+
Temperature: 0.7,
|
| 192 |
+
MaxTokens: 4096,
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
if opts.Temperature != 0.7 {
|
| 196 |
+
t.Errorf("Temperature mismatch: got %f", opts.Temperature)
|
| 197 |
+
}
|
| 198 |
+
if opts.MaxTokens != 4096 {
|
| 199 |
+
t.Errorf("MaxTokens mismatch: got %d", opts.MaxTokens)
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// TestQueryContext tests query context options
|
| 204 |
+
func TestQueryContext(t *testing.T) {
|
| 205 |
+
ctx := gwtypes.QueryContext{
|
| 206 |
+
MaxTurns: 5,
|
| 207 |
+
MemoryTypes: []string{"semantic", "episodic"},
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
if ctx.MaxTurns != 5 {
|
| 211 |
+
t.Error("MaxTurns mismatch")
|
| 212 |
+
}
|
| 213 |
+
if len(ctx.MemoryTypes) != 2 {
|
| 214 |
+
t.Error("MemoryTypes length mismatch")
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// MockHandler for testing HTTP handling patterns
|
| 219 |
+
type MockHandler struct {
|
| 220 |
+
logger *zap.Logger
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
func (h *MockHandler) jsonResponse(w http.ResponseWriter, status int, data interface{}) {
|
| 224 |
+
w.Header().Set("Content-Type", "application/json")
|
| 225 |
+
w.WriteHeader(status)
|
| 226 |
+
json.NewEncoder(w).Encode(data)
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
// TestMockHandler_JSONResponse tests JSON response helper
|
| 230 |
+
func TestMockHandler_JSONResponse(t *testing.T) {
|
| 231 |
+
logger, _ := zap.NewDevelopment()
|
| 232 |
+
h := &MockHandler{logger: logger}
|
| 233 |
+
|
| 234 |
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 235 |
+
rec := httptest.NewRecorder()
|
| 236 |
+
|
| 237 |
+
h.jsonResponse(rec, http.StatusOK, map[string]string{"status": "ok"})
|
| 238 |
+
|
| 239 |
+
if rec.Code != http.StatusOK {
|
| 240 |
+
t.Errorf("Expected status %d, got %d", http.StatusOK, rec.Code)
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
if rec.Header().Get("Content-Type") != "application/json" {
|
| 244 |
+
t.Error("Expected JSON content type")
|
| 245 |
+
}
|
| 246 |
+
_ = req
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
// TestRequestParsing tests request body parsing
|
| 250 |
+
func TestRequestParsing(t *testing.T) {
|
| 251 |
+
body := bytes.NewBufferString(`{"query": "test query"}`)
|
| 252 |
+
req := httptest.NewRequest(http.MethodPost, "/query", body)
|
| 253 |
+
req.Header.Set("Content-Type", "application/json")
|
| 254 |
+
|
| 255 |
+
var parsed gwtypes.QueryRequest
|
| 256 |
+
err := json.NewDecoder(req.Body).Decode(&parsed)
|
| 257 |
+
if err != nil {
|
| 258 |
+
t.Fatalf("Failed to decode: %v", err)
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
if parsed.Query != "test query" {
|
| 262 |
+
t.Errorf("Query mismatch: got '%s'", parsed.Query)
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
// BenchmarkJSONMarshal benchmarks JSON marshaling
|
| 267 |
+
func BenchmarkJSONMarshal(b *testing.B) {
|
| 268 |
+
resp := gwtypes.QueryResponse{
|
| 269 |
+
QueryID: "query-123",
|
| 270 |
+
Answer: "This is a test answer with some content.",
|
| 271 |
+
Sources: []gwtypes.Source{
|
| 272 |
+
{DocumentID: "doc-1", Score: 0.9, Content: "Content 1"},
|
| 273 |
+
{DocumentID: "doc-2", Score: 0.8, Content: "Content 2"},
|
| 274 |
+
},
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
b.ResetTimer()
|
| 278 |
+
for i := 0; i < b.N; i++ {
|
| 279 |
+
json.Marshal(resp)
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
// BenchmarkJSONUnmarshal benchmarks JSON unmarshaling
|
| 284 |
+
func BenchmarkJSONUnmarshal(b *testing.B) {
|
| 285 |
+
data := []byte(`{"query": "test query", "userId": "user-123", "sessionId": "session-456"}`)
|
| 286 |
+
|
| 287 |
+
b.ResetTimer()
|
| 288 |
+
for i := 0; i < b.N; i++ {
|
| 289 |
+
var req gwtypes.QueryRequest
|
| 290 |
+
json.Unmarshal(data, &req)
|
| 291 |
+
}
|
| 292 |
+
}
|
internal/gateway/handlers/memory.go
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"net/http"
|
| 6 |
+
"strconv"
|
| 7 |
+
|
| 8 |
+
"github.com/gorilla/mux"
|
| 9 |
+
"go.uber.org/zap"
|
| 10 |
+
|
| 11 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/gwtypes"
|
| 12 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/middleware"
|
| 13 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/services"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
// MemoryHandler handles memory-related endpoints
|
| 17 |
+
type MemoryHandler struct {
|
| 18 |
+
registry *services.Registry
|
| 19 |
+
logger *zap.Logger
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// NewMemoryHandler creates a new memory handler
|
| 23 |
+
func NewMemoryHandler(registry *services.Registry, logger *zap.Logger) *MemoryHandler {
|
| 24 |
+
return &MemoryHandler{
|
| 25 |
+
registry: registry,
|
| 26 |
+
logger: logger,
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// GetContextWindow handles GET /v2/memory/context
|
| 31 |
+
func (h *MemoryHandler) GetContextWindow(w http.ResponseWriter, r *http.Request) {
|
| 32 |
+
ctx := r.Context()
|
| 33 |
+
|
| 34 |
+
// Get session ID from query params
|
| 35 |
+
sessionID := r.URL.Query().Get("sessionId")
|
| 36 |
+
if sessionID == "" {
|
| 37 |
+
h.errorResponse(w, http.StatusBadRequest, "Session ID is required", "")
|
| 38 |
+
return
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Get max turns
|
| 42 |
+
maxTurns := 50
|
| 43 |
+
if mt := r.URL.Query().Get("maxTurns"); mt != "" {
|
| 44 |
+
if parsed, err := strconv.Atoi(mt); err == nil && parsed > 0 {
|
| 45 |
+
maxTurns = parsed
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Get user ID from context
|
| 50 |
+
userID := middleware.UserIDFromContext(ctx)
|
| 51 |
+
|
| 52 |
+
// Get memory client
|
| 53 |
+
memoryClient, err := h.registry.GetMemoryClient(ctx)
|
| 54 |
+
if err != nil {
|
| 55 |
+
h.logger.Error("Failed to get memory client", zap.Error(err))
|
| 56 |
+
h.errorResponse(w, http.StatusServiceUnavailable, "Service unavailable", "")
|
| 57 |
+
return
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Get context window from memory service
|
| 61 |
+
contextWindow, err := memoryClient.GetContextWindow(ctx, &services.ContextWindowRequest{
|
| 62 |
+
SessionID: sessionID,
|
| 63 |
+
UserID: userID,
|
| 64 |
+
MaxTurns: maxTurns,
|
| 65 |
+
})
|
| 66 |
+
if err != nil {
|
| 67 |
+
h.logger.Error("Failed to get context window", zap.Error(err))
|
| 68 |
+
h.errorResponse(w, http.StatusInternalServerError, "Failed to retrieve context", err.Error())
|
| 69 |
+
return
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Build response
|
| 73 |
+
response := gwtypes.ContextWindow{
|
| 74 |
+
SessionID: sessionID,
|
| 75 |
+
Entries: make([]gwtypes.ContextEntry, len(contextWindow.Entries)),
|
| 76 |
+
TotalTokens: contextWindow.TotalTokens,
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
for i, entry := range contextWindow.Entries {
|
| 80 |
+
response.Entries[i] = gwtypes.ContextEntry{
|
| 81 |
+
ID: entry.ID,
|
| 82 |
+
Type: entry.Type,
|
| 83 |
+
Content: entry.Content,
|
| 84 |
+
Timestamp: entry.Timestamp,
|
| 85 |
+
Score: entry.Score,
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
h.jsonResponse(w, http.StatusOK, response)
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// ConsolidateMemory handles POST /v2/memory/sessions/{sessionId}/consolidate
|
| 93 |
+
func (h *MemoryHandler) ConsolidateMemory(w http.ResponseWriter, r *http.Request) {
|
| 94 |
+
ctx := r.Context()
|
| 95 |
+
vars := mux.Vars(r)
|
| 96 |
+
sessionID := vars["sessionId"]
|
| 97 |
+
|
| 98 |
+
if sessionID == "" {
|
| 99 |
+
h.errorResponse(w, http.StatusBadRequest, "Session ID is required", "")
|
| 100 |
+
return
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Get user ID from context
|
| 104 |
+
userID := middleware.UserIDFromContext(ctx)
|
| 105 |
+
|
| 106 |
+
// Get memory client
|
| 107 |
+
memoryClient, err := h.registry.GetMemoryClient(ctx)
|
| 108 |
+
if err != nil {
|
| 109 |
+
h.logger.Error("Failed to get memory client", zap.Error(err))
|
| 110 |
+
h.errorResponse(w, http.StatusServiceUnavailable, "Service unavailable", "")
|
| 111 |
+
return
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// Trigger consolidation
|
| 115 |
+
err = memoryClient.ConsolidateMemory(ctx, &services.ConsolidateRequest{
|
| 116 |
+
SessionID: sessionID,
|
| 117 |
+
UserID: userID,
|
| 118 |
+
})
|
| 119 |
+
if err != nil {
|
| 120 |
+
h.logger.Error("Failed to consolidate memory", zap.Error(err))
|
| 121 |
+
h.errorResponse(w, http.StatusInternalServerError, "Consolidation failed", err.Error())
|
| 122 |
+
return
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Return 202 Accepted
|
| 126 |
+
w.WriteHeader(http.StatusAccepted)
|
| 127 |
+
json.NewEncoder(w).Encode(map[string]string{
|
| 128 |
+
"status": "accepted",
|
| 129 |
+
"sessionId": sessionID,
|
| 130 |
+
"message": "Memory consolidation started",
|
| 131 |
+
})
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
func (h *MemoryHandler) jsonResponse(w http.ResponseWriter, status int, data interface{}) {
|
| 135 |
+
w.Header().Set("Content-Type", "application/json")
|
| 136 |
+
w.WriteHeader(status)
|
| 137 |
+
json.NewEncoder(w).Encode(data)
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
func (h *MemoryHandler) errorResponse(w http.ResponseWriter, status int, message, detail string) {
|
| 141 |
+
w.Header().Set("Content-Type", "application/json")
|
| 142 |
+
w.WriteHeader(status)
|
| 143 |
+
resp := gwtypes.ErrorResponse{
|
| 144 |
+
Error: http.StatusText(status),
|
| 145 |
+
Message: message,
|
| 146 |
+
RequestID: w.Header().Get("X-Request-ID"),
|
| 147 |
+
}
|
| 148 |
+
if detail != "" {
|
| 149 |
+
resp.Code = detail
|
| 150 |
+
}
|
| 151 |
+
json.NewEncoder(w).Encode(resp)
|
| 152 |
+
}
|
internal/gateway/handlers/query.go
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package handlers provides HTTP request handlers for the API Gateway.
|
| 2 |
+
package handlers
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"context"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"net/http"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
"github.com/gorilla/mux"
|
| 12 |
+
"go.uber.org/zap"
|
| 13 |
+
|
| 14 |
+
gatewaycache "github.com/AmaniQuery/amaniquery/internal/gateway/cache"
|
| 15 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/gwtypes"
|
| 16 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/middleware"
|
| 17 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/services"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
// Ensure context package is used for request handling
|
| 21 |
+
var _ context.Context
|
| 22 |
+
|
| 23 |
+
// QueryHandler handles query-related endpoints
|
| 24 |
+
type QueryHandler struct {
|
| 25 |
+
registry *services.Registry
|
| 26 |
+
cache *gatewaycache.Manager
|
| 27 |
+
logger *zap.Logger
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// NewQueryHandler creates a new query handler
|
| 31 |
+
func NewQueryHandler(registry *services.Registry, cache *gatewaycache.Manager, logger *zap.Logger) *QueryHandler {
|
| 32 |
+
return &QueryHandler{
|
| 33 |
+
registry: registry,
|
| 34 |
+
cache: cache,
|
| 35 |
+
logger: logger,
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// ExecuteQuery handles POST /v2/queries
|
| 40 |
+
func (h *QueryHandler) ExecuteQuery(w http.ResponseWriter, r *http.Request) {
|
| 41 |
+
ctx := r.Context()
|
| 42 |
+
start := time.Now()
|
| 43 |
+
|
| 44 |
+
// Parse request
|
| 45 |
+
var req gwtypes.QueryRequest
|
| 46 |
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
| 47 |
+
h.errorResponse(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
| 48 |
+
return
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Validate required fields
|
| 52 |
+
if req.Query == "" {
|
| 53 |
+
h.errorResponse(w, http.StatusBadRequest, "Query is required", "")
|
| 54 |
+
return
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Get user info from context
|
| 58 |
+
userID := middleware.UserIDFromContext(ctx)
|
| 59 |
+
if req.UserID == "" {
|
| 60 |
+
req.UserID = userID
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Generate query ID
|
| 64 |
+
queryID := uuid.New().String()
|
| 65 |
+
|
| 66 |
+
// Check cache first
|
| 67 |
+
if h.cache != nil && !req.Streaming {
|
| 68 |
+
if cached, found := h.cache.Get(ctx, h.cacheKey(&req)); found {
|
| 69 |
+
w.Header().Set("X-Cache-Hit", "true")
|
| 70 |
+
h.jsonResponse(w, http.StatusOK, cached)
|
| 71 |
+
return
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Get agent service client
|
| 76 |
+
agentClient, err := h.registry.GetAgentClient(ctx)
|
| 77 |
+
if err != nil {
|
| 78 |
+
h.logger.Error("Failed to get agent client", zap.Error(err))
|
| 79 |
+
h.errorResponse(w, http.StatusServiceUnavailable, "Service unavailable", "")
|
| 80 |
+
return
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Execute query via gRPC
|
| 84 |
+
grpcReq := h.toGRPCRequest(&req, queryID)
|
| 85 |
+
grpcResp, err := agentClient.ProcessQuery(ctx, grpcReq)
|
| 86 |
+
if err != nil {
|
| 87 |
+
h.logger.Error("Query execution failed", zap.Error(err), zap.String("query_id", queryID))
|
| 88 |
+
h.errorResponse(w, http.StatusInternalServerError, "Query execution failed", err.Error())
|
| 89 |
+
return
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Build response
|
| 93 |
+
response := h.fromGRPCResponse(grpcResp, queryID, start)
|
| 94 |
+
|
| 95 |
+
// Cache response
|
| 96 |
+
if h.cache != nil && !req.Streaming {
|
| 97 |
+
h.cache.Set(ctx, h.cacheKey(&req), response, h.cacheTTL(&req))
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
w.Header().Set("X-Cache-Hit", "false")
|
| 101 |
+
h.jsonResponse(w, http.StatusOK, response)
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// GetQueryResult handles GET /v2/queries/{queryId}
|
| 105 |
+
func (h *QueryHandler) GetQueryResult(w http.ResponseWriter, r *http.Request) {
|
| 106 |
+
ctx := r.Context()
|
| 107 |
+
vars := mux.Vars(r)
|
| 108 |
+
queryID := vars["queryId"]
|
| 109 |
+
|
| 110 |
+
if queryID == "" {
|
| 111 |
+
h.errorResponse(w, http.StatusBadRequest, "Query ID is required", "")
|
| 112 |
+
return
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Get agent service client
|
| 116 |
+
agentClient, err := h.registry.GetAgentClient(ctx)
|
| 117 |
+
if err != nil {
|
| 118 |
+
h.logger.Error("Failed to get agent client", zap.Error(err))
|
| 119 |
+
h.errorResponse(w, http.StatusServiceUnavailable, "Service unavailable", "")
|
| 120 |
+
return
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// Check query status
|
| 124 |
+
status, err := agentClient.GetQueryStatus(ctx, queryID)
|
| 125 |
+
if err != nil {
|
| 126 |
+
h.logger.Error("Failed to get query status", zap.Error(err), zap.String("query_id", queryID))
|
| 127 |
+
h.errorResponse(w, http.StatusNotFound, "Query not found", "")
|
| 128 |
+
return
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// If still processing, return 202
|
| 132 |
+
if status.Status == "processing" {
|
| 133 |
+
w.Header().Set("Retry-After", "5")
|
| 134 |
+
h.jsonResponse(w, http.StatusAccepted, map[string]interface{}{
|
| 135 |
+
"queryId": queryID,
|
| 136 |
+
"status": status.Status,
|
| 137 |
+
"progress": status.Progress,
|
| 138 |
+
})
|
| 139 |
+
return
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// Return result
|
| 143 |
+
h.jsonResponse(w, http.StatusOK, status.Result)
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
func (h *QueryHandler) cacheKey(req *gwtypes.QueryRequest) string {
|
| 147 |
+
return req.Query + ":" + req.UserID
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
func (h *QueryHandler) cacheTTL(req *gwtypes.QueryRequest) time.Duration {
|
| 151 |
+
if req.Streaming {
|
| 152 |
+
return 30 * time.Second
|
| 153 |
+
}
|
| 154 |
+
if len(req.Query) < 100 {
|
| 155 |
+
return 5 * time.Minute
|
| 156 |
+
}
|
| 157 |
+
return 2 * time.Minute
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
func (h *QueryHandler) toGRPCRequest(req *gwtypes.QueryRequest, queryID string) *services.QueryRequest {
|
| 161 |
+
grpcReq := &services.QueryRequest{
|
| 162 |
+
QueryID: queryID,
|
| 163 |
+
Query: req.Query,
|
| 164 |
+
UserID: req.UserID,
|
| 165 |
+
SessionID: req.SessionID,
|
| 166 |
+
AgentID: req.AgentID,
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
if req.Options != nil {
|
| 170 |
+
grpcReq.Temperature = req.Options.Temperature
|
| 171 |
+
grpcReq.MaxTokens = req.Options.MaxTokens
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
if req.Context != nil {
|
| 175 |
+
grpcReq.MaxTurns = req.Context.MaxTurns
|
| 176 |
+
grpcReq.MemoryTypes = req.Context.MemoryTypes
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
return grpcReq
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
func (h *QueryHandler) fromGRPCResponse(resp *services.QueryResponse, queryID string, start time.Time) *gwtypes.QueryResponse {
|
| 183 |
+
response := &gwtypes.QueryResponse{
|
| 184 |
+
QueryID: queryID,
|
| 185 |
+
Answer: resp.Answer,
|
| 186 |
+
Sources: make([]gwtypes.Source, len(resp.Sources)),
|
| 187 |
+
Metadata: &gwtypes.ResponseMetadata{
|
| 188 |
+
LatencyMs: time.Since(start).Milliseconds(),
|
| 189 |
+
TokensUsed: resp.TokensUsed,
|
| 190 |
+
CacheHit: false,
|
| 191 |
+
},
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
for i, src := range resp.Sources {
|
| 195 |
+
response.Sources[i] = gwtypes.Source{
|
| 196 |
+
DocumentID: src.ID,
|
| 197 |
+
Score: src.Score,
|
| 198 |
+
Content: src.Content,
|
| 199 |
+
Metadata: src.Metadata,
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
return response
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
func (h *QueryHandler) jsonResponse(w http.ResponseWriter, status int, data interface{}) {
|
| 207 |
+
w.Header().Set("Content-Type", "application/json")
|
| 208 |
+
w.WriteHeader(status)
|
| 209 |
+
json.NewEncoder(w).Encode(data)
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
func (h *QueryHandler) errorResponse(w http.ResponseWriter, status int, message, detail string) {
|
| 213 |
+
w.Header().Set("Content-Type", "application/json")
|
| 214 |
+
w.WriteHeader(status)
|
| 215 |
+
resp := gwtypes.ErrorResponse{
|
| 216 |
+
Error: http.StatusText(status),
|
| 217 |
+
Message: message,
|
| 218 |
+
RequestID: w.Header().Get("X-Request-ID"),
|
| 219 |
+
}
|
| 220 |
+
if detail != "" {
|
| 221 |
+
resp.Code = detail
|
| 222 |
+
}
|
| 223 |
+
json.NewEncoder(w).Encode(resp)
|
| 224 |
+
}
|
internal/gateway/handlers/voice_files.go
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package handlers provides HTTP handlers for voice and file services
|
| 2 |
+
package handlers
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"net/http"
|
| 9 |
+
"strconv"
|
| 10 |
+
|
| 11 |
+
"github.com/gorilla/mux"
|
| 12 |
+
"go.uber.org/zap"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
// VoiceHandler handles voice-related requests
|
| 16 |
+
type VoiceHandler struct {
|
| 17 |
+
voiceServiceURL string
|
| 18 |
+
logger *zap.Logger
|
| 19 |
+
httpClient *http.Client
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// NewVoiceHandler creates a new voice handler
|
| 23 |
+
func NewVoiceHandler(voiceServiceURL string, logger *zap.Logger) *VoiceHandler {
|
| 24 |
+
return &VoiceHandler{
|
| 25 |
+
voiceServiceURL: voiceServiceURL,
|
| 26 |
+
logger: logger,
|
| 27 |
+
httpClient: &http.Client{},
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// RegisterRoutes registers voice routes
|
| 32 |
+
func (h *VoiceHandler) RegisterRoutes(r *mux.Router) {
|
| 33 |
+
r.HandleFunc("/api/v1/voice/sessions", h.ListSessions).Methods("GET")
|
| 34 |
+
r.HandleFunc("/api/v1/voice/sessions/{sessionId}", h.GetSession).Methods("GET")
|
| 35 |
+
r.HandleFunc("/api/v1/voice/sessions/{sessionId}/end", h.EndSession).Methods("POST")
|
| 36 |
+
r.HandleFunc("/api/v1/voice/voices", h.ListVoices).Methods("GET")
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// ListSessions lists active voice sessions
|
| 40 |
+
func (h *VoiceHandler) ListSessions(w http.ResponseWriter, r *http.Request) {
|
| 41 |
+
h.proxyRequest(w, r, "/api/v1/voice/sessions")
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// GetSession gets a voice session
|
| 45 |
+
func (h *VoiceHandler) GetSession(w http.ResponseWriter, r *http.Request) {
|
| 46 |
+
vars := mux.Vars(r)
|
| 47 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/voice/sessions/%s", vars["sessionId"]))
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// EndSession ends a voice session
|
| 51 |
+
func (h *VoiceHandler) EndSession(w http.ResponseWriter, r *http.Request) {
|
| 52 |
+
vars := mux.Vars(r)
|
| 53 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/voice/sessions/%s/end", vars["sessionId"]))
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// ListVoices lists available TTS voices
|
| 57 |
+
func (h *VoiceHandler) ListVoices(w http.ResponseWriter, r *http.Request) {
|
| 58 |
+
voices := []map[string]string{
|
| 59 |
+
{"id": "nova", "name": "Nova", "gender": "female", "style": "natural"},
|
| 60 |
+
{"id": "alloy", "name": "Alloy", "gender": "female", "style": "professional"},
|
| 61 |
+
{"id": "echo", "name": "Echo", "gender": "male", "style": "natural"},
|
| 62 |
+
{"id": "onyx", "name": "Onyx", "gender": "male", "style": "deep"},
|
| 63 |
+
{"id": "shimmer", "name": "Shimmer", "gender": "female", "style": "soft"},
|
| 64 |
+
{"id": "swahili", "name": "Rafiki", "gender": "male", "style": "swahili"},
|
| 65 |
+
}
|
| 66 |
+
json.NewEncoder(w).Encode(voices)
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
func (h *VoiceHandler) proxyRequest(w http.ResponseWriter, r *http.Request, path string) {
|
| 70 |
+
req, err := http.NewRequest(r.Method, h.voiceServiceURL+path, r.Body)
|
| 71 |
+
if err != nil {
|
| 72 |
+
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
| 73 |
+
return
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Copy headers
|
| 77 |
+
for key, values := range r.Header {
|
| 78 |
+
for _, value := range values {
|
| 79 |
+
req.Header.Add(key, value)
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
resp, err := h.httpClient.Do(req)
|
| 84 |
+
if err != nil {
|
| 85 |
+
http.Error(w, "Voice service unavailable", http.StatusServiceUnavailable)
|
| 86 |
+
return
|
| 87 |
+
}
|
| 88 |
+
defer resp.Body.Close()
|
| 89 |
+
|
| 90 |
+
// Copy response
|
| 91 |
+
for key, values := range resp.Header {
|
| 92 |
+
for _, value := range values {
|
| 93 |
+
w.Header().Add(key, value)
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
w.WriteHeader(resp.StatusCode)
|
| 97 |
+
io.Copy(w, resp.Body)
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// FileHandler handles file-related requests
|
| 101 |
+
type FileHandler struct {
|
| 102 |
+
fileServiceURL string
|
| 103 |
+
logger *zap.Logger
|
| 104 |
+
httpClient *http.Client
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// NewFileHandler creates a new file handler
|
| 108 |
+
func NewFileHandler(fileServiceURL string, logger *zap.Logger) *FileHandler {
|
| 109 |
+
return &FileHandler{
|
| 110 |
+
fileServiceURL: fileServiceURL,
|
| 111 |
+
logger: logger,
|
| 112 |
+
httpClient: &http.Client{},
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// RegisterRoutes registers file routes
|
| 117 |
+
func (h *FileHandler) RegisterRoutes(r *mux.Router) {
|
| 118 |
+
// Upload routes
|
| 119 |
+
r.HandleFunc("/api/v1/files/upload", h.InitiateUpload).Methods("POST")
|
| 120 |
+
r.HandleFunc("/api/v1/files/upload/{fileId}/chunk/{chunkIndex}", h.UploadChunk).Methods("POST")
|
| 121 |
+
r.HandleFunc("/api/v1/files/upload/{fileId}/complete", h.CompleteUpload).Methods("POST")
|
| 122 |
+
|
| 123 |
+
// File management routes
|
| 124 |
+
r.HandleFunc("/api/v1/files", h.ListFiles).Methods("GET")
|
| 125 |
+
r.HandleFunc("/api/v1/files/{fileId}", h.GetFile).Methods("GET")
|
| 126 |
+
r.HandleFunc("/api/v1/files/{fileId}", h.DeleteFile).Methods("DELETE")
|
| 127 |
+
r.HandleFunc("/api/v1/files/{fileId}/download", h.DownloadFile).Methods("GET")
|
| 128 |
+
r.HandleFunc("/api/v1/files/{fileId}/preview", h.PreviewFile).Methods("GET")
|
| 129 |
+
r.HandleFunc("/api/v1/files/{fileId}/share", h.ShareFile).Methods("POST")
|
| 130 |
+
|
| 131 |
+
// Chat integration
|
| 132 |
+
r.HandleFunc("/api/v1/files/{fileId}/chat", h.GetFileChat).Methods("GET")
|
| 133 |
+
r.HandleFunc("/api/v1/files/{fileId}/chat/message", h.SendFileMessage).Methods("POST")
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// InitiateUpload starts a file upload
|
| 137 |
+
func (h *FileHandler) InitiateUpload(w http.ResponseWriter, r *http.Request) {
|
| 138 |
+
h.proxyRequest(w, r, "/api/v1/files/upload")
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// UploadChunk handles chunk upload
|
| 142 |
+
func (h *FileHandler) UploadChunk(w http.ResponseWriter, r *http.Request) {
|
| 143 |
+
vars := mux.Vars(r)
|
| 144 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/files/upload/%s/chunk/%s", vars["fileId"], vars["chunkIndex"]))
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// CompleteUpload completes a file upload
|
| 148 |
+
func (h *FileHandler) CompleteUpload(w http.ResponseWriter, r *http.Request) {
|
| 149 |
+
vars := mux.Vars(r)
|
| 150 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/files/upload/%s/complete", vars["fileId"]))
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
// ListFiles lists user files
|
| 154 |
+
func (h *FileHandler) ListFiles(w http.ResponseWriter, r *http.Request) {
|
| 155 |
+
h.proxyRequest(w, r, "/api/v1/files")
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// GetFile gets file details
|
| 159 |
+
func (h *FileHandler) GetFile(w http.ResponseWriter, r *http.Request) {
|
| 160 |
+
vars := mux.Vars(r)
|
| 161 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/files/%s", vars["fileId"]))
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// DeleteFile deletes a file
|
| 165 |
+
func (h *FileHandler) DeleteFile(w http.ResponseWriter, r *http.Request) {
|
| 166 |
+
vars := mux.Vars(r)
|
| 167 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/files/%s", vars["fileId"]))
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// DownloadFile downloads a file
|
| 171 |
+
func (h *FileHandler) DownloadFile(w http.ResponseWriter, r *http.Request) {
|
| 172 |
+
vars := mux.Vars(r)
|
| 173 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/files/%s/download", vars["fileId"]))
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// PreviewFile previews a file
|
| 177 |
+
func (h *FileHandler) PreviewFile(w http.ResponseWriter, r *http.Request) {
|
| 178 |
+
vars := mux.Vars(r)
|
| 179 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/files/%s/preview", vars["fileId"]))
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// ShareFile creates a share link
|
| 183 |
+
func (h *FileHandler) ShareFile(w http.ResponseWriter, r *http.Request) {
|
| 184 |
+
vars := mux.Vars(r)
|
| 185 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/files/%s/share", vars["fileId"]))
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// GetFileChat gets chat messages for a file
|
| 189 |
+
func (h *FileHandler) GetFileChat(w http.ResponseWriter, r *http.Request) {
|
| 190 |
+
vars := mux.Vars(r)
|
| 191 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/files/%s/chat", vars["fileId"]))
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
// SendFileMessage sends a message about a file
|
| 195 |
+
func (h *FileHandler) SendFileMessage(w http.ResponseWriter, r *http.Request) {
|
| 196 |
+
vars := mux.Vars(r)
|
| 197 |
+
h.proxyRequest(w, r, fmt.Sprintf("/api/v1/files/%s/chat/message", vars["fileId"]))
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
func (h *FileHandler) proxyRequest(w http.ResponseWriter, r *http.Request, path string) {
|
| 201 |
+
req, err := http.NewRequest(r.Method, h.fileServiceURL+path, r.Body)
|
| 202 |
+
if err != nil {
|
| 203 |
+
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
| 204 |
+
return
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// Copy headers
|
| 208 |
+
for key, values := range r.Header {
|
| 209 |
+
for _, value := range values {
|
| 210 |
+
req.Header.Add(key, value)
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
resp, err := h.httpClient.Do(req)
|
| 215 |
+
if err != nil {
|
| 216 |
+
http.Error(w, "File service unavailable", http.StatusServiceUnavailable)
|
| 217 |
+
return
|
| 218 |
+
}
|
| 219 |
+
defer resp.Body.Close()
|
| 220 |
+
|
| 221 |
+
// Copy response
|
| 222 |
+
for key, values := range resp.Header {
|
| 223 |
+
for _, value := range values {
|
| 224 |
+
w.Header().Add(key, value)
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
w.WriteHeader(resp.StatusCode)
|
| 228 |
+
io.Copy(w, resp.Body)
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// Unused variable suppression
|
| 232 |
+
var _ = strconv.Atoi
|
internal/gateway/handlers/websocket.go
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"net/http"
|
| 7 |
+
"sync"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
"github.com/gorilla/websocket"
|
| 12 |
+
"go.uber.org/zap"
|
| 13 |
+
|
| 14 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/gwtypes"
|
| 15 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/middleware"
|
| 16 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/services"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
// WebSocketHandler handles WebSocket connections for streaming queries
|
| 20 |
+
type WebSocketHandler struct {
|
| 21 |
+
registry *services.Registry
|
| 22 |
+
config gwtypes.WebSocketConfig
|
| 23 |
+
upgrader websocket.Upgrader
|
| 24 |
+
connections sync.Map // map[string]*wsConnection
|
| 25 |
+
logger *zap.Logger
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
type wsConnection struct {
|
| 29 |
+
conn *websocket.Conn
|
| 30 |
+
userID string
|
| 31 |
+
sessionID string
|
| 32 |
+
createdAt time.Time
|
| 33 |
+
cancel context.CancelFunc
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// NewWebSocketHandler creates a new WebSocket handler
|
| 37 |
+
func NewWebSocketHandler(registry *services.Registry, cfg gwtypes.WebSocketConfig, logger *zap.Logger) *WebSocketHandler {
|
| 38 |
+
return &WebSocketHandler{
|
| 39 |
+
registry: registry,
|
| 40 |
+
config: cfg,
|
| 41 |
+
upgrader: websocket.Upgrader{
|
| 42 |
+
CheckOrigin: func(r *http.Request) bool {
|
| 43 |
+
// Allow all origins for now, configure in production
|
| 44 |
+
return true
|
| 45 |
+
},
|
| 46 |
+
ReadBufferSize: cfg.ReadBufferSize,
|
| 47 |
+
WriteBufferSize: cfg.WriteBufferSize,
|
| 48 |
+
Subprotocols: []string{"rag-agent-protocol"},
|
| 49 |
+
},
|
| 50 |
+
logger: logger,
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// HandleStream handles WebSocket upgrade and streaming
|
| 55 |
+
func (h *WebSocketHandler) HandleStream(w http.ResponseWriter, r *http.Request) {
|
| 56 |
+
// Upgrade connection
|
| 57 |
+
conn, err := h.upgrader.Upgrade(w, r, nil)
|
| 58 |
+
if err != nil {
|
| 59 |
+
h.logger.Error("WebSocket upgrade failed", zap.Error(err))
|
| 60 |
+
return
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Create connection context with cancel
|
| 64 |
+
ctx, cancel := context.WithCancel(r.Context())
|
| 65 |
+
|
| 66 |
+
// Generate connection ID
|
| 67 |
+
connID := uuid.New().String()
|
| 68 |
+
|
| 69 |
+
// Get user info from context
|
| 70 |
+
userID := middleware.UserIDFromContext(r.Context())
|
| 71 |
+
sessionID := r.URL.Query().Get("sessionId")
|
| 72 |
+
|
| 73 |
+
// Store connection
|
| 74 |
+
wsConn := &wsConnection{
|
| 75 |
+
conn: conn,
|
| 76 |
+
userID: userID,
|
| 77 |
+
sessionID: sessionID,
|
| 78 |
+
createdAt: time.Now(),
|
| 79 |
+
cancel: cancel,
|
| 80 |
+
}
|
| 81 |
+
h.connections.Store(connID, wsConn)
|
| 82 |
+
|
| 83 |
+
defer func() {
|
| 84 |
+
h.connections.Delete(connID)
|
| 85 |
+
cancel()
|
| 86 |
+
conn.Close()
|
| 87 |
+
}()
|
| 88 |
+
|
| 89 |
+
h.logger.Info("WebSocket connection established",
|
| 90 |
+
zap.String("conn_id", connID),
|
| 91 |
+
zap.String("user_id", userID),
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
// Start ping-pong keepalive
|
| 95 |
+
go h.keepalive(ctx, conn, connID)
|
| 96 |
+
|
| 97 |
+
// Message handling loop
|
| 98 |
+
h.messageLoop(ctx, conn, connID, userID, sessionID)
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
func (h *WebSocketHandler) messageLoop(ctx context.Context, conn *websocket.Conn, connID, userID, sessionID string) {
|
| 102 |
+
for {
|
| 103 |
+
select {
|
| 104 |
+
case <-ctx.Done():
|
| 105 |
+
return
|
| 106 |
+
default:
|
| 107 |
+
// Set read deadline
|
| 108 |
+
conn.SetReadDeadline(time.Now().Add(h.config.PongWait))
|
| 109 |
+
|
| 110 |
+
// Read message
|
| 111 |
+
_, message, err := conn.ReadMessage()
|
| 112 |
+
if err != nil {
|
| 113 |
+
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
| 114 |
+
h.logger.Error("WebSocket read error", zap.Error(err), zap.String("conn_id", connID))
|
| 115 |
+
}
|
| 116 |
+
return
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Parse message
|
| 120 |
+
var msg gwtypes.WebSocketMessage
|
| 121 |
+
if err := json.Unmarshal(message, &msg); err != nil {
|
| 122 |
+
h.sendError(conn, "Invalid message format")
|
| 123 |
+
continue
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// Handle message
|
| 127 |
+
go h.handleMessage(ctx, conn, connID, userID, sessionID, msg)
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
func (h *WebSocketHandler) handleMessage(ctx context.Context, conn *websocket.Conn, connID, userID, sessionID string, msg gwtypes.WebSocketMessage) {
|
| 133 |
+
switch msg.Type {
|
| 134 |
+
case "query":
|
| 135 |
+
h.handleQuery(ctx, conn, connID, userID, sessionID, msg)
|
| 136 |
+
case "ping":
|
| 137 |
+
h.sendMessage(conn, gwtypes.WebSocketServerMessage{
|
| 138 |
+
Type: "pong",
|
| 139 |
+
Timestamp: time.Now().Format(time.RFC3339),
|
| 140 |
+
})
|
| 141 |
+
case "cancel":
|
| 142 |
+
h.handleCancel(connID, msg)
|
| 143 |
+
default:
|
| 144 |
+
h.sendError(conn, "Unknown message type")
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
func (h *WebSocketHandler) handleQuery(ctx context.Context, conn *websocket.Conn, connID, userID, sessionID string, msg gwtypes.WebSocketMessage) {
|
| 149 |
+
// Parse query payload
|
| 150 |
+
payloadBytes, _ := json.Marshal(msg.Payload)
|
| 151 |
+
var payload gwtypes.WebSocketQueryPayload
|
| 152 |
+
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
|
| 153 |
+
h.sendError(conn, "Invalid query payload")
|
| 154 |
+
return
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Set defaults
|
| 158 |
+
if payload.UserID == "" {
|
| 159 |
+
payload.UserID = userID
|
| 160 |
+
}
|
| 161 |
+
if payload.SessionID == "" {
|
| 162 |
+
payload.SessionID = sessionID
|
| 163 |
+
}
|
| 164 |
+
if payload.QueryID == "" {
|
| 165 |
+
payload.QueryID = uuid.New().String()
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
h.logger.Info("Processing streaming query",
|
| 169 |
+
zap.String("conn_id", connID),
|
| 170 |
+
zap.String("query_id", payload.QueryID),
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
// Get agent client
|
| 174 |
+
agentClient, err := h.registry.GetAgentClient(ctx)
|
| 175 |
+
if err != nil {
|
| 176 |
+
h.logger.Error("Failed to get agent client", zap.Error(err))
|
| 177 |
+
h.sendError(conn, "Service unavailable")
|
| 178 |
+
return
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// Create streaming request
|
| 182 |
+
streamReq := &services.StreamQueryRequest{
|
| 183 |
+
QueryID: payload.QueryID,
|
| 184 |
+
Query: payload.Query,
|
| 185 |
+
UserID: payload.UserID,
|
| 186 |
+
SessionID: payload.SessionID,
|
| 187 |
+
AgentID: payload.AgentID,
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
if payload.Options != nil {
|
| 191 |
+
streamReq.Temperature = payload.Options.Temperature
|
| 192 |
+
streamReq.MaxTokens = payload.Options.MaxTokens
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// Execute streaming query
|
| 196 |
+
streamChan, errChan := agentClient.ProcessQueryStream(ctx, streamReq)
|
| 197 |
+
|
| 198 |
+
// Stream results to client
|
| 199 |
+
for {
|
| 200 |
+
select {
|
| 201 |
+
case <-ctx.Done():
|
| 202 |
+
return
|
| 203 |
+
case err := <-errChan:
|
| 204 |
+
if err != nil {
|
| 205 |
+
h.logger.Error("Stream error", zap.Error(err), zap.String("query_id", payload.QueryID))
|
| 206 |
+
h.sendError(conn, err.Error())
|
| 207 |
+
}
|
| 208 |
+
return
|
| 209 |
+
case chunk, ok := <-streamChan:
|
| 210 |
+
if !ok {
|
| 211 |
+
h.sendMessage(conn, gwtypes.WebSocketServerMessage{
|
| 212 |
+
Type: "done",
|
| 213 |
+
Timestamp: time.Now().Format(time.RFC3339),
|
| 214 |
+
Metadata: map[string]interface{}{
|
| 215 |
+
"queryId": payload.QueryID,
|
| 216 |
+
},
|
| 217 |
+
})
|
| 218 |
+
return
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// Send chunk
|
| 222 |
+
h.sendMessage(conn, gwtypes.WebSocketServerMessage{
|
| 223 |
+
Type: chunk.Type,
|
| 224 |
+
Timestamp: time.Now().Format(time.RFC3339),
|
| 225 |
+
Data: chunk.Data,
|
| 226 |
+
Metadata: chunk.Metadata,
|
| 227 |
+
})
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
func (h *WebSocketHandler) handleCancel(connID string, msg gwtypes.WebSocketMessage) {
|
| 233 |
+
if wsConn, ok := h.connections.Load(connID); ok {
|
| 234 |
+
wsConn.(*wsConnection).cancel()
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
func (h *WebSocketHandler) keepalive(ctx context.Context, conn *websocket.Conn, connID string) {
|
| 239 |
+
ticker := time.NewTicker(h.config.PingInterval)
|
| 240 |
+
defer ticker.Stop()
|
| 241 |
+
|
| 242 |
+
for {
|
| 243 |
+
select {
|
| 244 |
+
case <-ticker.C:
|
| 245 |
+
conn.SetWriteDeadline(time.Now().Add(h.config.WriteWait))
|
| 246 |
+
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
| 247 |
+
h.logger.Debug("Ping failed", zap.String("conn_id", connID), zap.Error(err))
|
| 248 |
+
return
|
| 249 |
+
}
|
| 250 |
+
case <-ctx.Done():
|
| 251 |
+
return
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
func (h *WebSocketHandler) sendMessage(conn *websocket.Conn, msg gwtypes.WebSocketServerMessage) {
|
| 257 |
+
conn.SetWriteDeadline(time.Now().Add(h.config.WriteWait))
|
| 258 |
+
if err := conn.WriteJSON(msg); err != nil {
|
| 259 |
+
h.logger.Error("Failed to send WebSocket message", zap.Error(err))
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
func (h *WebSocketHandler) sendError(conn *websocket.Conn, message string) {
|
| 264 |
+
h.sendMessage(conn, gwtypes.WebSocketServerMessage{
|
| 265 |
+
Type: "error",
|
| 266 |
+
Timestamp: time.Now().Format(time.RFC3339),
|
| 267 |
+
Data: message,
|
| 268 |
+
})
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
// CloseAll closes all active connections
|
| 272 |
+
func (h *WebSocketHandler) CloseAll() {
|
| 273 |
+
h.connections.Range(func(key, value interface{}) bool {
|
| 274 |
+
wsConn := value.(*wsConnection)
|
| 275 |
+
wsConn.cancel()
|
| 276 |
+
wsConn.conn.Close()
|
| 277 |
+
return true
|
| 278 |
+
})
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// ActiveConnections returns the number of active connections
|
| 282 |
+
func (h *WebSocketHandler) ActiveConnections() int {
|
| 283 |
+
count := 0
|
| 284 |
+
h.connections.Range(func(key, value interface{}) bool {
|
| 285 |
+
count++
|
| 286 |
+
return true
|
| 287 |
+
})
|
| 288 |
+
return count
|
| 289 |
+
}
|
internal/gateway/middleware/audit.go
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"net/http"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
"go.uber.org/zap"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
// AuditEvent represents an audit log entry
|
| 12 |
+
type AuditEvent struct {
|
| 13 |
+
Timestamp int64 `json:"timestamp"`
|
| 14 |
+
RequestID string `json:"request_id"`
|
| 15 |
+
UserID string `json:"user_id"`
|
| 16 |
+
TenantID string `json:"tenant_id"`
|
| 17 |
+
Method string `json:"method"`
|
| 18 |
+
Path string `json:"path"`
|
| 19 |
+
Query string `json:"query,omitempty"`
|
| 20 |
+
StatusCode int `json:"status_code"`
|
| 21 |
+
LatencyMs float64 `json:"latency_ms"`
|
| 22 |
+
RequestSize int64 `json:"request_size"`
|
| 23 |
+
ResponseSize int `json:"response_size"`
|
| 24 |
+
UserAgent string `json:"user_agent"`
|
| 25 |
+
IP string `json:"ip"`
|
| 26 |
+
Error string `json:"error,omitempty"`
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// AuditMiddleware logs all API requests for auditing
|
| 30 |
+
type AuditMiddleware struct {
|
| 31 |
+
logger *zap.Logger
|
| 32 |
+
skipPaths map[string]bool
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// NewAuditMiddleware creates a new audit middleware
|
| 36 |
+
func NewAuditMiddleware(logger *zap.Logger) *AuditMiddleware {
|
| 37 |
+
return &AuditMiddleware{
|
| 38 |
+
logger: logger,
|
| 39 |
+
skipPaths: map[string]bool{
|
| 40 |
+
"/admin/health": true,
|
| 41 |
+
"/admin/metrics": true,
|
| 42 |
+
},
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Handler is the middleware handler function
|
| 47 |
+
func (m *AuditMiddleware) Handler(next http.Handler) http.Handler {
|
| 48 |
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 49 |
+
// Skip audit for certain paths
|
| 50 |
+
if m.skipPaths[r.URL.Path] {
|
| 51 |
+
next.ServeHTTP(w, r)
|
| 52 |
+
return
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
start := time.Now()
|
| 56 |
+
|
| 57 |
+
// Wrap response writer to capture status code and size
|
| 58 |
+
rw := &responseWriter{
|
| 59 |
+
ResponseWriter: w,
|
| 60 |
+
statusCode: http.StatusOK,
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Call next handler
|
| 64 |
+
next.ServeHTTP(rw, r)
|
| 65 |
+
|
| 66 |
+
// Calculate latency
|
| 67 |
+
latency := time.Since(start)
|
| 68 |
+
|
| 69 |
+
// Build audit event
|
| 70 |
+
event := AuditEvent{
|
| 71 |
+
Timestamp: time.Now().UnixNano() / int64(time.Millisecond),
|
| 72 |
+
RequestID: w.Header().Get("X-Request-ID"),
|
| 73 |
+
UserID: UserIDFromContext(r.Context()),
|
| 74 |
+
TenantID: TenantIDFromContext(r.Context()),
|
| 75 |
+
Method: r.Method,
|
| 76 |
+
Path: r.URL.Path,
|
| 77 |
+
Query: r.URL.RawQuery,
|
| 78 |
+
StatusCode: rw.statusCode,
|
| 79 |
+
LatencyMs: float64(latency.Nanoseconds()) / float64(time.Millisecond),
|
| 80 |
+
RequestSize: r.ContentLength,
|
| 81 |
+
ResponseSize: rw.size,
|
| 82 |
+
UserAgent: r.UserAgent(),
|
| 83 |
+
IP: getClientIP(r),
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// Log the audit event
|
| 87 |
+
m.logEvent(event)
|
| 88 |
+
})
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
func (m *AuditMiddleware) logEvent(event AuditEvent) {
|
| 92 |
+
// Log as structured JSON
|
| 93 |
+
eventJSON, err := json.Marshal(event)
|
| 94 |
+
if err != nil {
|
| 95 |
+
m.logger.Error("Failed to marshal audit event", zap.Error(err))
|
| 96 |
+
return
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
m.logger.Info("audit",
|
| 100 |
+
zap.String("event", string(eventJSON)),
|
| 101 |
+
zap.String("request_id", event.RequestID),
|
| 102 |
+
zap.String("user_id", event.UserID),
|
| 103 |
+
zap.String("method", event.Method),
|
| 104 |
+
zap.String("path", event.Path),
|
| 105 |
+
zap.Int("status", event.StatusCode),
|
| 106 |
+
zap.Float64("latency_ms", event.LatencyMs),
|
| 107 |
+
)
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// responseWriter wraps http.ResponseWriter to capture response details
|
| 111 |
+
type responseWriter struct {
|
| 112 |
+
http.ResponseWriter
|
| 113 |
+
statusCode int
|
| 114 |
+
size int
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
func (rw *responseWriter) WriteHeader(code int) {
|
| 118 |
+
rw.statusCode = code
|
| 119 |
+
rw.ResponseWriter.WriteHeader(code)
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
func (rw *responseWriter) Write(b []byte) (int, error) {
|
| 123 |
+
size, err := rw.ResponseWriter.Write(b)
|
| 124 |
+
rw.size += size
|
| 125 |
+
return size, err
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// Flush implements http.Flusher
|
| 129 |
+
func (rw *responseWriter) Flush() {
|
| 130 |
+
if f, ok := rw.ResponseWriter.(http.Flusher); ok {
|
| 131 |
+
f.Flush()
|
| 132 |
+
}
|
| 133 |
+
}
|
internal/gateway/middleware/auth.go
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"context"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"errors"
|
| 8 |
+
"fmt"
|
| 9 |
+
"net/http"
|
| 10 |
+
"strings"
|
| 11 |
+
"time"
|
| 12 |
+
|
| 13 |
+
"github.com/golang-jwt/jwt/v5"
|
| 14 |
+
"go.uber.org/zap"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
// Context keys for request values
|
| 18 |
+
type contextKey string
|
| 19 |
+
|
| 20 |
+
const (
|
| 21 |
+
ContextKeyUserID contextKey = "userId"
|
| 22 |
+
ContextKeyTenantID contextKey = "tenantId"
|
| 23 |
+
ContextKeyClaims contextKey = "claims"
|
| 24 |
+
ContextKeyRoles contextKey = "roles"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
// AuthConfig holds authentication configuration
|
| 28 |
+
type AuthConfig struct {
|
| 29 |
+
JWTSecret string
|
| 30 |
+
JWTIssuer string
|
| 31 |
+
JWTAudience string
|
| 32 |
+
OPAEnabled bool
|
| 33 |
+
OPAAddr string
|
| 34 |
+
OPAPolicy string
|
| 35 |
+
SkipPaths []string
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// JWTClaims represents JWT token claims
|
| 39 |
+
type JWTClaims struct {
|
| 40 |
+
jwt.RegisteredClaims
|
| 41 |
+
UserID string `json:"user_id"`
|
| 42 |
+
Email string `json:"email"`
|
| 43 |
+
Roles []string `json:"roles"`
|
| 44 |
+
TenantID string `json:"tenant_id,omitempty"`
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// AuthMiddleware handles JWT authentication and OPA authorization
|
| 48 |
+
type AuthMiddleware struct {
|
| 49 |
+
config AuthConfig
|
| 50 |
+
skipPaths map[string]bool
|
| 51 |
+
opaClient *OPAClient
|
| 52 |
+
logger *zap.Logger
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// NewAuthMiddleware creates a new authentication middleware
|
| 56 |
+
func NewAuthMiddleware(cfg AuthConfig, logger *zap.Logger) *AuthMiddleware {
|
| 57 |
+
skipPaths := make(map[string]bool)
|
| 58 |
+
for _, path := range cfg.SkipPaths {
|
| 59 |
+
skipPaths[path] = true
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
m := &AuthMiddleware{
|
| 63 |
+
config: cfg,
|
| 64 |
+
skipPaths: skipPaths,
|
| 65 |
+
logger: logger,
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
if cfg.OPAEnabled && cfg.OPAAddr != "" {
|
| 69 |
+
m.opaClient = NewOPAClient(cfg.OPAAddr, cfg.OPAPolicy)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return m
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Handler is the middleware handler function
|
| 76 |
+
func (m *AuthMiddleware) Handler(next http.Handler) http.Handler {
|
| 77 |
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 78 |
+
// Skip authentication for certain paths
|
| 79 |
+
if m.skipPaths[r.URL.Path] {
|
| 80 |
+
next.ServeHTTP(w, r)
|
| 81 |
+
return
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Extract token
|
| 85 |
+
token := m.extractToken(r)
|
| 86 |
+
if token == "" {
|
| 87 |
+
m.unauthorized(w, "Missing authorization token")
|
| 88 |
+
return
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Validate JWT
|
| 92 |
+
claims, err := m.validateToken(token)
|
| 93 |
+
if err != nil {
|
| 94 |
+
m.logger.Debug("Token validation failed", zap.Error(err))
|
| 95 |
+
m.unauthorized(w, "Invalid token")
|
| 96 |
+
return
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Check token expiration
|
| 100 |
+
if claims.ExpiresAt != nil && time.Now().After(claims.ExpiresAt.Time) {
|
| 101 |
+
m.unauthorized(w, "Token expired")
|
| 102 |
+
return
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// OPA authorization
|
| 106 |
+
if m.opaClient != nil {
|
| 107 |
+
allowed, err := m.opaClient.Authorize(r.Context(), AuthzInput{
|
| 108 |
+
User: claims.UserID,
|
| 109 |
+
Roles: claims.Roles,
|
| 110 |
+
Action: r.Method,
|
| 111 |
+
Path: r.URL.Path,
|
| 112 |
+
Tenant: claims.TenantID,
|
| 113 |
+
})
|
| 114 |
+
if err != nil {
|
| 115 |
+
m.logger.Error("OPA authorization failed", zap.Error(err))
|
| 116 |
+
m.forbidden(w, "Authorization service unavailable")
|
| 117 |
+
return
|
| 118 |
+
}
|
| 119 |
+
if !allowed {
|
| 120 |
+
m.forbidden(w, "Access denied")
|
| 121 |
+
return
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Add claims to context
|
| 126 |
+
ctx := r.Context()
|
| 127 |
+
ctx = context.WithValue(ctx, ContextKeyClaims, claims)
|
| 128 |
+
ctx = context.WithValue(ctx, ContextKeyUserID, claims.UserID)
|
| 129 |
+
ctx = context.WithValue(ctx, ContextKeyTenantID, claims.TenantID)
|
| 130 |
+
ctx = context.WithValue(ctx, ContextKeyRoles, claims.Roles)
|
| 131 |
+
|
| 132 |
+
next.ServeHTTP(w, r.WithContext(ctx))
|
| 133 |
+
})
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
func (m *AuthMiddleware) extractToken(r *http.Request) string {
|
| 137 |
+
// Check Authorization header
|
| 138 |
+
authHeader := r.Header.Get("Authorization")
|
| 139 |
+
if authHeader != "" {
|
| 140 |
+
if strings.HasPrefix(authHeader, "Bearer ") {
|
| 141 |
+
return strings.TrimPrefix(authHeader, "Bearer ")
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Check query parameter (for WebSocket connections)
|
| 146 |
+
if token := r.URL.Query().Get("token"); token != "" {
|
| 147 |
+
return token
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
return ""
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
func (m *AuthMiddleware) validateToken(tokenString string) (*JWTClaims, error) {
|
| 154 |
+
claims := &JWTClaims{}
|
| 155 |
+
|
| 156 |
+
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
| 157 |
+
// Validate signing method
|
| 158 |
+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
| 159 |
+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
| 160 |
+
}
|
| 161 |
+
return []byte(m.config.JWTSecret), nil
|
| 162 |
+
})
|
| 163 |
+
|
| 164 |
+
if err != nil {
|
| 165 |
+
return nil, fmt.Errorf("failed to parse token: %w", err)
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
if !token.Valid {
|
| 169 |
+
return nil, errors.New("invalid token")
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// Validate issuer
|
| 173 |
+
if m.config.JWTIssuer != "" && claims.Issuer != m.config.JWTIssuer {
|
| 174 |
+
return nil, fmt.Errorf("invalid issuer: expected %s, got %s", m.config.JWTIssuer, claims.Issuer)
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// Validate audience
|
| 178 |
+
if m.config.JWTAudience != "" {
|
| 179 |
+
hasAudience := false
|
| 180 |
+
for _, aud := range claims.Audience {
|
| 181 |
+
if aud == m.config.JWTAudience {
|
| 182 |
+
hasAudience = true
|
| 183 |
+
break
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
if !hasAudience {
|
| 187 |
+
return nil, errors.New("invalid audience")
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
return claims, nil
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
func (m *AuthMiddleware) unauthorized(w http.ResponseWriter, message string) {
|
| 195 |
+
w.Header().Set("Content-Type", "application/json")
|
| 196 |
+
w.Header().Set("WWW-Authenticate", "Bearer")
|
| 197 |
+
w.WriteHeader(http.StatusUnauthorized)
|
| 198 |
+
json.NewEncoder(w).Encode(map[string]string{
|
| 199 |
+
"error": "unauthorized",
|
| 200 |
+
"message": message,
|
| 201 |
+
})
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
func (m *AuthMiddleware) forbidden(w http.ResponseWriter, message string) {
|
| 205 |
+
w.Header().Set("Content-Type", "application/json")
|
| 206 |
+
w.WriteHeader(http.StatusForbidden)
|
| 207 |
+
json.NewEncoder(w).Encode(map[string]string{
|
| 208 |
+
"error": "forbidden",
|
| 209 |
+
"message": message,
|
| 210 |
+
})
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// UserIDFromContext extracts user ID from context
|
| 214 |
+
func UserIDFromContext(ctx context.Context) string {
|
| 215 |
+
if userID, ok := ctx.Value(ContextKeyUserID).(string); ok {
|
| 216 |
+
return userID
|
| 217 |
+
}
|
| 218 |
+
return ""
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// TenantIDFromContext extracts tenant ID from context
|
| 222 |
+
func TenantIDFromContext(ctx context.Context) string {
|
| 223 |
+
if tenantID, ok := ctx.Value(ContextKeyTenantID).(string); ok {
|
| 224 |
+
return tenantID
|
| 225 |
+
}
|
| 226 |
+
return ""
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
// ClaimsFromContext extracts claims from context
|
| 230 |
+
func ClaimsFromContext(ctx context.Context) *JWTClaims {
|
| 231 |
+
if claims, ok := ctx.Value(ContextKeyClaims).(*JWTClaims); ok {
|
| 232 |
+
return claims
|
| 233 |
+
}
|
| 234 |
+
return nil
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// RolesFromContext extracts roles from context
|
| 238 |
+
func RolesFromContext(ctx context.Context) []string {
|
| 239 |
+
if roles, ok := ctx.Value(ContextKeyRoles).([]string); ok {
|
| 240 |
+
return roles
|
| 241 |
+
}
|
| 242 |
+
return nil
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// OPAClient is a client for Open Policy Agent
|
| 246 |
+
type OPAClient struct {
|
| 247 |
+
addr string
|
| 248 |
+
policy string
|
| 249 |
+
client *http.Client
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// NewOPAClient creates a new OPA client
|
| 253 |
+
func NewOPAClient(addr, policy string) *OPAClient {
|
| 254 |
+
return &OPAClient{
|
| 255 |
+
addr: addr,
|
| 256 |
+
policy: policy,
|
| 257 |
+
client: &http.Client{
|
| 258 |
+
Timeout: 5 * time.Second,
|
| 259 |
+
},
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
// AuthzInput represents the input to OPA authorization
|
| 264 |
+
type AuthzInput struct {
|
| 265 |
+
User string `json:"user"`
|
| 266 |
+
Roles []string `json:"roles"`
|
| 267 |
+
Action string `json:"action"`
|
| 268 |
+
Path string `json:"path"`
|
| 269 |
+
Tenant string `json:"tenant,omitempty"`
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// Authorize checks if the request is authorized
|
| 273 |
+
func (c *OPAClient) Authorize(ctx context.Context, input AuthzInput) (bool, error) {
|
| 274 |
+
// Build OPA query URL
|
| 275 |
+
url := fmt.Sprintf("%s/v1/data/%s", c.addr, strings.ReplaceAll(c.policy, "/", "."))
|
| 276 |
+
|
| 277 |
+
// Create request body
|
| 278 |
+
body := map[string]interface{}{
|
| 279 |
+
"input": input,
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
jsonBody, err := json.Marshal(body)
|
| 283 |
+
if err != nil {
|
| 284 |
+
return false, fmt.Errorf("failed to marshal OPA input: %w", err)
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// Create request
|
| 288 |
+
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody))
|
| 289 |
+
if err != nil {
|
| 290 |
+
return false, fmt.Errorf("failed to create OPA request: %w", err)
|
| 291 |
+
}
|
| 292 |
+
req.Header.Set("Content-Type", "application/json")
|
| 293 |
+
|
| 294 |
+
// Send request
|
| 295 |
+
resp, err := c.client.Do(req)
|
| 296 |
+
if err != nil {
|
| 297 |
+
return false, fmt.Errorf("OPA request failed: %w", err)
|
| 298 |
+
}
|
| 299 |
+
defer resp.Body.Close()
|
| 300 |
+
|
| 301 |
+
// Parse response
|
| 302 |
+
var result struct {
|
| 303 |
+
Result bool `json:"result"`
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
| 307 |
+
return false, fmt.Errorf("failed to decode OPA response: %w", err)
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
return result.Result, nil
|
| 311 |
+
}
|
internal/gateway/middleware/common.go
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"runtime/debug"
|
| 6 |
+
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
"go.uber.org/zap"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
// Recovery returns a middleware that recovers from panics
|
| 12 |
+
func Recovery(logger *zap.Logger) func(http.Handler) http.Handler {
|
| 13 |
+
return func(next http.Handler) http.Handler {
|
| 14 |
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 15 |
+
defer func() {
|
| 16 |
+
if rec := recover(); rec != nil {
|
| 17 |
+
// Log the panic
|
| 18 |
+
logger.Error("Panic recovered",
|
| 19 |
+
zap.Any("panic", rec),
|
| 20 |
+
zap.String("path", r.URL.Path),
|
| 21 |
+
zap.String("method", r.Method),
|
| 22 |
+
zap.String("stack", string(debug.Stack())),
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
// Return 500 error
|
| 26 |
+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
| 27 |
+
}
|
| 28 |
+
}()
|
| 29 |
+
|
| 30 |
+
next.ServeHTTP(w, r)
|
| 31 |
+
})
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// RequestID returns a middleware that adds a unique request ID
|
| 36 |
+
func RequestID() func(http.Handler) http.Handler {
|
| 37 |
+
return func(next http.Handler) http.Handler {
|
| 38 |
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 39 |
+
// Check if request ID already exists
|
| 40 |
+
requestID := r.Header.Get("X-Request-ID")
|
| 41 |
+
if requestID == "" {
|
| 42 |
+
requestID = uuid.New().String()
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Set request ID in response header
|
| 46 |
+
w.Header().Set("X-Request-ID", requestID)
|
| 47 |
+
|
| 48 |
+
next.ServeHTTP(w, r)
|
| 49 |
+
})
|
| 50 |
+
}
|
| 51 |
+
}
|
internal/gateway/middleware/cors.go
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package middleware provides HTTP middleware for the API Gateway.
|
| 2 |
+
package middleware
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"net/http"
|
| 6 |
+
"strconv"
|
| 7 |
+
"strings"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
// CORSConfig holds CORS middleware configuration
|
| 11 |
+
type CORSConfig struct {
|
| 12 |
+
AllowedOrigins []string
|
| 13 |
+
AllowedMethods []string
|
| 14 |
+
AllowedHeaders []string
|
| 15 |
+
ExposedHeaders []string
|
| 16 |
+
AllowCredentials bool
|
| 17 |
+
MaxAge int
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// CORSMiddleware handles Cross-Origin Resource Sharing
|
| 21 |
+
type CORSMiddleware struct {
|
| 22 |
+
config CORSConfig
|
| 23 |
+
allowedOrigins map[string]bool
|
| 24 |
+
allowAllOrigins bool
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// NewCORSMiddleware creates a new CORS middleware
|
| 28 |
+
func NewCORSMiddleware(cfg CORSConfig) *CORSMiddleware {
|
| 29 |
+
m := &CORSMiddleware{
|
| 30 |
+
config: cfg,
|
| 31 |
+
allowedOrigins: make(map[string]bool),
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
for _, origin := range cfg.AllowedOrigins {
|
| 35 |
+
if origin == "*" {
|
| 36 |
+
m.allowAllOrigins = true
|
| 37 |
+
break
|
| 38 |
+
}
|
| 39 |
+
m.allowedOrigins[origin] = true
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
return m
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Handler is the middleware handler function
|
| 46 |
+
func (m *CORSMiddleware) Handler(next http.Handler) http.Handler {
|
| 47 |
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 48 |
+
origin := r.Header.Get("Origin")
|
| 49 |
+
|
| 50 |
+
// Check if origin is allowed
|
| 51 |
+
if m.isOriginAllowed(origin) {
|
| 52 |
+
w.Header().Set("Access-Control-Allow-Origin", origin)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Set CORS headers
|
| 56 |
+
if m.config.AllowCredentials {
|
| 57 |
+
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if len(m.config.ExposedHeaders) > 0 {
|
| 61 |
+
w.Header().Set("Access-Control-Expose-Headers", strings.Join(m.config.ExposedHeaders, ", "))
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Security headers
|
| 65 |
+
m.setSecurityHeaders(w)
|
| 66 |
+
|
| 67 |
+
// Handle preflight
|
| 68 |
+
if r.Method == http.MethodOptions {
|
| 69 |
+
m.handlePreflight(w, r)
|
| 70 |
+
return
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
next.ServeHTTP(w, r)
|
| 74 |
+
})
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// HandlePreflight handles OPTIONS preflight requests
|
| 78 |
+
func (m *CORSMiddleware) HandlePreflight(w http.ResponseWriter, r *http.Request) {
|
| 79 |
+
origin := r.Header.Get("Origin")
|
| 80 |
+
|
| 81 |
+
if m.isOriginAllowed(origin) {
|
| 82 |
+
w.Header().Set("Access-Control-Allow-Origin", origin)
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
m.handlePreflight(w, r)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
func (m *CORSMiddleware) handlePreflight(w http.ResponseWriter, r *http.Request) {
|
| 89 |
+
// Allow methods
|
| 90 |
+
if len(m.config.AllowedMethods) > 0 {
|
| 91 |
+
w.Header().Set("Access-Control-Allow-Methods", strings.Join(m.config.AllowedMethods, ", "))
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Allow headers
|
| 95 |
+
if len(m.config.AllowedHeaders) > 0 {
|
| 96 |
+
w.Header().Set("Access-Control-Allow-Headers", strings.Join(m.config.AllowedHeaders, ", "))
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Max age
|
| 100 |
+
if m.config.MaxAge > 0 {
|
| 101 |
+
w.Header().Set("Access-Control-Max-Age", strconv.Itoa(m.config.MaxAge))
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// Credentials
|
| 105 |
+
if m.config.AllowCredentials {
|
| 106 |
+
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Security headers
|
| 110 |
+
m.setSecurityHeaders(w)
|
| 111 |
+
|
| 112 |
+
w.WriteHeader(http.StatusNoContent)
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
func (m *CORSMiddleware) isOriginAllowed(origin string) bool {
|
| 116 |
+
if origin == "" {
|
| 117 |
+
return false
|
| 118 |
+
}
|
| 119 |
+
if m.allowAllOrigins {
|
| 120 |
+
return true
|
| 121 |
+
}
|
| 122 |
+
return m.allowedOrigins[origin]
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
func (m *CORSMiddleware) setSecurityHeaders(w http.ResponseWriter) {
|
| 126 |
+
// Prevent MIME type sniffing
|
| 127 |
+
w.Header().Set("X-Content-Type-Options", "nosniff")
|
| 128 |
+
|
| 129 |
+
// Prevent clickjacking
|
| 130 |
+
w.Header().Set("X-Frame-Options", "DENY")
|
| 131 |
+
|
| 132 |
+
// XSS protection
|
| 133 |
+
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
| 134 |
+
|
| 135 |
+
// HSTS - force HTTPS
|
| 136 |
+
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
| 137 |
+
|
| 138 |
+
// Referrer policy
|
| 139 |
+
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
| 140 |
+
|
| 141 |
+
// Content Security Policy
|
| 142 |
+
w.Header().Set("Content-Security-Policy", "default-src 'self'")
|
| 143 |
+
}
|
internal/gateway/middleware/middleware_test.go
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware_test
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"net/http/httptest"
|
| 6 |
+
"testing"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/AmaniQuery/amaniquery/internal/gateway/middleware"
|
| 10 |
+
"go.uber.org/zap"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
// TestCORSMiddleware_AllowAllOrigins tests wildcard origin support
|
| 14 |
+
func TestCORSMiddleware_AllowAllOrigins(t *testing.T) {
|
| 15 |
+
cfg := middleware.CORSConfig{
|
| 16 |
+
AllowedOrigins: []string{"*"},
|
| 17 |
+
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
| 18 |
+
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
| 19 |
+
AllowCredentials: false,
|
| 20 |
+
MaxAge: 3600,
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
cors := middleware.NewCORSMiddleware(cfg)
|
| 24 |
+
|
| 25 |
+
handler := cors.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 26 |
+
w.WriteHeader(http.StatusOK)
|
| 27 |
+
}))
|
| 28 |
+
|
| 29 |
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 30 |
+
req.Header.Set("Origin", "https://example.com")
|
| 31 |
+
rec := httptest.NewRecorder()
|
| 32 |
+
|
| 33 |
+
handler.ServeHTTP(rec, req)
|
| 34 |
+
|
| 35 |
+
if rec.Header().Get("Access-Control-Allow-Origin") != "https://example.com" {
|
| 36 |
+
t.Errorf("Expected origin to be allowed, got: %s", rec.Header().Get("Access-Control-Allow-Origin"))
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// TestCORSMiddleware_SpecificOrigins tests specific origin allowlist
|
| 41 |
+
func TestCORSMiddleware_SpecificOrigins(t *testing.T) {
|
| 42 |
+
cfg := middleware.CORSConfig{
|
| 43 |
+
AllowedOrigins: []string{"https://allowed.com", "https://another.com"},
|
| 44 |
+
AllowedMethods: []string{"GET", "POST"},
|
| 45 |
+
AllowedHeaders: []string{"Content-Type"},
|
| 46 |
+
AllowCredentials: true,
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
cors := middleware.NewCORSMiddleware(cfg)
|
| 50 |
+
|
| 51 |
+
handler := cors.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 52 |
+
w.WriteHeader(http.StatusOK)
|
| 53 |
+
}))
|
| 54 |
+
|
| 55 |
+
testCases := []struct {
|
| 56 |
+
name string
|
| 57 |
+
origin string
|
| 58 |
+
shouldAllow bool
|
| 59 |
+
}{
|
| 60 |
+
{"allowed origin", "https://allowed.com", true},
|
| 61 |
+
{"another allowed", "https://another.com", true},
|
| 62 |
+
{"not allowed", "https://evil.com", false},
|
| 63 |
+
{"no origin", "", false},
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
for _, tc := range testCases {
|
| 67 |
+
t.Run(tc.name, func(t *testing.T) {
|
| 68 |
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 69 |
+
if tc.origin != "" {
|
| 70 |
+
req.Header.Set("Origin", tc.origin)
|
| 71 |
+
}
|
| 72 |
+
rec := httptest.NewRecorder()
|
| 73 |
+
|
| 74 |
+
handler.ServeHTTP(rec, req)
|
| 75 |
+
|
| 76 |
+
hasOrigin := rec.Header().Get("Access-Control-Allow-Origin") != ""
|
| 77 |
+
if hasOrigin != tc.shouldAllow {
|
| 78 |
+
t.Errorf("Origin %s: expected allowed=%v, got allowed=%v", tc.origin, tc.shouldAllow, hasOrigin)
|
| 79 |
+
}
|
| 80 |
+
})
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// TestCORSMiddleware_Preflight tests OPTIONS preflight handling
|
| 85 |
+
func TestCORSMiddleware_Preflight(t *testing.T) {
|
| 86 |
+
cfg := middleware.CORSConfig{
|
| 87 |
+
AllowedOrigins: []string{"https://allowed.com"},
|
| 88 |
+
AllowedMethods: []string{"GET", "POST", "PUT"},
|
| 89 |
+
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
| 90 |
+
AllowCredentials: true,
|
| 91 |
+
MaxAge: 7200,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
cors := middleware.NewCORSMiddleware(cfg)
|
| 95 |
+
|
| 96 |
+
handler := cors.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 97 |
+
t.Error("Handler should not be called for OPTIONS request")
|
| 98 |
+
}))
|
| 99 |
+
|
| 100 |
+
req := httptest.NewRequest(http.MethodOptions, "/test", nil)
|
| 101 |
+
req.Header.Set("Origin", "https://allowed.com")
|
| 102 |
+
req.Header.Set("Access-Control-Request-Method", "POST")
|
| 103 |
+
rec := httptest.NewRecorder()
|
| 104 |
+
|
| 105 |
+
handler.ServeHTTP(rec, req)
|
| 106 |
+
|
| 107 |
+
if rec.Code != http.StatusNoContent {
|
| 108 |
+
t.Errorf("Expected status %d, got %d", http.StatusNoContent, rec.Code)
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Check preflight headers
|
| 112 |
+
if rec.Header().Get("Access-Control-Allow-Methods") == "" {
|
| 113 |
+
t.Error("Expected Access-Control-Allow-Methods header")
|
| 114 |
+
}
|
| 115 |
+
if rec.Header().Get("Access-Control-Allow-Headers") == "" {
|
| 116 |
+
t.Error("Expected Access-Control-Allow-Headers header")
|
| 117 |
+
}
|
| 118 |
+
if rec.Header().Get("Access-Control-Max-Age") != "7200" {
|
| 119 |
+
t.Errorf("Expected Max-Age 7200, got %s", rec.Header().Get("Access-Control-Max-Age"))
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// TestCORSMiddleware_SecurityHeaders tests security headers are set
|
| 124 |
+
func TestCORSMiddleware_SecurityHeaders(t *testing.T) {
|
| 125 |
+
cfg := middleware.CORSConfig{
|
| 126 |
+
AllowedOrigins: []string{"*"},
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
cors := middleware.NewCORSMiddleware(cfg)
|
| 130 |
+
|
| 131 |
+
handler := cors.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 132 |
+
w.WriteHeader(http.StatusOK)
|
| 133 |
+
}))
|
| 134 |
+
|
| 135 |
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 136 |
+
req.Header.Set("Origin", "https://example.com")
|
| 137 |
+
rec := httptest.NewRecorder()
|
| 138 |
+
|
| 139 |
+
handler.ServeHTTP(rec, req)
|
| 140 |
+
|
| 141 |
+
securityHeaders := []string{
|
| 142 |
+
"X-Content-Type-Options",
|
| 143 |
+
"X-Frame-Options",
|
| 144 |
+
"X-XSS-Protection",
|
| 145 |
+
"Strict-Transport-Security",
|
| 146 |
+
"Referrer-Policy",
|
| 147 |
+
"Content-Security-Policy",
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
for _, header := range securityHeaders {
|
| 151 |
+
if rec.Header().Get(header) == "" {
|
| 152 |
+
t.Errorf("Expected security header %s to be set", header)
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// TestCORSMiddleware_Credentials tests credentials header
|
| 158 |
+
func TestCORSMiddleware_Credentials(t *testing.T) {
|
| 159 |
+
cfg := middleware.CORSConfig{
|
| 160 |
+
AllowedOrigins: []string{"https://allowed.com"},
|
| 161 |
+
AllowCredentials: true,
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
cors := middleware.NewCORSMiddleware(cfg)
|
| 165 |
+
|
| 166 |
+
handler := cors.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 167 |
+
w.WriteHeader(http.StatusOK)
|
| 168 |
+
}))
|
| 169 |
+
|
| 170 |
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 171 |
+
req.Header.Set("Origin", "https://allowed.com")
|
| 172 |
+
rec := httptest.NewRecorder()
|
| 173 |
+
|
| 174 |
+
handler.ServeHTTP(rec, req)
|
| 175 |
+
|
| 176 |
+
if rec.Header().Get("Access-Control-Allow-Credentials") != "true" {
|
| 177 |
+
t.Error("Expected Access-Control-Allow-Credentials to be true")
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// TestRateLimitMiddleware_LocalRateLimit tests local rate limiting
|
| 182 |
+
func TestRateLimitMiddleware_LocalRateLimit(t *testing.T) {
|
| 183 |
+
logger, _ := zap.NewDevelopment()
|
| 184 |
+
cfg := middleware.RateLimitConfig{
|
| 185 |
+
RequestsPerSec: 1, // 1 request per second
|
| 186 |
+
BurstSize: 1, // Allow 1 request burst
|
| 187 |
+
CleanupInterval: time.Minute,
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
rl, err := middleware.NewRateLimitMiddleware(cfg, logger)
|
| 191 |
+
if err != nil {
|
| 192 |
+
t.Fatalf("Failed to create rate limit middleware: %v", err)
|
| 193 |
+
}
|
| 194 |
+
defer rl.Close()
|
| 195 |
+
|
| 196 |
+
handler := rl.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 197 |
+
w.WriteHeader(http.StatusOK)
|
| 198 |
+
}))
|
| 199 |
+
|
| 200 |
+
// First request should succeed (uses burst token)
|
| 201 |
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 202 |
+
req.RemoteAddr = "127.0.0.1:12345"
|
| 203 |
+
rec := httptest.NewRecorder()
|
| 204 |
+
handler.ServeHTTP(rec, req)
|
| 205 |
+
|
| 206 |
+
if rec.Code != http.StatusOK {
|
| 207 |
+
t.Errorf("First request should be allowed, got status %d", rec.Code)
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// Second request immediately after should be rate limited
|
| 211 |
+
req2 := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 212 |
+
req2.RemoteAddr = "127.0.0.1:12345"
|
| 213 |
+
rec2 := httptest.NewRecorder()
|
| 214 |
+
handler.ServeHTTP(rec2, req2)
|
| 215 |
+
|
| 216 |
+
if rec2.Code != http.StatusTooManyRequests {
|
| 217 |
+
t.Errorf("Expected status %d (rate limited), got %d", http.StatusTooManyRequests, rec2.Code)
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// TestRateLimitMiddleware_Headers tests rate limit headers
|
| 222 |
+
func TestRateLimitMiddleware_Headers(t *testing.T) {
|
| 223 |
+
logger, _ := zap.NewDevelopment()
|
| 224 |
+
cfg := middleware.RateLimitConfig{
|
| 225 |
+
RequestsPerSec: 10,
|
| 226 |
+
BurstSize: 10,
|
| 227 |
+
CleanupInterval: time.Minute,
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
rl, err := middleware.NewRateLimitMiddleware(cfg, logger)
|
| 231 |
+
if err != nil {
|
| 232 |
+
t.Fatalf("Failed to create rate limit middleware: %v", err)
|
| 233 |
+
}
|
| 234 |
+
defer rl.Close()
|
| 235 |
+
|
| 236 |
+
handler := rl.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 237 |
+
w.WriteHeader(http.StatusOK)
|
| 238 |
+
}))
|
| 239 |
+
|
| 240 |
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 241 |
+
req.RemoteAddr = "127.0.0.1:12345"
|
| 242 |
+
rec := httptest.NewRecorder()
|
| 243 |
+
|
| 244 |
+
handler.ServeHTTP(rec, req)
|
| 245 |
+
|
| 246 |
+
// Check rate limit headers
|
| 247 |
+
if rec.Header().Get("X-RateLimit-Limit") == "" {
|
| 248 |
+
t.Error("Expected X-RateLimit-Limit header")
|
| 249 |
+
}
|
| 250 |
+
if rec.Header().Get("X-RateLimit-Remaining") == "" {
|
| 251 |
+
t.Error("Expected X-RateLimit-Remaining header")
|
| 252 |
+
}
|
| 253 |
+
if rec.Header().Get("X-RateLimit-Reset") == "" {
|
| 254 |
+
t.Error("Expected X-RateLimit-Reset header")
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// TestRateLimitMiddleware_DifferentIPs tests per-IP rate limiting
|
| 259 |
+
func TestRateLimitMiddleware_DifferentIPs(t *testing.T) {
|
| 260 |
+
logger, _ := zap.NewDevelopment()
|
| 261 |
+
cfg := middleware.RateLimitConfig{
|
| 262 |
+
RequestsPerSec: 1,
|
| 263 |
+
BurstSize: 1,
|
| 264 |
+
CleanupInterval: time.Minute,
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
rl, err := middleware.NewRateLimitMiddleware(cfg, logger)
|
| 268 |
+
if err != nil {
|
| 269 |
+
t.Fatalf("Failed to create rate limit middleware: %v", err)
|
| 270 |
+
}
|
| 271 |
+
defer rl.Close()
|
| 272 |
+
|
| 273 |
+
handler := rl.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 274 |
+
w.WriteHeader(http.StatusOK)
|
| 275 |
+
}))
|
| 276 |
+
|
| 277 |
+
// First IP exhausts its limit
|
| 278 |
+
req1 := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 279 |
+
req1.RemoteAddr = "10.0.0.1:12345"
|
| 280 |
+
rec1 := httptest.NewRecorder()
|
| 281 |
+
handler.ServeHTTP(rec1, req1)
|
| 282 |
+
|
| 283 |
+
if rec1.Code != http.StatusOK {
|
| 284 |
+
t.Errorf("First request from IP1 should succeed, got %d", rec1.Code)
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// Second request from same IP should be limited
|
| 288 |
+
req2 := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 289 |
+
req2.RemoteAddr = "10.0.0.1:12345"
|
| 290 |
+
rec2 := httptest.NewRecorder()
|
| 291 |
+
handler.ServeHTTP(rec2, req2)
|
| 292 |
+
|
| 293 |
+
if rec2.Code != http.StatusTooManyRequests {
|
| 294 |
+
t.Errorf("Second request from IP1 should be limited, got %d", rec2.Code)
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
// Different IP should still be allowed
|
| 298 |
+
req3 := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 299 |
+
req3.RemoteAddr = "10.0.0.2:12345"
|
| 300 |
+
rec3 := httptest.NewRecorder()
|
| 301 |
+
handler.ServeHTTP(rec3, req3)
|
| 302 |
+
|
| 303 |
+
if rec3.Code != http.StatusOK {
|
| 304 |
+
t.Errorf("First request from IP2 should succeed, got %d", rec3.Code)
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
// TestRateLimitMiddleware_XForwardedFor tests X-Forwarded-For handling
|
| 309 |
+
func TestRateLimitMiddleware_XForwardedFor(t *testing.T) {
|
| 310 |
+
logger, _ := zap.NewDevelopment()
|
| 311 |
+
cfg := middleware.RateLimitConfig{
|
| 312 |
+
RequestsPerSec: 1,
|
| 313 |
+
BurstSize: 1,
|
| 314 |
+
CleanupInterval: time.Minute,
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
rl, err := middleware.NewRateLimitMiddleware(cfg, logger)
|
| 318 |
+
if err != nil {
|
| 319 |
+
t.Fatalf("Failed to create rate limit middleware: %v", err)
|
| 320 |
+
}
|
| 321 |
+
defer rl.Close()
|
| 322 |
+
|
| 323 |
+
handler := rl.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 324 |
+
w.WriteHeader(http.StatusOK)
|
| 325 |
+
}))
|
| 326 |
+
|
| 327 |
+
// Request with X-Forwarded-For header
|
| 328 |
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 329 |
+
req.RemoteAddr = "127.0.0.1:12345"
|
| 330 |
+
req.Header.Set("X-Forwarded-For", "203.0.113.195, 70.41.3.18, 150.172.238.178")
|
| 331 |
+
rec := httptest.NewRecorder()
|
| 332 |
+
|
| 333 |
+
handler.ServeHTTP(rec, req)
|
| 334 |
+
|
| 335 |
+
if rec.Code != http.StatusOK {
|
| 336 |
+
t.Errorf("Request should succeed, got %d", rec.Code)
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
// BenchmarkCORSMiddleware benchmarks CORS middleware
|
| 341 |
+
func BenchmarkCORSMiddleware(b *testing.B) {
|
| 342 |
+
cfg := middleware.CORSConfig{
|
| 343 |
+
AllowedOrigins: []string{"https://example.com", "https://test.com"},
|
| 344 |
+
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
| 345 |
+
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
cors := middleware.NewCORSMiddleware(cfg)
|
| 349 |
+
|
| 350 |
+
handler := cors.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 351 |
+
w.WriteHeader(http.StatusOK)
|
| 352 |
+
}))
|
| 353 |
+
|
| 354 |
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 355 |
+
req.Header.Set("Origin", "https://example.com")
|
| 356 |
+
|
| 357 |
+
b.ResetTimer()
|
| 358 |
+
for i := 0; i < b.N; i++ {
|
| 359 |
+
rec := httptest.NewRecorder()
|
| 360 |
+
handler.ServeHTTP(rec, req)
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// BenchmarkRateLimitMiddleware benchmarks rate limit middleware
|
| 365 |
+
func BenchmarkRateLimitMiddleware(b *testing.B) {
|
| 366 |
+
logger, _ := zap.NewProduction()
|
| 367 |
+
cfg := middleware.RateLimitConfig{
|
| 368 |
+
RequestsPerSec: 1000000, // High limit for benchmarking
|
| 369 |
+
BurstSize: 1000000,
|
| 370 |
+
CleanupInterval: time.Minute,
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
rl, _ := middleware.NewRateLimitMiddleware(cfg, logger)
|
| 374 |
+
defer rl.Close()
|
| 375 |
+
|
| 376 |
+
handler := rl.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 377 |
+
w.WriteHeader(http.StatusOK)
|
| 378 |
+
}))
|
| 379 |
+
|
| 380 |
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
| 381 |
+
req.RemoteAddr = "127.0.0.1:12345"
|
| 382 |
+
|
| 383 |
+
b.ResetTimer()
|
| 384 |
+
for i := 0; i < b.N; i++ {
|
| 385 |
+
rec := httptest.NewRecorder()
|
| 386 |
+
handler.ServeHTTP(rec, req)
|
| 387 |
+
}
|
| 388 |
+
}
|
internal/gateway/middleware/ratelimit.go
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"net/http"
|
| 7 |
+
"sync"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"github.com/redis/go-redis/v9"
|
| 11 |
+
"go.uber.org/zap"
|
| 12 |
+
"golang.org/x/time/rate"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
// RateLimitConfig holds rate limiting configuration
|
| 16 |
+
type RateLimitConfig struct {
|
| 17 |
+
RequestsPerSec float64
|
| 18 |
+
BurstSize int
|
| 19 |
+
PerTenant bool
|
| 20 |
+
PerUser bool
|
| 21 |
+
RedisAddr string
|
| 22 |
+
RedisEnabled bool
|
| 23 |
+
CleanupInterval time.Duration
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// RateLimitMiddleware implements token bucket rate limiting
|
| 27 |
+
type RateLimitMiddleware struct {
|
| 28 |
+
config RateLimitConfig
|
| 29 |
+
limiters sync.Map // map[string]*rate.Limiter
|
| 30 |
+
redis *redis.Client
|
| 31 |
+
logger *zap.Logger
|
| 32 |
+
stop chan struct{}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// NewRateLimitMiddleware creates a new rate limit middleware
|
| 36 |
+
func NewRateLimitMiddleware(cfg RateLimitConfig, logger *zap.Logger) (*RateLimitMiddleware, error) {
|
| 37 |
+
m := &RateLimitMiddleware{
|
| 38 |
+
config: cfg,
|
| 39 |
+
logger: logger,
|
| 40 |
+
stop: make(chan struct{}),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Initialize Redis if enabled
|
| 44 |
+
if cfg.RedisEnabled && cfg.RedisAddr != "" {
|
| 45 |
+
m.redis = redis.NewClient(&redis.Options{
|
| 46 |
+
Addr: cfg.RedisAddr,
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
| 50 |
+
defer cancel()
|
| 51 |
+
|
| 52 |
+
if err := m.redis.Ping(ctx).Err(); err != nil {
|
| 53 |
+
logger.Warn("Redis ping failed, using local rate limiting", zap.Error(err))
|
| 54 |
+
m.redis = nil
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Start cleanup goroutine for local limiters
|
| 59 |
+
if cfg.CleanupInterval > 0 {
|
| 60 |
+
go m.cleanupLoop()
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return m, nil
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Handler is the middleware handler function
|
| 67 |
+
func (m *RateLimitMiddleware) Handler(next http.Handler) http.Handler {
|
| 68 |
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 69 |
+
// Get rate limit key
|
| 70 |
+
key := m.getKey(r)
|
| 71 |
+
|
| 72 |
+
// Check rate limit
|
| 73 |
+
allowed, remaining, reset := m.checkLimit(r.Context(), key)
|
| 74 |
+
|
| 75 |
+
// Set rate limit headers
|
| 76 |
+
w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%.0f", m.config.RequestsPerSec))
|
| 77 |
+
w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", remaining))
|
| 78 |
+
w.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", reset))
|
| 79 |
+
|
| 80 |
+
if !allowed {
|
| 81 |
+
w.Header().Set("Retry-After", fmt.Sprintf("%d", reset-time.Now().Unix()))
|
| 82 |
+
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
| 83 |
+
return
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
next.ServeHTTP(w, r)
|
| 87 |
+
})
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
func (m *RateLimitMiddleware) getKey(r *http.Request) string {
|
| 91 |
+
var parts []string
|
| 92 |
+
|
| 93 |
+
// Get tenant from context
|
| 94 |
+
if m.config.PerTenant {
|
| 95 |
+
if tenantID := r.Context().Value(ContextKeyTenantID); tenantID != nil {
|
| 96 |
+
parts = append(parts, fmt.Sprintf("tenant:%s", tenantID))
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Get user from context
|
| 101 |
+
if m.config.PerUser {
|
| 102 |
+
if userID := r.Context().Value(ContextKeyUserID); userID != nil {
|
| 103 |
+
parts = append(parts, fmt.Sprintf("user:%s", userID))
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Fall back to IP if no tenant/user
|
| 108 |
+
if len(parts) == 0 {
|
| 109 |
+
parts = append(parts, fmt.Sprintf("ip:%s", getClientIP(r)))
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
key := ""
|
| 113 |
+
for i, part := range parts {
|
| 114 |
+
if i > 0 {
|
| 115 |
+
key += ":"
|
| 116 |
+
}
|
| 117 |
+
key += part
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
return key
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
func (m *RateLimitMiddleware) checkLimit(ctx context.Context, key string) (allowed bool, remaining int, reset int64) {
|
| 124 |
+
// Use Redis for distributed rate limiting if available
|
| 125 |
+
if m.redis != nil {
|
| 126 |
+
return m.checkRedisLimit(ctx, key)
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Fall back to local rate limiting
|
| 130 |
+
return m.checkLocalLimit(key)
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
func (m *RateLimitMiddleware) checkLocalLimit(key string) (allowed bool, remaining int, reset int64) {
|
| 134 |
+
// Get or create limiter for this key
|
| 135 |
+
limiterI, _ := m.limiters.LoadOrStore(key, &limiterEntry{
|
| 136 |
+
limiter: rate.NewLimiter(rate.Limit(m.config.RequestsPerSec), m.config.BurstSize),
|
| 137 |
+
lastAccess: time.Now(),
|
| 138 |
+
})
|
| 139 |
+
|
| 140 |
+
entry := limiterI.(*limiterEntry)
|
| 141 |
+
entry.lastAccess = time.Now()
|
| 142 |
+
|
| 143 |
+
// Check if request is allowed
|
| 144 |
+
allowed = entry.limiter.Allow()
|
| 145 |
+
|
| 146 |
+
// Calculate remaining tokens (approximate)
|
| 147 |
+
tokens := entry.limiter.Tokens()
|
| 148 |
+
if tokens < 0 {
|
| 149 |
+
remaining = 0
|
| 150 |
+
} else {
|
| 151 |
+
remaining = int(tokens)
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
// Reset time is when a token will be available
|
| 155 |
+
reset = time.Now().Add(entry.limiter.Reserve().Delay()).Unix()
|
| 156 |
+
|
| 157 |
+
return allowed, remaining, reset
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
func (m *RateLimitMiddleware) checkRedisLimit(ctx context.Context, key string) (allowed bool, remaining int, reset int64) {
|
| 161 |
+
redisKey := fmt.Sprintf("ratelimit:%s", key)
|
| 162 |
+
now := time.Now()
|
| 163 |
+
windowStart := now.Truncate(time.Second)
|
| 164 |
+
windowEnd := windowStart.Add(time.Second)
|
| 165 |
+
|
| 166 |
+
// Use Redis MULTI/EXEC for atomic operations
|
| 167 |
+
pipe := m.redis.Pipeline()
|
| 168 |
+
|
| 169 |
+
// Increment counter
|
| 170 |
+
incrCmd := pipe.Incr(ctx, redisKey)
|
| 171 |
+
pipe.ExpireAt(ctx, redisKey, windowEnd.Add(time.Second))
|
| 172 |
+
|
| 173 |
+
_, err := pipe.Exec(ctx)
|
| 174 |
+
if err != nil {
|
| 175 |
+
m.logger.Warn("Redis rate limit check failed", zap.Error(err))
|
| 176 |
+
// Fall back to local limiting on Redis error
|
| 177 |
+
return m.checkLocalLimit(key)
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
count := incrCmd.Val()
|
| 181 |
+
limit := int64(m.config.RequestsPerSec)
|
| 182 |
+
|
| 183 |
+
allowed = count <= limit
|
| 184 |
+
remaining = int(limit - count)
|
| 185 |
+
if remaining < 0 {
|
| 186 |
+
remaining = 0
|
| 187 |
+
}
|
| 188 |
+
reset = windowEnd.Unix()
|
| 189 |
+
|
| 190 |
+
return allowed, remaining, reset
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
type limiterEntry struct {
|
| 194 |
+
limiter *rate.Limiter
|
| 195 |
+
lastAccess time.Time
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
func (m *RateLimitMiddleware) cleanupLoop() {
|
| 199 |
+
ticker := time.NewTicker(m.config.CleanupInterval)
|
| 200 |
+
defer ticker.Stop()
|
| 201 |
+
|
| 202 |
+
for {
|
| 203 |
+
select {
|
| 204 |
+
case <-ticker.C:
|
| 205 |
+
m.cleanup()
|
| 206 |
+
case <-m.stop:
|
| 207 |
+
return
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
func (m *RateLimitMiddleware) cleanup() {
|
| 213 |
+
expiry := time.Now().Add(-m.config.CleanupInterval * 2)
|
| 214 |
+
|
| 215 |
+
m.limiters.Range(func(key, value interface{}) bool {
|
| 216 |
+
entry := value.(*limiterEntry)
|
| 217 |
+
if entry.lastAccess.Before(expiry) {
|
| 218 |
+
m.limiters.Delete(key)
|
| 219 |
+
}
|
| 220 |
+
return true
|
| 221 |
+
})
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// Close stops the cleanup goroutine
|
| 225 |
+
func (m *RateLimitMiddleware) Close() {
|
| 226 |
+
close(m.stop)
|
| 227 |
+
if m.redis != nil {
|
| 228 |
+
m.redis.Close()
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
func getClientIP(r *http.Request) string {
|
| 233 |
+
// Check X-Forwarded-For header
|
| 234 |
+
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
| 235 |
+
// Take the first IP in the chain
|
| 236 |
+
if idx := len(xff); idx > 0 {
|
| 237 |
+
for i, c := range xff {
|
| 238 |
+
if c == ',' {
|
| 239 |
+
return xff[:i]
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
return xff
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
// Check X-Real-IP header
|
| 247 |
+
if xrip := r.Header.Get("X-Real-IP"); xrip != "" {
|
| 248 |
+
return xrip
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
// Fall back to remote address
|
| 252 |
+
return r.RemoteAddr
|
| 253 |
+
}
|
internal/gateway/middleware/tracing.go
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
|
| 6 |
+
"go.opentelemetry.io/otel/attribute"
|
| 7 |
+
"go.opentelemetry.io/otel/trace"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
// TracingMiddleware adds OpenTelemetry tracing to requests
|
| 11 |
+
type TracingMiddleware struct {
|
| 12 |
+
tracer trace.Tracer
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
// NewTracingMiddleware creates a new tracing middleware
|
| 16 |
+
func NewTracingMiddleware(tracer trace.Tracer) *TracingMiddleware {
|
| 17 |
+
return &TracingMiddleware{
|
| 18 |
+
tracer: tracer,
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// Handler is the middleware handler function
|
| 23 |
+
func (m *TracingMiddleware) Handler(next http.Handler) http.Handler {
|
| 24 |
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 25 |
+
// Start span
|
| 26 |
+
ctx, span := m.tracer.Start(r.Context(), r.URL.Path,
|
| 27 |
+
trace.WithSpanKind(trace.SpanKindServer),
|
| 28 |
+
)
|
| 29 |
+
defer span.End()
|
| 30 |
+
|
| 31 |
+
// Add request attributes
|
| 32 |
+
span.SetAttributes(
|
| 33 |
+
attribute.String("http.method", r.Method),
|
| 34 |
+
attribute.String("http.url", r.URL.String()),
|
| 35 |
+
attribute.String("http.host", r.Host),
|
| 36 |
+
attribute.String("http.user_agent", r.UserAgent()),
|
| 37 |
+
attribute.String("http.remote_addr", r.RemoteAddr),
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
// Add request ID if present
|
| 41 |
+
if requestID := r.Header.Get("X-Request-ID"); requestID != "" {
|
| 42 |
+
span.SetAttributes(attribute.String("http.request_id", requestID))
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Wrap response writer to capture status
|
| 46 |
+
rw := &tracingResponseWriter{
|
| 47 |
+
ResponseWriter: w,
|
| 48 |
+
statusCode: http.StatusOK,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Call next handler with traced context
|
| 52 |
+
next.ServeHTTP(rw, r.WithContext(ctx))
|
| 53 |
+
|
| 54 |
+
// Add response attributes
|
| 55 |
+
span.SetAttributes(
|
| 56 |
+
attribute.Int("http.status_code", rw.statusCode),
|
| 57 |
+
attribute.Int("http.response_size", rw.size),
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
// Add user context if available
|
| 61 |
+
if userID := UserIDFromContext(ctx); userID != "" {
|
| 62 |
+
span.SetAttributes(attribute.String("user.id", userID))
|
| 63 |
+
}
|
| 64 |
+
if tenantID := TenantIDFromContext(ctx); tenantID != "" {
|
| 65 |
+
span.SetAttributes(attribute.String("tenant.id", tenantID))
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// Mark error status
|
| 69 |
+
if rw.statusCode >= 400 {
|
| 70 |
+
span.SetAttributes(attribute.Bool("error", true))
|
| 71 |
+
}
|
| 72 |
+
})
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
type tracingResponseWriter struct {
|
| 76 |
+
http.ResponseWriter
|
| 77 |
+
statusCode int
|
| 78 |
+
size int
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
func (rw *tracingResponseWriter) WriteHeader(code int) {
|
| 82 |
+
rw.statusCode = code
|
| 83 |
+
rw.ResponseWriter.WriteHeader(code)
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
func (rw *tracingResponseWriter) Write(b []byte) (int, error) {
|
| 87 |
+
size, err := rw.ResponseWriter.Write(b)
|
| 88 |
+
rw.size += size
|
| 89 |
+
return size, err
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
func (rw *tracingResponseWriter) Flush() {
|
| 93 |
+
if f, ok := rw.ResponseWriter.(http.Flusher); ok {
|
| 94 |
+
f.Flush()
|
| 95 |
+
}
|
| 96 |
+
}
|
internal/gateway/observability/metrics.go
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package observability provides metrics for the API Gateway.
|
| 2 |
+
package observability
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"github.com/prometheus/client_golang/prometheus"
|
| 6 |
+
"github.com/prometheus/client_golang/prometheus/promauto"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
var (
|
| 10 |
+
// HTTPRequestsTotal counts total HTTP requests
|
| 11 |
+
HTTPRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
| 12 |
+
Name: "gateway_http_requests_total",
|
| 13 |
+
Help: "Total HTTP requests",
|
| 14 |
+
}, []string{"method", "path", "status", "cached"})
|
| 15 |
+
|
| 16 |
+
// HTTPRequestDuration tracks request latency
|
| 17 |
+
HTTPRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
| 18 |
+
Name: "gateway_http_request_duration_seconds",
|
| 19 |
+
Help: "HTTP request duration in seconds",
|
| 20 |
+
Buckets: prometheus.DefBuckets,
|
| 21 |
+
}, []string{"method", "path"})
|
| 22 |
+
|
| 23 |
+
// WebSocketConnections tracks active connections
|
| 24 |
+
WebSocketConnections = promauto.NewGauge(prometheus.GaugeOpts{
|
| 25 |
+
Name: "gateway_websocket_connections_active",
|
| 26 |
+
Help: "Active WebSocket connections",
|
| 27 |
+
})
|
| 28 |
+
|
| 29 |
+
// RateLimitHits counts rate limit violations
|
| 30 |
+
RateLimitHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
| 31 |
+
Name: "gateway_rate_limit_hits_total",
|
| 32 |
+
Help: "Total rate limit hits",
|
| 33 |
+
}, []string{"tenant", "user"})
|
| 34 |
+
|
| 35 |
+
// CacheHits tracks cache performance
|
| 36 |
+
CacheHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
| 37 |
+
Name: "gateway_cache_hits_total",
|
| 38 |
+
Help: "Cache hit/miss counts",
|
| 39 |
+
}, []string{"hit"})
|
| 40 |
+
|
| 41 |
+
// CircuitBreakerState tracks circuit breaker states
|
| 42 |
+
CircuitBreakerState = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
| 43 |
+
Name: "gateway_circuit_breaker_state",
|
| 44 |
+
Help: "Circuit breaker state (0=closed, 1=open, 2=half-open)",
|
| 45 |
+
}, []string{"service"})
|
| 46 |
+
|
| 47 |
+
// JWTValidationErrors counts auth failures
|
| 48 |
+
JWTValidationErrors = promauto.NewCounter(prometheus.CounterOpts{
|
| 49 |
+
Name: "gateway_jwt_validation_errors_total",
|
| 50 |
+
Help: "JWT validation errors",
|
| 51 |
+
})
|
| 52 |
+
|
| 53 |
+
// QueryLatency tracks query processing time
|
| 54 |
+
QueryLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
| 55 |
+
Name: "gateway_query_latency_seconds",
|
| 56 |
+
Help: "Query processing latency",
|
| 57 |
+
Buckets: []float64{.01, .05, .1, .25, .5, 1, 2.5, 5, 10},
|
| 58 |
+
}, []string{"agent_id", "cached"})
|
| 59 |
+
|
| 60 |
+
// StreamingChunks counts streamed chunks
|
| 61 |
+
StreamingChunks = promauto.NewCounterVec(prometheus.CounterOpts{
|
| 62 |
+
Name: "gateway_streaming_chunks_total",
|
| 63 |
+
Help: "Total streaming chunks sent",
|
| 64 |
+
}, []string{"type"})
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
// RecordRequest records HTTP request metrics
|
| 68 |
+
func RecordRequest(method, path, status string, cached bool, duration float64) {
|
| 69 |
+
cachedStr := "false"
|
| 70 |
+
if cached {
|
| 71 |
+
cachedStr = "true"
|
| 72 |
+
}
|
| 73 |
+
HTTPRequestsTotal.WithLabelValues(method, path, status, cachedStr).Inc()
|
| 74 |
+
HTTPRequestDuration.WithLabelValues(method, path).Observe(duration)
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// RecordCacheHit records cache hit/miss
|
| 78 |
+
func RecordCacheHit(hit bool) {
|
| 79 |
+
hitStr := "false"
|
| 80 |
+
if hit {
|
| 81 |
+
hitStr = "true"
|
| 82 |
+
}
|
| 83 |
+
CacheHits.WithLabelValues(hitStr).Inc()
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// RecordRateLimit records rate limit event
|
| 87 |
+
func RecordRateLimit(tenant, user string) {
|
| 88 |
+
RateLimitHits.WithLabelValues(tenant, user).Inc()
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// SetCircuitBreakerState sets circuit breaker gauge
|
| 92 |
+
func SetCircuitBreakerState(service string, state int) {
|
| 93 |
+
CircuitBreakerState.WithLabelValues(service).Set(float64(state))
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// IncrementWSConnections increments active WS connections
|
| 97 |
+
func IncrementWSConnections() {
|
| 98 |
+
WebSocketConnections.Inc()
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// DecrementWSConnections decrements active WS connections
|
| 102 |
+
func DecrementWSConnections() {
|
| 103 |
+
WebSocketConnections.Dec()
|
| 104 |
+
}
|