Compare commits
9 Commits
docs/compr
...
dd8be05e9c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd8be05e9c | ||
|
|
df5ec34b4f | ||
|
|
fe6765afea | ||
|
|
511e52a05c | ||
|
|
f7130b327c | ||
|
|
7381137db5 | ||
|
|
9f480986fa | ||
|
|
4d424764e5 | ||
|
|
63dab5c4d4 |
@@ -1,3 +1,19 @@
|
||||
# ⚠️ DEPRECATED: DO NOT USE THIS DOCKERFILE ⚠️
|
||||
#
|
||||
# This Alpine-based Dockerfile is INCOMPATIBLE with the chorus-agent binary
|
||||
# built by 'make build-agent'. The binary is compiled with glibc dependencies
|
||||
# and will NOT run on Alpine's musl libc.
|
||||
#
|
||||
# ERROR when used: "exec /app/chorus-agent: no such file or directory"
|
||||
#
|
||||
# ✅ USE Dockerfile.ubuntu INSTEAD
|
||||
#
|
||||
# This file is kept for reference only and should not be used for builds.
|
||||
# Last failed: 2025-10-01
|
||||
# Reason: Alpine musl libc incompatibility with glibc-linked binary
|
||||
#
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
# CHORUS - Simple Docker image using pre-built binary
|
||||
FROM alpine:3.18
|
||||
|
||||
5
Makefile
5
Makefile
@@ -90,10 +90,13 @@ run-hap: build-hap
|
||||
./$(BUILD_DIR)/$(BINARY_NAME_HAP)
|
||||
|
||||
# Docker builds
|
||||
# NOTE: Always use Dockerfile.ubuntu for production builds!
|
||||
# Dockerfile.simple.DEPRECATED uses Alpine which is incompatible with glibc-linked binaries
|
||||
.PHONY: docker-agent
|
||||
docker-agent:
|
||||
@echo "🐳 Building Docker image for CHORUS agent..."
|
||||
docker build -f docker/Dockerfile.agent -t chorus-agent:$(VERSION) .
|
||||
docker build -f Dockerfile.ubuntu -t chorus-agent:$(VERSION) .
|
||||
@echo "⚠️ IMPORTANT: Production images MUST use Dockerfile.ubuntu (glibc compatibility)"
|
||||
|
||||
.PHONY: docker-hap
|
||||
docker-hap:
|
||||
|
||||
@@ -4,10 +4,15 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chorus/internal/council"
|
||||
"chorus/internal/logging"
|
||||
"chorus/p2p"
|
||||
"chorus/pkg/config"
|
||||
"chorus/pubsub"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -15,19 +20,96 @@ import (
|
||||
|
||||
// HTTPServer provides HTTP API endpoints for CHORUS
|
||||
type HTTPServer struct {
|
||||
port int
|
||||
hypercoreLog *logging.HypercoreLog
|
||||
pubsub *pubsub.PubSub
|
||||
server *http.Server
|
||||
port int
|
||||
hypercoreLog *logging.HypercoreLog
|
||||
pubsub *pubsub.PubSub
|
||||
server *http.Server
|
||||
CouncilManager *council.Manager // Exported for brief processing
|
||||
whooshEndpoint string
|
||||
}
|
||||
|
||||
// NewHTTPServer creates a new HTTP server for CHORUS API
|
||||
func NewHTTPServer(port int, hlog *logging.HypercoreLog, ps *pubsub.PubSub) *HTTPServer {
|
||||
return &HTTPServer{
|
||||
port: port,
|
||||
hypercoreLog: hlog,
|
||||
pubsub: ps,
|
||||
func NewHTTPServer(cfg *config.Config, node *p2p.Node, hlog *logging.HypercoreLog, ps *pubsub.PubSub) *HTTPServer {
|
||||
agentID := cfg.Agent.ID
|
||||
agentName := deriveAgentName(cfg)
|
||||
endpoint := deriveAgentEndpoint(cfg)
|
||||
p2pAddr := deriveAgentP2PAddress(cfg, node)
|
||||
capabilities := cfg.Agent.Capabilities
|
||||
if len(capabilities) == 0 {
|
||||
capabilities = []string{"general_development", "task_coordination"}
|
||||
}
|
||||
|
||||
councilMgr := council.NewManager(agentID, agentName, endpoint, p2pAddr, capabilities)
|
||||
|
||||
whooshEndpoint := overrideWhooshEndpoint(cfg)
|
||||
|
||||
return &HTTPServer{
|
||||
port: cfg.Network.APIPort,
|
||||
hypercoreLog: hlog,
|
||||
pubsub: ps,
|
||||
CouncilManager: councilMgr,
|
||||
whooshEndpoint: strings.TrimRight(whooshEndpoint, "/"),
|
||||
}
|
||||
}
|
||||
|
||||
func deriveAgentName(cfg *config.Config) string {
|
||||
if v := strings.TrimSpace(os.Getenv("CHORUS_AGENT_NAME")); v != "" {
|
||||
return v
|
||||
}
|
||||
if cfg.Agent.Specialization != "" {
|
||||
return cfg.Agent.Specialization
|
||||
}
|
||||
return cfg.Agent.ID
|
||||
}
|
||||
|
||||
func deriveAgentEndpoint(cfg *config.Config) string {
|
||||
if v := strings.TrimSpace(os.Getenv("CHORUS_AGENT_ENDPOINT")); v != "" {
|
||||
return strings.TrimRight(v, "/")
|
||||
}
|
||||
host := strings.TrimSpace(os.Getenv("CHORUS_AGENT_SERVICE_HOST"))
|
||||
if host == "" {
|
||||
host = "chorus"
|
||||
}
|
||||
scheme := strings.TrimSpace(os.Getenv("CHORUS_AGENT_ENDPOINT_SCHEME"))
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
return fmt.Sprintf("%s://%s:%d", scheme, host, cfg.Network.APIPort)
|
||||
}
|
||||
|
||||
func deriveAgentP2PAddress(cfg *config.Config, node *p2p.Node) string {
|
||||
if v := strings.TrimSpace(os.Getenv("CHORUS_AGENT_P2P_ENDPOINT")); v != "" {
|
||||
return v
|
||||
}
|
||||
if node != nil {
|
||||
addrs := node.Addresses()
|
||||
if len(addrs) > 0 {
|
||||
return fmt.Sprintf("%s/p2p/%s", addrs[0], node.ID())
|
||||
}
|
||||
}
|
||||
host := strings.TrimSpace(os.Getenv("CHORUS_AGENT_SERVICE_HOST"))
|
||||
if host == "" {
|
||||
host = "chorus"
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", host, cfg.Network.P2PPort)
|
||||
}
|
||||
|
||||
func overrideWhooshEndpoint(cfg *config.Config) string {
|
||||
if v := strings.TrimSpace(os.Getenv("CHORUS_WHOOSH_ENDPOINT")); v != "" {
|
||||
return strings.TrimRight(v, "/")
|
||||
}
|
||||
candidate := cfg.WHOOSHAPI.BaseURL
|
||||
if candidate == "" {
|
||||
candidate = cfg.WHOOSHAPI.URL
|
||||
}
|
||||
if candidate == "" {
|
||||
return "http://whoosh:8080"
|
||||
}
|
||||
trimmed := strings.TrimRight(candidate, "/")
|
||||
if strings.Contains(trimmed, "localhost") || strings.Contains(trimmed, "127.0.0.1") {
|
||||
return "http://whoosh:8080"
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
@@ -65,6 +147,12 @@ func (h *HTTPServer) Start() error {
|
||||
// Status endpoint
|
||||
api.HandleFunc("/status", h.handleStatus).Methods("GET")
|
||||
|
||||
// Council opportunity endpoints (v1)
|
||||
v1 := api.PathPrefix("/v1").Subrouter()
|
||||
v1.HandleFunc("/opportunities/council", h.handleCouncilOpportunity).Methods("POST")
|
||||
v1.HandleFunc("/councils/status", h.handleCouncilStatusUpdate).Methods("POST")
|
||||
v1.HandleFunc("/councils/{councilID}/roles/{roleName}/brief", h.handleCouncilBrief).Methods("POST")
|
||||
|
||||
h.server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", h.port),
|
||||
Handler: router,
|
||||
@@ -242,3 +330,209 @@ func (h *HTTPServer) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
// handleCouncilOpportunity receives council formation opportunities from WHOOSH
|
||||
func (h *HTTPServer) handleCouncilOpportunity(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
var opportunity council.CouncilOpportunity
|
||||
if err := json.NewDecoder(r.Body).Decode(&opportunity); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Invalid JSON payload: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Log the received opportunity to hypercore
|
||||
logData := map[string]interface{}{
|
||||
"event": "council_opportunity_received",
|
||||
"council_id": opportunity.CouncilID,
|
||||
"project_name": opportunity.ProjectName,
|
||||
"repository": opportunity.Repository,
|
||||
"core_roles": len(opportunity.CoreRoles),
|
||||
"optional_roles": len(opportunity.OptionalRoles),
|
||||
"ucxl_address": opportunity.UCXLAddress,
|
||||
"message": fmt.Sprintf("📡 Received council opportunity for project: %s", opportunity.ProjectName),
|
||||
}
|
||||
|
||||
if _, err := h.hypercoreLog.Append(logging.NetworkEvent, logData); err != nil {
|
||||
fmt.Printf("Failed to log council opportunity: %v\n", err)
|
||||
}
|
||||
|
||||
// Log to console for immediate visibility
|
||||
fmt.Printf("\n📡 COUNCIL OPPORTUNITY RECEIVED\n")
|
||||
fmt.Printf(" Council ID: %s\n", opportunity.CouncilID)
|
||||
fmt.Printf(" Project: %s\n", opportunity.ProjectName)
|
||||
fmt.Printf(" Repository: %s\n", opportunity.Repository)
|
||||
fmt.Printf(" Core Roles: %d\n", len(opportunity.CoreRoles))
|
||||
fmt.Printf(" Optional Roles: %d\n", len(opportunity.OptionalRoles))
|
||||
fmt.Printf(" UCXL: %s\n", opportunity.UCXLAddress)
|
||||
fmt.Printf("\n Available Roles:\n")
|
||||
for _, role := range opportunity.CoreRoles {
|
||||
fmt.Printf(" - %s (%s) [CORE]\n", role.AgentName, role.RoleName)
|
||||
}
|
||||
for _, role := range opportunity.OptionalRoles {
|
||||
fmt.Printf(" - %s (%s) [OPTIONAL]\n", role.AgentName, role.RoleName)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
// Evaluate the opportunity and claim a role if suitable
|
||||
go func() {
|
||||
if err := h.CouncilManager.EvaluateOpportunity(&opportunity, h.whooshEndpoint); err != nil {
|
||||
fmt.Printf("Failed to evaluate/claim council role: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "received",
|
||||
"council_id": opportunity.CouncilID,
|
||||
"message": "Council opportunity received and being evaluated",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"agent_id": h.CouncilManager.AgentID(),
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// handleCouncilStatusUpdate receives council staffing updates from WHOOSH
|
||||
func (h *HTTPServer) handleCouncilStatusUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
type roleCountsPayload struct {
|
||||
Total int `json:"total"`
|
||||
Claimed int `json:"claimed"`
|
||||
}
|
||||
|
||||
type councilStatusPayload struct {
|
||||
CouncilID string `json:"council_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
CoreRoles roleCountsPayload `json:"core_roles"`
|
||||
Optional roleCountsPayload `json:"optional_roles"`
|
||||
}
|
||||
|
||||
var payload councilStatusPayload
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Invalid JSON payload: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if payload.CouncilID == "" {
|
||||
http.Error(w, "council_id is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if payload.Status == "" {
|
||||
payload.Status = "unknown"
|
||||
}
|
||||
|
||||
if payload.Timestamp.IsZero() {
|
||||
payload.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
if payload.Message == "" {
|
||||
payload.Message = fmt.Sprintf("Council status update: %s (core %d/%d, optional %d/%d)",
|
||||
payload.Status,
|
||||
payload.CoreRoles.Claimed, payload.CoreRoles.Total,
|
||||
payload.Optional.Claimed, payload.Optional.Total,
|
||||
)
|
||||
}
|
||||
|
||||
logData := map[string]interface{}{
|
||||
"event": "council_status_update",
|
||||
"council_id": payload.CouncilID,
|
||||
"project_name": payload.ProjectName,
|
||||
"status": payload.Status,
|
||||
"message": payload.Message,
|
||||
"timestamp": payload.Timestamp.Format(time.RFC3339),
|
||||
"core_roles_total": payload.CoreRoles.Total,
|
||||
"core_roles_claimed": payload.CoreRoles.Claimed,
|
||||
"optional_roles_total": payload.Optional.Total,
|
||||
"optional_roles_claimed": payload.Optional.Claimed,
|
||||
}
|
||||
|
||||
if _, err := h.hypercoreLog.Append(logging.NetworkEvent, logData); err != nil {
|
||||
fmt.Printf("Failed to log council status update: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n🏁 COUNCIL STATUS UPDATE\n")
|
||||
fmt.Printf(" Council ID: %s\n", payload.CouncilID)
|
||||
if payload.ProjectName != "" {
|
||||
fmt.Printf(" Project: %s\n", payload.ProjectName)
|
||||
}
|
||||
fmt.Printf(" Status: %s\n", payload.Status)
|
||||
fmt.Printf(" Core Roles: %d/%d claimed\n", payload.CoreRoles.Claimed, payload.CoreRoles.Total)
|
||||
fmt.Printf(" Optional Roles: %d/%d claimed\n", payload.Optional.Claimed, payload.Optional.Total)
|
||||
fmt.Printf(" Message: %s\n\n", payload.Message)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "received",
|
||||
"council_id": payload.CouncilID,
|
||||
"timestamp": payload.Timestamp.Unix(),
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (h *HTTPServer) handleCouncilBrief(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
councilID := vars["councilID"]
|
||||
roleName := vars["roleName"]
|
||||
|
||||
if councilID == "" || roleName == "" {
|
||||
http.Error(w, "councilID and roleName are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var brief council.CouncilBrief
|
||||
if err := json.NewDecoder(r.Body).Decode(&brief); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Invalid JSON payload: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
brief.CouncilID = councilID
|
||||
brief.RoleName = roleName
|
||||
|
||||
fmt.Printf("\n📦 Received council brief for %s (%s)\n", councilID, roleName)
|
||||
if brief.BriefURL != "" {
|
||||
fmt.Printf(" Brief URL: %s\n", brief.BriefURL)
|
||||
}
|
||||
if brief.Summary != "" {
|
||||
fmt.Printf(" Summary: %s\n", brief.Summary)
|
||||
}
|
||||
|
||||
if h.CouncilManager != nil {
|
||||
h.CouncilManager.HandleCouncilBrief(councilID, roleName, &brief)
|
||||
}
|
||||
|
||||
logData := map[string]interface{}{
|
||||
"event": "council_brief_received",
|
||||
"council_id": councilID,
|
||||
"role_name": roleName,
|
||||
"project_name": brief.ProjectName,
|
||||
"repository": brief.Repository,
|
||||
"brief_url": brief.BriefURL,
|
||||
"ucxl_address": brief.UCXLAddress,
|
||||
"hmmm_topic": brief.HMMMTopic,
|
||||
"expected_artifacts": brief.ExpectedArtifacts,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if _, err := h.hypercoreLog.Append(logging.NetworkEvent, logData); err != nil {
|
||||
fmt.Printf("Failed to log council brief: %v\n", err)
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "received",
|
||||
"council_id": councilID,
|
||||
"role_name": roleName,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
@@ -11,18 +11,18 @@ WORKDIR /build
|
||||
# Copy go mod files first (for better caching)
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
# Skip go mod download; we rely on vendored deps to avoid local replaces
|
||||
RUN echo "Using vendored dependencies (skipping go mod download)"
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the CHORUS binary with mod mode
|
||||
# Build the CHORUS agent binary with vendored deps
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
-mod=mod \
|
||||
-mod=vendor \
|
||||
-ldflags='-w -s -extldflags "-static"' \
|
||||
-o chorus \
|
||||
./cmd/chorus
|
||||
-o chorus-agent \
|
||||
./cmd/agent
|
||||
|
||||
# Final minimal runtime image
|
||||
FROM alpine:3.18
|
||||
@@ -42,8 +42,8 @@ RUN mkdir -p /app/data && \
|
||||
chown -R chorus:chorus /app
|
||||
|
||||
# Copy binary from builder stage
|
||||
COPY --from=builder /build/chorus /app/chorus
|
||||
RUN chmod +x /app/chorus
|
||||
COPY --from=builder /build/chorus-agent /app/chorus-agent
|
||||
RUN chmod +x /app/chorus-agent
|
||||
|
||||
# Switch to non-root user
|
||||
USER chorus
|
||||
@@ -64,5 +64,5 @@ ENV LOG_LEVEL=info \
|
||||
CHORUS_HEALTH_PORT=8081 \
|
||||
CHORUS_P2P_PORT=9000
|
||||
|
||||
# Start CHORUS
|
||||
ENTRYPOINT ["/app/chorus"]
|
||||
# Start CHORUS Agent
|
||||
ENTRYPOINT ["/app/chorus-agent"]
|
||||
|
||||
@@ -29,8 +29,8 @@ services:
|
||||
- CHORUS_MAX_CONCURRENT_DHT=16 # Limit concurrent DHT queries
|
||||
|
||||
# Election stability windows (Medium-risk fix 2.1)
|
||||
- CHORUS_ELECTION_MIN_TERM=30s # Minimum time between elections to prevent churn
|
||||
- CHORUS_LEADER_MIN_TERM=45s # Minimum time before challenging healthy leader
|
||||
- CHORUS_ELECTION_MIN_TERM=120s # Minimum time between elections to prevent churn
|
||||
- CHORUS_LEADER_MIN_TERM=240s # Minimum time before challenging healthy leader
|
||||
|
||||
# Assignment system for runtime configuration (Medium-risk fix 2.2)
|
||||
- ASSIGN_URL=${ASSIGN_URL:-} # Optional: WHOOSH assignment endpoint
|
||||
@@ -38,6 +38,10 @@ services:
|
||||
- TASK_ID=${TASK_ID:-} # Optional: Task identifier
|
||||
- NODE_ID=${NODE_ID:-} # Optional: Node identifier
|
||||
|
||||
# WHOOSH API configuration for bootstrap peer discovery
|
||||
- WHOOSH_API_BASE_URL=${WHOOSH_API_BASE_URL:-http://whoosh:8080}
|
||||
- WHOOSH_API_ENABLED=true
|
||||
|
||||
# Bootstrap pool configuration (supports JSON and CSV)
|
||||
- BOOTSTRAP_JSON=/config/bootstrap.json # Optional: JSON bootstrap config
|
||||
- CHORUS_BOOTSTRAP_PEERS=${CHORUS_BOOTSTRAP_PEERS:-} # CSV fallback
|
||||
@@ -56,7 +60,14 @@ services:
|
||||
# Model configuration
|
||||
- CHORUS_MODELS=${CHORUS_MODELS:-meta/llama-3.1-8b-instruct}
|
||||
- CHORUS_DEFAULT_REASONING_MODEL=${CHORUS_DEFAULT_REASONING_MODEL:-meta/llama-3.1-8b-instruct}
|
||||
|
||||
|
||||
# LightRAG configuration (optional RAG enhancement)
|
||||
- CHORUS_LIGHTRAG_ENABLED=${CHORUS_LIGHTRAG_ENABLED:-false}
|
||||
- CHORUS_LIGHTRAG_BASE_URL=${CHORUS_LIGHTRAG_BASE_URL:-http://lightrag:9621}
|
||||
- CHORUS_LIGHTRAG_TIMEOUT=${CHORUS_LIGHTRAG_TIMEOUT:-30s}
|
||||
- CHORUS_LIGHTRAG_API_KEY=${CHORUS_LIGHTRAG_API_KEY:-your-secure-api-key-here}
|
||||
- CHORUS_LIGHTRAG_DEFAULT_MODE=${CHORUS_LIGHTRAG_DEFAULT_MODE:-hybrid}
|
||||
|
||||
# Logging configuration
|
||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||
- LOG_FORMAT=${LOG_FORMAT:-structured}
|
||||
@@ -95,7 +106,7 @@ services:
|
||||
# Container resource limits
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: ${CHORUS_REPLICAS:-9}
|
||||
replicas: ${CHORUS_REPLICAS:-20}
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
@@ -166,6 +177,8 @@ services:
|
||||
WHOOSH_SERVER_READ_TIMEOUT: "30s"
|
||||
WHOOSH_SERVER_WRITE_TIMEOUT: "30s"
|
||||
WHOOSH_SERVER_SHUTDOWN_TIMEOUT: "30s"
|
||||
# UI static directory (served at site root by WHOOSH)
|
||||
WHOOSH_UI_DIR: "/app/ui"
|
||||
|
||||
# GITEA configuration
|
||||
WHOOSH_GITEA_BASE_URL: https://gitea.chorus.services
|
||||
@@ -200,8 +213,8 @@ services:
|
||||
WHOOSH_BACKBEAT_AGENT_ID: "whoosh"
|
||||
WHOOSH_BACKBEAT_NATS_URL: "nats://backbeat-nats:4222"
|
||||
|
||||
# Docker integration configuration (disabled for agent assignment architecture)
|
||||
WHOOSH_DOCKER_ENABLED: "false"
|
||||
# Docker integration configuration - ENABLED for complete agent discovery
|
||||
WHOOSH_DOCKER_ENABLED: "true"
|
||||
|
||||
secrets:
|
||||
- whoosh_db_password
|
||||
@@ -210,10 +223,11 @@ services:
|
||||
- jwt_secret
|
||||
- service_tokens
|
||||
- redis_password
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock # Disabled for agent assignment architecture
|
||||
volumes:
|
||||
- whoosh_ui:/app/ui:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Required for Docker Swarm agent discovery
|
||||
deploy:
|
||||
replicas: 2
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
@@ -247,11 +261,11 @@ services:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=tengig
|
||||
- traefik.http.routers.whoosh.rule=Host(`whoosh.chorus.services`)
|
||||
- traefik.http.routers.whoosh.entrypoints=web,web-secured
|
||||
- traefik.http.routers.whoosh.tls=true
|
||||
- traefik.http.routers.whoosh.tls.certresolver=letsencryptresolver
|
||||
- traefik.http.routers.photoprism.entrypoints=web,web-secured
|
||||
- traefik.http.services.whoosh.loadbalancer.server.port=8080
|
||||
- traefik.http.services.photoprism.loadbalancer.passhostheader=true
|
||||
- traefik.http.services.whoosh.loadbalancer.passhostheader=true
|
||||
- traefik.http.middlewares.whoosh-auth.basicauth.users=admin:$2y$10$example_hash
|
||||
networks:
|
||||
- tengig
|
||||
@@ -407,7 +421,7 @@ services:
|
||||
# REQ: BACKBEAT-REQ-001 - Single BeatFrame publisher per cluster
|
||||
# REQ: BACKBEAT-OPS-001 - One replica prefers leadership
|
||||
backbeat-pulse:
|
||||
image: anthonyrawlins/backbeat-pulse:v1.0.5
|
||||
image: anthonyrawlins/backbeat-pulse:v1.0.6
|
||||
command: >
|
||||
./pulse
|
||||
-cluster=chorus-production
|
||||
@@ -574,6 +588,46 @@ services:
|
||||
max-file: "3"
|
||||
tag: "nats/{{.Name}}/{{.ID}}"
|
||||
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
command: --interval 300 --cleanup --revive-stopped --include-stopped
|
||||
restart: always
|
||||
|
||||
# HMMM Traffic Monitor - Observes P2P pub/sub traffic
|
||||
hmmm-monitor:
|
||||
image: anthonyrawlins/hmmm-monitor:latest
|
||||
environment:
|
||||
- WHOOSH_API_BASE_URL=http://whoosh:8080
|
||||
ports:
|
||||
- "9001:9001" # P2P port for peer discovery
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == acacia # Keep monitor on acacia for stable peer ID
|
||||
resources:
|
||||
limits:
|
||||
memory: 128M
|
||||
cpus: '0.25'
|
||||
reservations:
|
||||
memory: 64M
|
||||
cpus: '0.1'
|
||||
networks:
|
||||
- chorus_net
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
tag: "hmmm-monitor/{{.Name}}/{{.ID}}"
|
||||
|
||||
# KACHING services are deployed separately in their own stack
|
||||
# License validation will access https://kaching.chorus.services/api
|
||||
|
||||
@@ -611,6 +665,12 @@ volumes:
|
||||
type: none
|
||||
o: bind
|
||||
device: /rust/containers/WHOOSH/redis
|
||||
whoosh_ui:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: /rust/containers/WHOOSH/ui
|
||||
|
||||
|
||||
# Networks for CHORUS communication
|
||||
@@ -645,7 +705,7 @@ secrets:
|
||||
name: whoosh_webhook_token
|
||||
jwt_secret:
|
||||
external: true
|
||||
name: whoosh_jwt_secret
|
||||
name: whoosh_jwt_secret_v4
|
||||
service_tokens:
|
||||
external: true
|
||||
name: whoosh_service_tokens
|
||||
|
||||
388
docs/LIGHTRAG_INTEGRATION.md
Normal file
388
docs/LIGHTRAG_INTEGRATION.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# LightRAG MCP Integration
|
||||
|
||||
**Status:** ✅ Production Ready
|
||||
**Version:** 1.0.0
|
||||
**Date:** 2025-09-30
|
||||
|
||||
## Overview
|
||||
|
||||
CHORUS now includes optional LightRAG integration for Retrieval-Augmented Generation (RAG) capabilities. LightRAG provides graph-based knowledge retrieval to enrich AI reasoning and context resolution.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components
|
||||
|
||||
1. **LightRAG Client** (`pkg/mcp/lightrag_client.go`)
|
||||
- HTTP client for LightRAG MCP server
|
||||
- Supports 4 query modes: naive, local, global, hybrid
|
||||
- Health checking and document insertion
|
||||
- Configurable timeouts and API authentication
|
||||
|
||||
2. **Reasoning Engine Integration** (`reasoning/reasoning.go`)
|
||||
- `GenerateResponseWithRAG()` - RAG-enriched response generation
|
||||
- `GenerateResponseSmartWithRAG()` - Combines model selection + RAG
|
||||
- `SetLightRAGClient()` - Configure RAG client
|
||||
- Non-fatal error handling (degrades gracefully)
|
||||
|
||||
3. **SLURP Context Enrichment** (`pkg/slurp/context/lightrag.go`)
|
||||
- `LightRAGEnricher` - Enriches context nodes with RAG data
|
||||
- `EnrichContextNode()` - Add insights to individual nodes
|
||||
- `EnrichResolvedContext()` - Enrich resolved context chains
|
||||
- `InsertContextNode()` - Build knowledge base over time
|
||||
|
||||
4. **Configuration** (`pkg/config/config.go`)
|
||||
- `LightRAGConfig` struct with 5 configuration options
|
||||
- Environment variable support
|
||||
- Automatic initialization in runtime
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Enable LightRAG integration
|
||||
CHORUS_LIGHTRAG_ENABLED=true
|
||||
|
||||
# LightRAG server endpoint
|
||||
CHORUS_LIGHTRAG_BASE_URL=http://127.0.0.1:9621
|
||||
|
||||
# Query timeout
|
||||
CHORUS_LIGHTRAG_TIMEOUT=30s
|
||||
|
||||
# Optional API key
|
||||
CHORUS_LIGHTRAG_API_KEY=your-api-key
|
||||
|
||||
# Default query mode (naive, local, global, hybrid)
|
||||
CHORUS_LIGHTRAG_DEFAULT_MODE=hybrid
|
||||
```
|
||||
|
||||
### Docker Configuration
|
||||
|
||||
```yaml
|
||||
services:
|
||||
chorus-agent:
|
||||
environment:
|
||||
- CHORUS_LIGHTRAG_ENABLED=true
|
||||
- CHORUS_LIGHTRAG_BASE_URL=http://lightrag:9621
|
||||
- CHORUS_LIGHTRAG_DEFAULT_MODE=hybrid
|
||||
depends_on:
|
||||
- lightrag
|
||||
|
||||
lightrag:
|
||||
image: lightrag/lightrag:latest
|
||||
ports:
|
||||
- "9621:9621"
|
||||
volumes:
|
||||
- lightrag-data:/app/data
|
||||
```
|
||||
|
||||
## Query Modes
|
||||
|
||||
LightRAG supports 4 query modes with different retrieval strategies:
|
||||
|
||||
1. **Naive Mode** (`QueryModeNaive`)
|
||||
- Simple semantic search
|
||||
- Fastest, least context
|
||||
- Use for: Quick lookups
|
||||
|
||||
2. **Local Mode** (`QueryModeLocal`)
|
||||
- Local graph traversal
|
||||
- Context from immediate neighbors
|
||||
- Use for: Related information
|
||||
|
||||
3. **Global Mode** (`QueryModeGlobal`)
|
||||
- Global graph analysis
|
||||
- Broad context from entire knowledge base
|
||||
- Use for: High-level questions
|
||||
|
||||
4. **Hybrid Mode** (`QueryModeHybrid`) ⭐ **Recommended**
|
||||
- Combined approach
|
||||
- Balances breadth and depth
|
||||
- Use for: General purpose RAG
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Reasoning Engine with RAG
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"chorus/reasoning"
|
||||
"chorus/pkg/mcp"
|
||||
)
|
||||
|
||||
// Initialize LightRAG client
|
||||
config := mcp.LightRAGConfig{
|
||||
BaseURL: "http://127.0.0.1:9621",
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
client := mcp.NewLightRAGClient(config)
|
||||
|
||||
// Configure reasoning engine
|
||||
reasoning.SetLightRAGClient(client)
|
||||
|
||||
// Generate RAG-enriched response
|
||||
ctx := context.Background()
|
||||
response, err := reasoning.GenerateResponseWithRAG(
|
||||
ctx,
|
||||
"meta/llama-3.1-8b-instruct",
|
||||
"How does CHORUS handle P2P networking?",
|
||||
mcp.QueryModeHybrid,
|
||||
)
|
||||
```
|
||||
|
||||
### SLURP Context Enrichment
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"chorus/pkg/slurp/context"
|
||||
"chorus/pkg/mcp"
|
||||
)
|
||||
|
||||
// Create enricher
|
||||
enricher := context.NewLightRAGEnricher(client, "hybrid")
|
||||
|
||||
// Enrich a context node
|
||||
node := &context.ContextNode{
|
||||
Path: "/pkg/p2p",
|
||||
Summary: "P2P networking implementation",
|
||||
Purpose: "Provides libp2p networking layer",
|
||||
}
|
||||
|
||||
err := enricher.EnrichContextNode(ctx, node)
|
||||
// node.Insights now contains RAG-retrieved information
|
||||
|
||||
// Insert for future retrieval
|
||||
err = enricher.InsertContextNode(ctx, node)
|
||||
```
|
||||
|
||||
### Direct LightRAG Client
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"chorus/pkg/mcp"
|
||||
)
|
||||
|
||||
client := mcp.NewLightRAGClient(config)
|
||||
|
||||
// Health check
|
||||
healthy := client.IsHealthy(ctx)
|
||||
|
||||
// Query with response
|
||||
response, err := client.Query(ctx, "query", mcp.QueryModeHybrid)
|
||||
|
||||
// Get context only
|
||||
context, err := client.GetContext(ctx, "query", mcp.QueryModeHybrid)
|
||||
|
||||
// Insert document
|
||||
err := client.Insert(ctx, "text content", "description")
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Runtime Initialization
|
||||
|
||||
LightRAG is initialized automatically in `internal/runtime/shared.go`:
|
||||
|
||||
```go
|
||||
// Line 685-704
|
||||
if cfg.LightRAG.Enabled {
|
||||
lightragConfig := mcp.LightRAGConfig{
|
||||
BaseURL: cfg.LightRAG.BaseURL,
|
||||
Timeout: cfg.LightRAG.Timeout,
|
||||
APIKey: cfg.LightRAG.APIKey,
|
||||
}
|
||||
lightragClient := mcp.NewLightRAGClient(lightragConfig)
|
||||
|
||||
if lightragClient.IsHealthy(ctx) {
|
||||
reasoning.SetLightRAGClient(lightragClient)
|
||||
logger.Info("📚 LightRAG RAG system enabled")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
LightRAG integration is **completely optional** and **non-blocking**:
|
||||
|
||||
- If `CHORUS_LIGHTRAG_ENABLED=false`, no LightRAG calls are made
|
||||
- If LightRAG server is unavailable, health check fails gracefully
|
||||
- If RAG queries fail, reasoning engine falls back to non-RAG generation
|
||||
- SLURP enrichment failures are logged but don't block context resolution
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```bash
|
||||
# Run all LightRAG tests (requires running server)
|
||||
go test -v ./pkg/mcp/
|
||||
|
||||
# Run only unit tests (no server required)
|
||||
go test -v -short ./pkg/mcp/
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```bash
|
||||
# Start LightRAG server
|
||||
cd ~/chorus/mcp-include/LightRAG
|
||||
python main.py
|
||||
|
||||
# Run integration tests
|
||||
cd ~/chorus/project-queues/active/CHORUS
|
||||
go test -v ./pkg/mcp/ -run TestLightRAGClient
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Query Timeouts
|
||||
|
||||
- Default: 30 seconds
|
||||
- Hybrid mode is slowest (analyzes entire graph)
|
||||
- Naive mode is fastest (simple semantic search)
|
||||
|
||||
### Caching
|
||||
|
||||
LightRAG includes internal caching:
|
||||
- Repeated queries return cached results
|
||||
- Cache TTL managed by LightRAG server
|
||||
- No CHORUS-side caching required
|
||||
|
||||
### Resource Usage
|
||||
|
||||
- Memory: Proportional to knowledge base size
|
||||
- CPU: Query modes have different compute requirements
|
||||
- Network: HTTP requests to LightRAG server
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server Not Healthy
|
||||
|
||||
**Symptom:** `LightRAG enabled but server not healthy`
|
||||
|
||||
**Solutions:**
|
||||
1. Check if LightRAG server is running: `curl http://127.0.0.1:9621/health`
|
||||
2. Verify correct port in `CHORUS_LIGHTRAG_BASE_URL`
|
||||
3. Check LightRAG logs for errors
|
||||
4. Ensure network connectivity between CHORUS and LightRAG
|
||||
|
||||
### Empty Responses
|
||||
|
||||
**Symptom:** RAG queries return empty results
|
||||
|
||||
**Solutions:**
|
||||
1. Knowledge base may be empty - insert documents first
|
||||
2. Query may not match indexed content
|
||||
3. Try different query mode (hybrid recommended)
|
||||
4. Check LightRAG indexing logs
|
||||
|
||||
### Timeout Errors
|
||||
|
||||
**Symptom:** `context deadline exceeded`
|
||||
|
||||
**Solutions:**
|
||||
1. Increase `CHORUS_LIGHTRAG_TIMEOUT`
|
||||
2. Use faster query mode (naive or local)
|
||||
3. Optimize LightRAG server performance
|
||||
4. Check network latency
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### API Authentication
|
||||
|
||||
Optional API key support:
|
||||
```bash
|
||||
CHORUS_LIGHTRAG_API_KEY=your-secret-key
|
||||
```
|
||||
|
||||
Keys are sent as Bearer tokens in Authorization header.
|
||||
|
||||
### Network Security
|
||||
|
||||
- Run LightRAG on internal network only
|
||||
- Use HTTPS for production deployments
|
||||
- Consider firewall rules to restrict access
|
||||
- LightRAG doesn't include built-in encryption
|
||||
|
||||
### Data Privacy
|
||||
|
||||
- All queries and documents are stored in LightRAG
|
||||
- Consider what data is being indexed
|
||||
- Implement data retention policies
|
||||
- Use access control on LightRAG server
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```go
|
||||
// Check LightRAG availability
|
||||
if client.IsHealthy(ctx) {
|
||||
// Server is healthy
|
||||
}
|
||||
|
||||
// Get detailed health info
|
||||
health, err := client.Health(ctx)
|
||||
// Returns: Status, CoreVersion, APIVersion, etc.
|
||||
```
|
||||
|
||||
### Metrics
|
||||
|
||||
Consider adding:
|
||||
- RAG query latency
|
||||
- Cache hit rates
|
||||
- Enrichment success/failure rates
|
||||
- Knowledge base size
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
|
||||
1. **Batch Query Optimization**
|
||||
- Batch multiple RAG queries together
|
||||
- Reduce HTTP overhead
|
||||
|
||||
2. **Adaptive Query Mode Selection**
|
||||
- Automatically choose query mode based on question type
|
||||
- Learn from past query performance
|
||||
|
||||
3. **Knowledge Base Management**
|
||||
- Automated document insertion from SLURP contexts
|
||||
- Background indexing of code repositories
|
||||
- Scheduled knowledge base updates
|
||||
|
||||
4. **Advanced Caching**
|
||||
- CHORUS-side caching with TTL
|
||||
- Semantic cache (similar queries share cache)
|
||||
- Persistent cache across restarts
|
||||
|
||||
5. **Multi-tenant Support**
|
||||
- Per-agent knowledge bases
|
||||
- Role-based access to documents
|
||||
- Encrypted knowledge storage
|
||||
|
||||
## Files Changed
|
||||
|
||||
1. `pkg/mcp/lightrag_client.go` - NEW (277 lines)
|
||||
2. `pkg/mcp/lightrag_client_test.go` - NEW (239 lines)
|
||||
3. `pkg/config/config.go` - Modified (added LightRAGConfig)
|
||||
4. `reasoning/reasoning.go` - Modified (added RAG functions)
|
||||
5. `internal/runtime/shared.go` - Modified (added initialization)
|
||||
6. `pkg/slurp/context/lightrag.go` - NEW (203 lines)
|
||||
|
||||
**Total:** 3 new files, 3 modified files, ~750 lines of code
|
||||
|
||||
## References
|
||||
|
||||
- LightRAG Documentation: https://github.com/HKUDS/LightRAG
|
||||
- MCP Protocol Spec: https://spec.modelcontextprotocol.io
|
||||
- CHORUS Documentation: `docs/comprehensive/`
|
||||
|
||||
---
|
||||
|
||||
**Maintainer:** CHORUS Project Team
|
||||
**Last Updated:** 2025-09-30
|
||||
**Status:** Production Ready
|
||||
2
hmmm-monitor/.gitignore
vendored
Normal file
2
hmmm-monitor/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
hmmm-monitor
|
||||
*.log
|
||||
41
hmmm-monitor/Dockerfile
Normal file
41
hmmm-monitor/Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache git ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum* ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download || true
|
||||
|
||||
# Copy source code
|
||||
COPY main.go ./
|
||||
|
||||
# Build the binary
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o hmmm-monitor main.go
|
||||
|
||||
# Final stage - minimal image
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /app/hmmm-monitor .
|
||||
|
||||
# Run as non-root user
|
||||
RUN addgroup -g 1000 monitor && \
|
||||
adduser -D -u 1000 -G monitor monitor && \
|
||||
chown -R monitor:monitor /app
|
||||
|
||||
USER monitor
|
||||
|
||||
# Set metadata
|
||||
LABEL maintainer="CHORUS Ecosystem" \
|
||||
description="HMMM Traffic Monitor - Real-time libp2p message monitoring for CHORUS"
|
||||
|
||||
ENTRYPOINT ["./hmmm-monitor"]
|
||||
120
hmmm-monitor/README.md
Normal file
120
hmmm-monitor/README.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# HMMM Traffic Monitor
|
||||
|
||||
Real-time monitoring tool for CHORUS libp2p pub/sub messages (HMMM and Bzzz).
|
||||
|
||||
## Purpose
|
||||
|
||||
This standalone monitoring container subscribes to all CHORUS pub/sub topics and logs all traffic in real-time. It's designed for:
|
||||
|
||||
- **Debugging**: See exactly what messages are being sent
|
||||
- **Observability**: Monitor agent coordination and task execution
|
||||
- **Development**: Understand message flow during development
|
||||
- **Troubleshooting**: Identify communication issues between agents
|
||||
|
||||
## Topics Monitored
|
||||
|
||||
- `chorus-bzzz`: Main coordination topic (task claims, availability, progress)
|
||||
- `chorus-hmmm`: Meta-discussion topic (help requests, collaboration)
|
||||
- `chorus-context`: Context feedback messages
|
||||
- `council-formation`: Council formation broadcasts
|
||||
- `council-assignments`: Role assignments
|
||||
|
||||
## Usage
|
||||
|
||||
### Build the Image
|
||||
|
||||
```bash
|
||||
cd hmmm-monitor
|
||||
docker build -t anthonyrawlins/hmmm-monitor:latest .
|
||||
```
|
||||
|
||||
### Run Locally
|
||||
|
||||
```bash
|
||||
docker run --rm --network chorus_net anthonyrawlins/hmmm-monitor:latest
|
||||
```
|
||||
|
||||
### Deploy to Swarm
|
||||
|
||||
```bash
|
||||
docker stack deploy -c docker-compose.yml hmmm-monitor
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# Real-time logs
|
||||
docker service logs -f hmmm-monitor_hmmm-monitor
|
||||
|
||||
# Filter by topic
|
||||
docker service logs hmmm-monitor_hmmm-monitor | grep "chorus-bzzz"
|
||||
|
||||
# Filter by message type
|
||||
docker service logs hmmm-monitor_hmmm-monitor | grep "availability_broadcast"
|
||||
|
||||
# Export to file
|
||||
docker service logs hmmm-monitor_hmmm-monitor > hmmm-traffic-$(date +%Y%m%d).log
|
||||
```
|
||||
|
||||
## Message Format
|
||||
|
||||
Each logged message includes:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-10-11T12:30:45Z",
|
||||
"topic": "chorus-bzzz",
|
||||
"from": "12D3Koo...",
|
||||
"type": "availability_broadcast",
|
||||
"payload": {
|
||||
"agent_id": "agent-123",
|
||||
"current_tasks": 1,
|
||||
"max_tasks": 3,
|
||||
"available_for_work": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Emojis
|
||||
|
||||
The monitor uses emojis to quickly identify message types:
|
||||
|
||||
- 🐝 General Bzzz coordination
|
||||
- 📊 Availability broadcasts
|
||||
- 🎯 Capability broadcasts
|
||||
- ✋ Task claims
|
||||
- ⏳ Task progress
|
||||
- ✅ Task complete
|
||||
- 🧠 HMMM meta-discussion
|
||||
- 💬 Discussion messages
|
||||
- 🆘 Help requests
|
||||
- 💡 Help responses
|
||||
- 🚨 Escalation triggers
|
||||
- 🎭 Council formation
|
||||
- 👔 Council assignments
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No messages appearing
|
||||
|
||||
1. Check network connectivity: `docker exec hmmm-monitor ping chorus`
|
||||
2. Verify container is on correct network: `docker inspect hmmm-monitor | grep NetworkMode`
|
||||
3. Check CHORUS agents are publishing: `docker service logs CHORUS_chorus | grep "broadcast"`
|
||||
|
||||
### High CPU usage
|
||||
|
||||
The monitor processes all pub/sub traffic. If CPU usage is high, consider:
|
||||
- Reducing replicas count
|
||||
- Filtering logs externally rather than in the container
|
||||
- Running only during debugging sessions
|
||||
|
||||
## Architecture
|
||||
|
||||
The monitor is a minimal libp2p node that:
|
||||
|
||||
1. Joins the same libp2p network as CHORUS agents
|
||||
2. Subscribes to gossipsub topics
|
||||
3. Logs all received messages
|
||||
4. Does NOT publish any messages (read-only)
|
||||
|
||||
This makes it safe to run in production without affecting agent behavior.
|
||||
34
hmmm-monitor/docker-compose.yml
Normal file
34
hmmm-monitor/docker-compose.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
hmmm-monitor:
|
||||
build: .
|
||||
image: anthonyrawlins/hmmm-monitor:latest
|
||||
container_name: hmmm-monitor
|
||||
networks:
|
||||
- chorus_net
|
||||
environment:
|
||||
- LOG_LEVEL=info
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == walnut # Deploy on same node as CHORUS for network access
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.5'
|
||||
memory: 256M
|
||||
reservations:
|
||||
cpus: '0.1'
|
||||
memory: 128M
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
chorus_net:
|
||||
external: true
|
||||
name: CHORUS_chorus_net
|
||||
113
hmmm-monitor/go.mod
Normal file
113
hmmm-monitor/go.mod
Normal file
@@ -0,0 +1,113 @@
|
||||
module hmmm-monitor
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/libp2p/go-libp2p v0.36.5
|
||||
github.com/libp2p/go-libp2p-pubsub v0.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/elastic/gosigar v0.14.3 // indirect
|
||||
github.com/flynn/noise v1.1.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/ipfs/go-cid v0.4.1 // indirect
|
||||
github.com/ipfs/go-log/v2 v2.5.1 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
|
||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||
github.com/libp2p/go-nat v0.2.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||
github.com/libp2p/go-reuseport v0.4.0 // indirect
|
||||
github.com/libp2p/go-yamux/v4 v4.0.1 // indirect
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multiaddr v0.13.0 // indirect
|
||||
github.com/multiformats/go-multiaddr-dns v0.4.0 // indirect
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
|
||||
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||
github.com/multiformats/go-multicodec v0.9.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-multistream v0.5.0 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.20.0 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.0 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pion/datachannel v1.5.8 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/ice/v2 v2.3.34 // indirect
|
||||
github.com/pion/interceptor v0.1.30 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.14 // indirect
|
||||
github.com/pion/rtp v1.8.9 // indirect
|
||||
github.com/pion/sctp v1.8.33 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.20 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pion/webrtc/v3 v3.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.20.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/quic-go v0.46.0 // indirect
|
||||
github.com/quic-go/webtransport-go v0.8.0 // indirect
|
||||
github.com/raulk/go-watchdog v1.3.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.4 // indirect
|
||||
go.uber.org/dig v1.18.0 // indirect
|
||||
go.uber.org/fx v1.22.2 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
)
|
||||
538
hmmm-monitor/go.sum
Normal file
538
hmmm-monitor/go.sum
Normal file
@@ -0,0 +1,538 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
|
||||
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
|
||||
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
|
||||
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
|
||||
github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
|
||||
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
||||
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||
github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM=
|
||||
github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro=
|
||||
github.com/libp2p/go-libp2p v0.36.5 h1:DoABsaHO0VXwH6pwCs2F6XKAXWYjFMO4HFBoVxTnF9g=
|
||||
github.com/libp2p/go-libp2p v0.36.5/go.mod h1:CpszAtXxHYOcyvB7K8rSHgnNlh21eKjYbEfLoMerbEI=
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94=
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8=
|
||||
github.com/libp2p/go-libp2p-pubsub v0.12.0 h1:PENNZjSfk8KYxANRlpipdS7+BfLmOl3L2E/6vSNjbdI=
|
||||
github.com/libp2p/go-libp2p-pubsub v0.12.0/go.mod h1:Oi0zw9aw8/Y5GC99zt+Ef2gYAl+0nZlwdJonDyOz/sE=
|
||||
github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=
|
||||
github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
|
||||
github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=
|
||||
github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM=
|
||||
github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk=
|
||||
github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk=
|
||||
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
|
||||
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
|
||||
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
|
||||
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
|
||||
github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ=
|
||||
github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
|
||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU=
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc=
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s=
|
||||
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
|
||||
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
|
||||
github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ=
|
||||
github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII=
|
||||
github.com/multiformats/go-multiaddr-dns v0.4.0 h1:P76EJ3qzBXpUXZ3twdCDx/kvagMsNo0LMFXpyms/zgU=
|
||||
github.com/multiformats/go-multiaddr-dns v0.4.0/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc=
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
|
||||
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
||||
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
||||
github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
|
||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||
github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE=
|
||||
github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA=
|
||||
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
|
||||
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw=
|
||||
github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
|
||||
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo=
|
||||
github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.34 h1:Ic1ppYCj4tUOcPAp76U6F3fVrlSw8A9JtRXLqw6BbUM=
|
||||
github.com/pion/ice/v2 v2.3.34/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
|
||||
github.com/pion/interceptor v0.1.30 h1:au5rlVHsgmxNi+v/mjOPazbW1SHzfx7/hYOEYQnUcxA=
|
||||
github.com/pion/interceptor v0.1.30/go.mod h1:RQuKT5HTdkP2Fi0cuOS5G5WNymTjzXaGF75J4k7z2nc=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
|
||||
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
|
||||
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
||||
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.3.0 h1:Rf4u6n6U5t5sUxhYPQk/samzU/oDv7jk6BA5hyO2F9I=
|
||||
github.com/pion/webrtc/v3 v3.3.0/go.mod h1:hVmrDJvwhEertRWObeb1xzulzHGeVUoPlWvxdGzcfU0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI=
|
||||
github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y=
|
||||
github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
|
||||
github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv/cD7QFJg=
|
||||
github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
|
||||
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
||||
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.4 h1:0de1OFQxnNqAu+x2FAKKCVIrnfGKQbs7FQz++tB0+Uw=
|
||||
github.com/wlynxg/anet v0.0.4/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
|
||||
go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.22.2 h1:iPW+OPxv0G8w75OemJ1RAnTUrF55zOJlXlo1TbJ0Buw=
|
||||
go.uber.org/fx v1.22.2/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
195
hmmm-monitor/main.go
Normal file
195
hmmm-monitor/main.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/libp2p/go-libp2p"
|
||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
)
|
||||
|
||||
// MessageLog represents a logged HMMM/Bzzz message
|
||||
type MessageLog struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Topic string `json:"topic"`
|
||||
From string `json:"from"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Handle graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigChan
|
||||
log.Println("🛑 Shutting down HMMM monitor...")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
log.Println("🔍 Starting HMMM Traffic Monitor...")
|
||||
|
||||
// Create libp2p host
|
||||
h, err := libp2p.New(
|
||||
libp2p.ListenAddrStrings("/ip4/0.0.0.0/tcp/0"),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create libp2p host:", err)
|
||||
}
|
||||
defer h.Close()
|
||||
|
||||
log.Printf("📡 Monitor node ID: %s", h.ID().String())
|
||||
log.Printf("📍 Listening on: %v", h.Addrs())
|
||||
|
||||
// Create PubSub instance
|
||||
ps, err := pubsub.NewGossipSub(ctx, h)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create PubSub:", err)
|
||||
}
|
||||
|
||||
// Topics to monitor
|
||||
topics := []string{
|
||||
"chorus-bzzz", // Main CHORUS coordination topic
|
||||
"chorus-hmmm", // HMMM meta-discussion topic
|
||||
"chorus-context", // Context feedback topic
|
||||
"council-formation", // Council formation broadcasts
|
||||
"council-assignments", // Role assignments
|
||||
}
|
||||
|
||||
// Subscribe to all topics
|
||||
for _, topicName := range topics {
|
||||
go monitorTopic(ctx, ps, h, topicName)
|
||||
}
|
||||
|
||||
log.Println("✅ HMMM Monitor ready - listening for traffic...")
|
||||
log.Println(" Press Ctrl+C to stop")
|
||||
|
||||
// Keep running until context is cancelled
|
||||
<-ctx.Done()
|
||||
log.Println("✅ HMMM Monitor stopped")
|
||||
}
|
||||
|
||||
func monitorTopic(ctx context.Context, ps *pubsub.PubSub, h host.Host, topicName string) {
|
||||
// Join topic
|
||||
topic, err := ps.Join(topicName)
|
||||
if err != nil {
|
||||
log.Printf("❌ Failed to join topic %s: %v", topicName, err)
|
||||
return
|
||||
}
|
||||
defer topic.Close()
|
||||
|
||||
// Subscribe to topic
|
||||
sub, err := topic.Subscribe()
|
||||
if err != nil {
|
||||
log.Printf("❌ Failed to subscribe to %s: %v", topicName, err)
|
||||
return
|
||||
}
|
||||
defer sub.Cancel()
|
||||
|
||||
log.Printf("👂 Monitoring topic: %s", topicName)
|
||||
|
||||
// Process messages
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
msg, err := sub.Next(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Printf("⚠️ Error reading from %s: %v", topicName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip messages from ourselves
|
||||
if msg.ReceivedFrom == h.ID() {
|
||||
continue
|
||||
}
|
||||
|
||||
logMessage(topicName, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logMessage(topicName string, msg *pubsub.Message) {
|
||||
// Try to parse as JSON
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(msg.Data, &payload); err != nil {
|
||||
// Not JSON, log as raw data
|
||||
log.Printf("🐝 [%s] from %s: %s", topicName, msg.ReceivedFrom.ShortString(), string(msg.Data))
|
||||
return
|
||||
}
|
||||
|
||||
// Extract message type if available
|
||||
msgType, _ := payload["type"].(string)
|
||||
|
||||
logEntry := MessageLog{
|
||||
Timestamp: time.Now(),
|
||||
Topic: topicName,
|
||||
From: msg.ReceivedFrom.ShortString(),
|
||||
Type: msgType,
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
// Pretty print JSON log
|
||||
jsonLog, _ := json.MarshalIndent(logEntry, "", " ")
|
||||
|
||||
// Use emoji based on topic
|
||||
emoji := getTopicEmoji(topicName, msgType)
|
||||
|
||||
fmt.Printf("\n%s [%s] from %s\n%s\n", emoji, topicName, msg.ReceivedFrom.ShortString(), jsonLog)
|
||||
}
|
||||
|
||||
func getTopicEmoji(topic, msgType string) string {
|
||||
// Topic-based emojis
|
||||
switch topic {
|
||||
case "chorus-bzzz":
|
||||
switch msgType {
|
||||
case "availability_broadcast":
|
||||
return "📊"
|
||||
case "capability_broadcast":
|
||||
return "🎯"
|
||||
case "task_claim":
|
||||
return "✋"
|
||||
case "task_progress":
|
||||
return "⏳"
|
||||
case "task_complete":
|
||||
return "✅"
|
||||
default:
|
||||
return "🐝"
|
||||
}
|
||||
case "chorus-hmmm":
|
||||
switch msgType {
|
||||
case "meta_discussion":
|
||||
return "💬"
|
||||
case "task_help_request":
|
||||
return "🆘"
|
||||
case "task_help_response":
|
||||
return "💡"
|
||||
case "escalation_trigger":
|
||||
return "🚨"
|
||||
default:
|
||||
return "🧠"
|
||||
}
|
||||
case "chorus-context":
|
||||
return "📝"
|
||||
case "council-formation":
|
||||
return "🎭"
|
||||
case "council-assignments":
|
||||
return "👔"
|
||||
default:
|
||||
return "📡"
|
||||
}
|
||||
}
|
||||
451
internal/council/manager.go
Normal file
451
internal/council/manager.go
Normal file
@@ -0,0 +1,451 @@
|
||||
package council
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"chorus/internal/persona"
|
||||
)
|
||||
|
||||
// CouncilOpportunity represents a council formation opportunity from WHOOSH.
|
||||
type CouncilOpportunity struct {
|
||||
CouncilID string `json:"council_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
Repository string `json:"repository"`
|
||||
ProjectBrief string `json:"project_brief"`
|
||||
CoreRoles []CouncilRole `json:"core_roles"`
|
||||
OptionalRoles []CouncilRole `json:"optional_roles"`
|
||||
UCXLAddress string `json:"ucxl_address"`
|
||||
FormationDeadline time.Time `json:"formation_deadline"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// CouncilRole represents a single role available within a council.
|
||||
type CouncilRole struct {
|
||||
RoleName string `json:"role_name"`
|
||||
AgentName string `json:"agent_name"`
|
||||
Required bool `json:"required"`
|
||||
RequiredSkills []string `json:"required_skills"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// RoleProfile mirrors WHOOSH role profile metadata included in claim responses.
|
||||
type RoleProfile struct {
|
||||
RoleName string `json:"role_name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
PromptKey string `json:"prompt_key"`
|
||||
PromptPack string `json:"prompt_pack"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
BriefRoutingHint string `json:"brief_routing_hint"`
|
||||
DefaultBriefOwner bool `json:"default_brief_owner"`
|
||||
}
|
||||
|
||||
// CouncilBrief carries the high-level brief metadata for an activated council.
|
||||
type CouncilBrief struct {
|
||||
CouncilID string `json:"council_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
ProjectName string `json:"project_name"`
|
||||
Repository string `json:"repository"`
|
||||
Summary string `json:"summary"`
|
||||
BriefURL string `json:"brief_url"`
|
||||
IssueID *int64 `json:"issue_id"`
|
||||
UCXLAddress string `json:"ucxl_address"`
|
||||
ExpectedArtifacts []string `json:"expected_artifacts"`
|
||||
HMMMTopic string `json:"hmmm_topic"`
|
||||
}
|
||||
|
||||
// RoleAssignment keeps track of the agent's current council engagement.
|
||||
type RoleAssignment struct {
|
||||
CouncilID string
|
||||
RoleName string
|
||||
UCXLAddress string
|
||||
AssignedAt time.Time
|
||||
Profile RoleProfile
|
||||
Brief *CouncilBrief
|
||||
Persona *persona.Persona
|
||||
PersonaHash string
|
||||
}
|
||||
|
||||
var ErrRoleConflict = errors.New("council role already claimed")
|
||||
|
||||
const defaultModelProvider = "ollama"
|
||||
|
||||
// Manager handles council opportunity evaluation, persona preparation, and brief handoff.
|
||||
type Manager struct {
|
||||
agentID string
|
||||
agentName string
|
||||
endpoint string
|
||||
p2pAddr string
|
||||
capabilities []string
|
||||
|
||||
httpClient *http.Client
|
||||
personaLoader *persona.Loader
|
||||
|
||||
mu sync.Mutex
|
||||
currentAssignment *RoleAssignment
|
||||
}
|
||||
|
||||
// NewManager creates a new council manager.
|
||||
func NewManager(agentID, agentName, endpoint, p2pAddr string, capabilities []string) *Manager {
|
||||
loader, err := persona.NewLoader()
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ Persona loader initialisation failed: %v\n", err)
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
agentID: agentID,
|
||||
agentName: agentName,
|
||||
endpoint: endpoint,
|
||||
p2pAddr: p2pAddr,
|
||||
capabilities: capabilities,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
personaLoader: loader,
|
||||
}
|
||||
}
|
||||
|
||||
// AgentID returns the agent's identifier.
|
||||
func (m *Manager) AgentID() string {
|
||||
return m.agentID
|
||||
}
|
||||
|
||||
// EvaluateOpportunity analyzes a council opportunity and decides whether to claim a role.
|
||||
func (m *Manager) EvaluateOpportunity(opportunity *CouncilOpportunity, whooshEndpoint string) error {
|
||||
fmt.Printf("\n🤔 Evaluating council opportunity for: %s\n", opportunity.ProjectName)
|
||||
|
||||
if current := m.currentAssignmentSnapshot(); current != nil {
|
||||
fmt.Printf(" ℹ️ Agent already assigned to council %s as %s; skipping new claims\n", current.CouncilID, current.RoleName)
|
||||
return nil
|
||||
}
|
||||
|
||||
const maxAttempts = 10
|
||||
const retryDelay = 3 * time.Second
|
||||
|
||||
var attemptedAtLeastOne bool
|
||||
|
||||
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||
assignment, attemptedCore, err := m.tryClaimRoles(opportunity.CoreRoles, opportunity, whooshEndpoint, "CORE")
|
||||
attemptedAtLeastOne = attemptedAtLeastOne || attemptedCore
|
||||
if assignment != nil {
|
||||
m.setCurrentAssignment(assignment)
|
||||
return nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, ErrRoleConflict) {
|
||||
return err
|
||||
}
|
||||
|
||||
assignment, attemptedOptional, err := m.tryClaimRoles(opportunity.OptionalRoles, opportunity, whooshEndpoint, "OPTIONAL")
|
||||
attemptedAtLeastOne = attemptedAtLeastOne || attemptedOptional
|
||||
if assignment != nil {
|
||||
m.setCurrentAssignment(assignment)
|
||||
return nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, ErrRoleConflict) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !attemptedAtLeastOne {
|
||||
fmt.Printf(" ✗ No suitable roles found for this agent\n\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf(" ↻ Attempt %d did not secure a council role; retrying in %s...\n", attempt, retryDelay)
|
||||
time.Sleep(retryDelay)
|
||||
}
|
||||
|
||||
return fmt.Errorf("exhausted council role claim attempts for council %s", opportunity.CouncilID)
|
||||
}
|
||||
|
||||
func (m *Manager) tryClaimRoles(roles []CouncilRole, opportunity *CouncilOpportunity, whooshEndpoint string, roleType string) (*RoleAssignment, bool, error) {
|
||||
var attempted bool
|
||||
|
||||
// Shuffle roles deterministically per agent+council to reduce herd on the first role
|
||||
shuffled := append([]CouncilRole(nil), roles...)
|
||||
if len(shuffled) > 1 {
|
||||
h := fnv.New64a()
|
||||
_, _ = h.Write([]byte(m.agentID))
|
||||
_, _ = h.Write([]byte(opportunity.CouncilID))
|
||||
seed := int64(h.Sum64())
|
||||
r := rand.New(rand.NewSource(seed))
|
||||
r.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] })
|
||||
}
|
||||
|
||||
for _, role := range shuffled {
|
||||
if !m.shouldClaimRole(role, opportunity) {
|
||||
continue
|
||||
}
|
||||
|
||||
attempted = true
|
||||
fmt.Printf(" ✓ Attempting to claim %s role: %s (%s)\n", roleType, role.AgentName, role.RoleName)
|
||||
|
||||
assignment, err := m.claimRole(opportunity, role, whooshEndpoint)
|
||||
if assignment != nil {
|
||||
return assignment, attempted, nil
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrRoleConflict) {
|
||||
fmt.Printf(" ⚠️ Role %s already claimed by another agent, trying next role...\n", role.RoleName)
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, attempted, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, attempted, nil
|
||||
}
|
||||
|
||||
func (m *Manager) shouldClaimRole(role CouncilRole, _ *CouncilOpportunity) bool {
|
||||
if m.hasActiveAssignment() {
|
||||
return false
|
||||
}
|
||||
// TODO: implement capability-based selection. For now, opportunistically claim any available role.
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Manager) claimRole(opportunity *CouncilOpportunity, role CouncilRole, whooshEndpoint string) (*RoleAssignment, error) {
|
||||
claimURL := fmt.Sprintf("%s/api/v1/councils/%s/claims", strings.TrimRight(whooshEndpoint, "/"), opportunity.CouncilID)
|
||||
|
||||
claim := map[string]interface{}{
|
||||
"agent_id": m.agentID,
|
||||
"agent_name": m.agentName,
|
||||
"role_name": role.RoleName,
|
||||
"capabilities": m.capabilities,
|
||||
"confidence": 0.75, // TODO: calculate based on capability match quality.
|
||||
"reasoning": fmt.Sprintf("Agent has capabilities matching role: %s", role.RoleName),
|
||||
"endpoint": m.endpoint,
|
||||
"p2p_addr": m.p2pAddr,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(claim)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal claim: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, claimURL, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create claim request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send claim: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
var errorResp map[string]interface{}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&errorResp)
|
||||
|
||||
if resp.StatusCode == http.StatusConflict {
|
||||
reason := "role already claimed"
|
||||
if msg, ok := errorResp["error"].(string); ok && msg != "" {
|
||||
reason = msg
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrRoleConflict, reason)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("claim rejected (status %d): %v", resp.StatusCode, errorResp)
|
||||
}
|
||||
|
||||
var claimResp roleClaimResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&claimResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode claim response: %w", err)
|
||||
}
|
||||
|
||||
assignment := &RoleAssignment{
|
||||
CouncilID: opportunity.CouncilID,
|
||||
RoleName: role.RoleName,
|
||||
UCXLAddress: claimResp.UCXLAddress,
|
||||
Profile: claimResp.RoleProfile,
|
||||
}
|
||||
|
||||
if t, err := time.Parse(time.RFC3339, claimResp.AssignedAt); err == nil {
|
||||
assignment.AssignedAt = t
|
||||
}
|
||||
|
||||
if claimResp.CouncilBrief != nil {
|
||||
assignment.Brief = claimResp.CouncilBrief
|
||||
}
|
||||
|
||||
fmt.Printf("\n✅ ROLE CLAIM ACCEPTED!\n")
|
||||
fmt.Printf(" Council ID: %s\n", opportunity.CouncilID)
|
||||
fmt.Printf(" Role: %s (%s)\n", role.AgentName, role.RoleName)
|
||||
fmt.Printf(" UCXL: %s\n", assignment.UCXLAddress)
|
||||
fmt.Printf(" Assigned At: %s\n", claimResp.AssignedAt)
|
||||
|
||||
if err := m.preparePersonaAndAck(opportunity.CouncilID, role.RoleName, &assignment.Profile, claimResp.CouncilBrief, whooshEndpoint, assignment); err != nil {
|
||||
fmt.Printf(" ⚠️ Persona preparation encountered an issue: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
return assignment, nil
|
||||
}
|
||||
|
||||
func (m *Manager) preparePersonaAndAck(councilID, roleName string, profile *RoleProfile, brief *CouncilBrief, whooshEndpoint string, assignment *RoleAssignment) error {
|
||||
if m.personaLoader == nil {
|
||||
return m.sendPersonaAck(councilID, roleName, whooshEndpoint, nil, "", "failed", []string{"persona loader unavailable"})
|
||||
}
|
||||
|
||||
promptKey := profile.PromptKey
|
||||
if promptKey == "" {
|
||||
promptKey = roleName
|
||||
}
|
||||
|
||||
personaCapabilities := profile.Capabilities
|
||||
personaCapabilities = append([]string{}, personaCapabilities...)
|
||||
|
||||
personaEntry, err := m.personaLoader.Compose(promptKey, profile.DisplayName, "", personaCapabilities)
|
||||
if err != nil {
|
||||
return m.sendPersonaAck(councilID, roleName, whooshEndpoint, nil, "", "failed", []string{err.Error()})
|
||||
}
|
||||
|
||||
hash := sha256.Sum256([]byte(personaEntry.SystemPrompt))
|
||||
personaHash := hex.EncodeToString(hash[:])
|
||||
|
||||
assignment.Persona = personaEntry
|
||||
assignment.PersonaHash = personaHash
|
||||
|
||||
if err := m.sendPersonaAck(councilID, roleName, whooshEndpoint, personaEntry, personaHash, "loaded", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) sendPersonaAck(councilID, roleName, whooshEndpoint string, personaEntry *persona.Persona, personaHash string, status string, errs []string) error {
|
||||
ackURL := fmt.Sprintf("%s/api/v1/councils/%s/roles/%s/personas", strings.TrimRight(whooshEndpoint, "/"), councilID, roleName)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"agent_id": m.agentID,
|
||||
"status": status,
|
||||
"model_provider": defaultModelProvider,
|
||||
"capabilities": m.capabilities,
|
||||
"metadata": map[string]interface{}{
|
||||
"endpoint": m.endpoint,
|
||||
"p2p_addr": m.p2pAddr,
|
||||
"agent_name": m.agentName,
|
||||
},
|
||||
}
|
||||
|
||||
if personaEntry != nil {
|
||||
payload["system_prompt_hash"] = personaHash
|
||||
payload["model_name"] = personaEntry.Model
|
||||
if len(personaEntry.Capabilities) > 0 {
|
||||
payload["capabilities"] = personaEntry.Capabilities
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
payload["errors"] = errs
|
||||
}
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal persona ack: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, ackURL, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create persona ack request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send persona ack: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("persona ack rejected with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
fmt.Printf(" 📫 Persona status '%s' acknowledged by WHOOSH\n", status)
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleCouncilBrief records the design brief assigned to this agent once WHOOSH dispatches it.
|
||||
func (m *Manager) HandleCouncilBrief(councilID, roleName string, brief *CouncilBrief) {
|
||||
if brief == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.currentAssignment == nil {
|
||||
fmt.Printf("⚠️ Received council brief for %s (%s) but agent has no active assignment\n", councilID, roleName)
|
||||
return
|
||||
}
|
||||
|
||||
if m.currentAssignment.CouncilID != councilID || !strings.EqualFold(m.currentAssignment.RoleName, roleName) {
|
||||
fmt.Printf("⚠️ Received council brief for %s (%s) but agent is assigned to %s (%s)\n", councilID, roleName, m.currentAssignment.CouncilID, m.currentAssignment.RoleName)
|
||||
return
|
||||
}
|
||||
|
||||
brief.CouncilID = councilID
|
||||
brief.RoleName = roleName
|
||||
m.currentAssignment.Brief = brief
|
||||
|
||||
fmt.Printf("📦 Design brief received for council %s (%s)\n", councilID, roleName)
|
||||
if brief.BriefURL != "" {
|
||||
fmt.Printf(" Brief URL: %s\n", brief.BriefURL)
|
||||
}
|
||||
if brief.Summary != "" {
|
||||
fmt.Printf(" Summary: %s\n", brief.Summary)
|
||||
}
|
||||
if len(brief.ExpectedArtifacts) > 0 {
|
||||
fmt.Printf(" Expected Artifacts: %v\n", brief.ExpectedArtifacts)
|
||||
}
|
||||
if brief.HMMMTopic != "" {
|
||||
fmt.Printf(" HMMM Topic: %s\n", brief.HMMMTopic)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) hasActiveAssignment() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.currentAssignment != nil
|
||||
}
|
||||
|
||||
func (m *Manager) setCurrentAssignment(assignment *RoleAssignment) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.currentAssignment = assignment
|
||||
}
|
||||
|
||||
func (m *Manager) currentAssignmentSnapshot() *RoleAssignment {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.currentAssignment
|
||||
}
|
||||
|
||||
// GetCurrentAssignment returns the current role assignment (public accessor)
|
||||
func (m *Manager) GetCurrentAssignment() *RoleAssignment {
|
||||
return m.currentAssignmentSnapshot()
|
||||
}
|
||||
|
||||
// roleClaimResponse mirrors WHOOSH role claim response payload.
|
||||
type roleClaimResponse struct {
|
||||
Status string `json:"status"`
|
||||
CouncilID string `json:"council_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
UCXLAddress string `json:"ucxl_address"`
|
||||
AssignedAt string `json:"assigned_at"`
|
||||
RoleProfile RoleProfile `json:"role_profile"`
|
||||
CouncilBrief *CouncilBrief `json:"council_brief"`
|
||||
PersonaStatus string `json:"persona_status"`
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"chorus/internal/council"
|
||||
"chorus/internal/logging"
|
||||
"chorus/pkg/ai"
|
||||
"chorus/pkg/dht"
|
||||
"chorus/pkg/execution"
|
||||
"chorus/pkg/health"
|
||||
"chorus/pkg/shutdown"
|
||||
"chorus/pubsub"
|
||||
@@ -39,6 +45,10 @@ func (r *SharedRuntime) StartAgentMode() error {
|
||||
// Start status reporting
|
||||
go r.statusReporter()
|
||||
|
||||
// Start council brief processing
|
||||
ctx := context.Background()
|
||||
go r.processBriefs(ctx)
|
||||
|
||||
r.Logger.Info("🔍 Listening for peers on container network...")
|
||||
r.Logger.Info("📡 Ready for task coordination and meta-discussion")
|
||||
r.Logger.Info("🎯 HMMM collaborative reasoning enabled")
|
||||
@@ -321,3 +331,206 @@ func (r *SharedRuntime) setupGracefulShutdown(shutdownManager *shutdown.Manager,
|
||||
|
||||
r.Logger.Info("🛡️ Graceful shutdown components registered")
|
||||
}
|
||||
|
||||
// processBriefs polls for council briefs and executes them
|
||||
func (r *SharedRuntime) processBriefs(ctx context.Context) {
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
r.Logger.Info("📦 Brief processing loop started")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
r.Logger.Info("📦 Brief processing loop stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
if r.HTTPServer == nil || r.HTTPServer.CouncilManager == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
assignment := r.HTTPServer.CouncilManager.GetCurrentAssignment()
|
||||
if assignment == nil || assignment.Brief == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we have a brief to execute
|
||||
brief := assignment.Brief
|
||||
if brief.BriefURL == "" && brief.Summary == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
r.Logger.Info("📦 Processing design brief for council %s, role %s", assignment.CouncilID, assignment.RoleName)
|
||||
|
||||
// Execute the brief
|
||||
if err := r.executeBrief(ctx, assignment); err != nil {
|
||||
r.Logger.Error("❌ Failed to execute brief: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
r.Logger.Info("✅ Brief execution completed for council %s", assignment.CouncilID)
|
||||
|
||||
// Clear the brief after execution to prevent re-execution
|
||||
assignment.Brief = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeBrief executes a council brief using the ExecutionEngine
|
||||
func (r *SharedRuntime) executeBrief(ctx context.Context, assignment *council.RoleAssignment) error {
|
||||
brief := assignment.Brief
|
||||
if brief == nil {
|
||||
return fmt.Errorf("no brief to execute")
|
||||
}
|
||||
|
||||
// Create execution engine
|
||||
engine := execution.NewTaskExecutionEngine()
|
||||
|
||||
// Create AI provider factory with proper configuration
|
||||
aiFactory := ai.NewProviderFactory()
|
||||
|
||||
// Register the configured provider
|
||||
providerConfig := ai.ProviderConfig{
|
||||
Type: r.Config.AI.Provider,
|
||||
Endpoint: r.Config.AI.Ollama.Endpoint,
|
||||
DefaultModel: "llama3.1:8b",
|
||||
Timeout: r.Config.AI.Ollama.Timeout,
|
||||
}
|
||||
|
||||
if err := aiFactory.RegisterProvider(r.Config.AI.Provider, providerConfig); err != nil {
|
||||
r.Logger.Warn("⚠️ Failed to register AI provider: %v", err)
|
||||
}
|
||||
|
||||
// Set role mapping with default provider
|
||||
// This ensures GetProviderForRole() can find a provider for any role
|
||||
roleMapping := ai.RoleModelMapping{
|
||||
DefaultProvider: r.Config.AI.Provider,
|
||||
FallbackProvider: r.Config.AI.Provider,
|
||||
Roles: make(map[string]ai.RoleConfig),
|
||||
}
|
||||
aiFactory.SetRoleMapping(roleMapping)
|
||||
|
||||
engineConfig := &execution.EngineConfig{
|
||||
AIProviderFactory: aiFactory,
|
||||
MaxConcurrentTasks: 1,
|
||||
DefaultTimeout: time.Hour,
|
||||
EnableMetrics: true,
|
||||
LogLevel: "info",
|
||||
}
|
||||
|
||||
if err := engine.Initialize(ctx, engineConfig); err != nil {
|
||||
return fmt.Errorf("failed to initialize execution engine: %w", err)
|
||||
}
|
||||
defer engine.Shutdown()
|
||||
|
||||
// Build execution request
|
||||
request := r.buildExecutionRequest(assignment)
|
||||
|
||||
r.Logger.Info("🚀 Executing brief for council %s, role %s", assignment.CouncilID, assignment.RoleName)
|
||||
|
||||
// Track task
|
||||
taskID := fmt.Sprintf("council-%s-%s", assignment.CouncilID, assignment.RoleName)
|
||||
r.TaskTracker.AddTask(taskID)
|
||||
defer r.TaskTracker.RemoveTask(taskID)
|
||||
|
||||
// Execute the task
|
||||
result, err := engine.ExecuteTask(ctx, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("task execution failed: %w", err)
|
||||
}
|
||||
|
||||
r.Logger.Info("✅ Task execution successful. Output: %s", result.Output)
|
||||
|
||||
// Upload results to WHOOSH
|
||||
if err := r.uploadResults(assignment, result); err != nil {
|
||||
r.Logger.Error("⚠️ Failed to upload results to WHOOSH: %v", err)
|
||||
// Don't fail the execution if upload fails
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildExecutionRequest converts a council brief to an execution request
|
||||
func (r *SharedRuntime) buildExecutionRequest(assignment *council.RoleAssignment) *execution.TaskExecutionRequest {
|
||||
brief := assignment.Brief
|
||||
|
||||
// Build task description from brief
|
||||
taskDescription := brief.Summary
|
||||
if taskDescription == "" {
|
||||
taskDescription = "Execute council brief"
|
||||
}
|
||||
|
||||
// Add additional context
|
||||
additionalContext := map[string]interface{}{
|
||||
"council_id": assignment.CouncilID,
|
||||
"role_name": assignment.RoleName,
|
||||
"brief_url": brief.BriefURL,
|
||||
"expected_artifacts": brief.ExpectedArtifacts,
|
||||
"hmmm_topic": brief.HMMMTopic,
|
||||
"persona": assignment.Persona,
|
||||
}
|
||||
|
||||
return &execution.TaskExecutionRequest{
|
||||
ID: fmt.Sprintf("council-%s-%s", assignment.CouncilID, assignment.RoleName),
|
||||
Type: "council_brief",
|
||||
Description: taskDescription,
|
||||
Context: additionalContext,
|
||||
Requirements: &execution.TaskRequirements{
|
||||
AIModel: r.Config.AI.Provider,
|
||||
SandboxType: "docker",
|
||||
RequiredTools: []string{},
|
||||
},
|
||||
Timeout: time.Hour,
|
||||
}
|
||||
}
|
||||
|
||||
// uploadResults uploads execution results to WHOOSH
|
||||
func (r *SharedRuntime) uploadResults(assignment *council.RoleAssignment, result *execution.TaskExecutionResult) error {
|
||||
// Get WHOOSH endpoint from environment or config
|
||||
whooshEndpoint := r.Config.WHOOSHAPI.BaseURL
|
||||
if whooshEndpoint == "" {
|
||||
whooshEndpoint = "http://whoosh:8080"
|
||||
}
|
||||
|
||||
// Build result payload
|
||||
payload := map[string]interface{}{
|
||||
"council_id": assignment.CouncilID,
|
||||
"role_name": assignment.RoleName,
|
||||
"agent_id": r.Config.Agent.ID,
|
||||
"ucxl_address": assignment.UCXLAddress,
|
||||
"output": result.Output,
|
||||
"artifacts": result.Artifacts,
|
||||
"success": result.Success,
|
||||
"error_message": result.ErrorMessage,
|
||||
"execution_time": result.Metrics.Duration.Seconds(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal result payload: %w", err)
|
||||
}
|
||||
|
||||
// Send to WHOOSH
|
||||
url := fmt.Sprintf("%s/api/councils/%s/results", whooshEndpoint, assignment.CouncilID)
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create HTTP request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send results to WHOOSH: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("WHOOSH returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
r.Logger.Info("📤 Results uploaded to WHOOSH for council %s", assignment.CouncilID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"chorus/pkg/dht"
|
||||
"chorus/pkg/election"
|
||||
"chorus/pkg/health"
|
||||
"chorus/pkg/mcp"
|
||||
"chorus/pkg/metrics"
|
||||
"chorus/pkg/prompt"
|
||||
"chorus/pkg/shhh"
|
||||
@@ -682,5 +683,26 @@ func initializeAIProvider(cfg *config.Config, logger *SimpleLogger) error {
|
||||
reasoning.SetDefaultSystemPrompt(d)
|
||||
}
|
||||
|
||||
// Initialize LightRAG client if enabled
|
||||
if cfg.LightRAG.Enabled {
|
||||
lightragConfig := mcp.LightRAGConfig{
|
||||
BaseURL: cfg.LightRAG.BaseURL,
|
||||
Timeout: cfg.LightRAG.Timeout,
|
||||
APIKey: cfg.LightRAG.APIKey,
|
||||
}
|
||||
lightragClient := mcp.NewLightRAGClient(lightragConfig)
|
||||
|
||||
// Test connectivity
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if lightragClient.IsHealthy(ctx) {
|
||||
reasoning.SetLightRAGClient(lightragClient)
|
||||
logger.Info("📚 LightRAG RAG system enabled - Endpoint: %s, Mode: %s",
|
||||
cfg.LightRAG.BaseURL, cfg.LightRAG.DefaultMode)
|
||||
} else {
|
||||
logger.Warn("⚠️ LightRAG enabled but server not healthy at %s", cfg.LightRAG.BaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -408,13 +408,16 @@ func (p *OllamaProvider) getSupportedModels() []string {
|
||||
|
||||
// parseResponseForActions extracts actions and artifacts from the response
|
||||
func (p *OllamaProvider) parseResponseForActions(response string, request *TaskRequest) ([]TaskAction, []Artifact) {
|
||||
var actions []TaskAction
|
||||
var artifacts []Artifact
|
||||
// Use the response parser to extract structured actions and artifacts
|
||||
parser := NewResponseParser()
|
||||
actions, artifacts := parser.ParseResponse(response)
|
||||
|
||||
// This is a simplified implementation - in reality, you'd parse the response
|
||||
// to extract specific actions like file changes, commands to run, etc.
|
||||
// If parser found concrete actions, return them
|
||||
if len(actions) > 0 {
|
||||
return actions, artifacts
|
||||
}
|
||||
|
||||
// For now, just create a basic action indicating task analysis
|
||||
// Otherwise, create a basic task analysis action as fallback
|
||||
action := TaskAction{
|
||||
Type: "task_analysis",
|
||||
Target: request.TaskTitle,
|
||||
|
||||
@@ -477,10 +477,16 @@ func (p *ResetDataProvider) handleHTTPError(statusCode int, body []byte) *Provid
|
||||
|
||||
// parseResponseForActions extracts actions from the response text
|
||||
func (p *ResetDataProvider) parseResponseForActions(response string, request *TaskRequest) ([]TaskAction, []Artifact) {
|
||||
var actions []TaskAction
|
||||
var artifacts []Artifact
|
||||
// Use the response parser to extract structured actions and artifacts
|
||||
parser := NewResponseParser()
|
||||
actions, artifacts := parser.ParseResponse(response)
|
||||
|
||||
// Create a basic task analysis action
|
||||
// If parser found concrete actions, return them
|
||||
if len(actions) > 0 {
|
||||
return actions, artifacts
|
||||
}
|
||||
|
||||
// Otherwise, create a basic task analysis action as fallback
|
||||
action := TaskAction{
|
||||
Type: "task_analysis",
|
||||
Target: request.TaskTitle,
|
||||
|
||||
206
pkg/ai/response_parser.go
Normal file
206
pkg/ai/response_parser.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ResponseParser extracts actions and artifacts from LLM text responses
|
||||
type ResponseParser struct{}
|
||||
|
||||
// NewResponseParser creates a new response parser instance
|
||||
func NewResponseParser() *ResponseParser {
|
||||
return &ResponseParser{}
|
||||
}
|
||||
|
||||
// ParseResponse extracts structured actions and artifacts from LLM response text
|
||||
func (rp *ResponseParser) ParseResponse(response string) ([]TaskAction, []Artifact) {
|
||||
var actions []TaskAction
|
||||
var artifacts []Artifact
|
||||
|
||||
// Extract code blocks with filenames
|
||||
fileBlocks := rp.extractFileBlocks(response)
|
||||
for _, block := range fileBlocks {
|
||||
// Create file creation action
|
||||
action := TaskAction{
|
||||
Type: "file_create",
|
||||
Target: block.Filename,
|
||||
Content: block.Content,
|
||||
Result: "File created from LLM response",
|
||||
Success: true,
|
||||
Timestamp: time.Now(),
|
||||
Metadata: map[string]interface{}{
|
||||
"language": block.Language,
|
||||
},
|
||||
}
|
||||
actions = append(actions, action)
|
||||
|
||||
// Create artifact
|
||||
artifact := Artifact{
|
||||
Name: block.Filename,
|
||||
Type: "file",
|
||||
Path: block.Filename,
|
||||
Content: block.Content,
|
||||
Size: int64(len(block.Content)),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
artifacts = append(artifacts, artifact)
|
||||
}
|
||||
|
||||
// Extract shell commands
|
||||
commands := rp.extractCommands(response)
|
||||
for _, cmd := range commands {
|
||||
action := TaskAction{
|
||||
Type: "command_run",
|
||||
Target: "shell",
|
||||
Content: cmd,
|
||||
Result: "Command extracted from LLM response",
|
||||
Success: true,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
actions = append(actions, action)
|
||||
}
|
||||
|
||||
return actions, artifacts
|
||||
}
|
||||
|
||||
// FileBlock represents a code block with filename
|
||||
type FileBlock struct {
|
||||
Filename string
|
||||
Language string
|
||||
Content string
|
||||
}
|
||||
|
||||
// extractFileBlocks finds code blocks that represent files
|
||||
func (rp *ResponseParser) extractFileBlocks(response string) []FileBlock {
|
||||
var blocks []FileBlock
|
||||
|
||||
// Pattern 1: Markdown code blocks with filename comments
|
||||
// ```language
|
||||
// // filename: path/to/file.ext
|
||||
// content
|
||||
// ```
|
||||
pattern1 := regexp.MustCompile("(?s)```(\\w+)?\\s*\\n(?://|#)\\s*(?:filename|file|path):\\s*([^\\n]+)\\n(.*?)```")
|
||||
matches1 := pattern1.FindAllStringSubmatch(response, -1)
|
||||
for _, match := range matches1 {
|
||||
if len(match) >= 4 {
|
||||
blocks = append(blocks, FileBlock{
|
||||
Filename: strings.TrimSpace(match[2]),
|
||||
Language: match[1],
|
||||
Content: strings.TrimSpace(match[3]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: Filename in backticks followed by "content" and code block
|
||||
// Matches: `filename.ext` ... content ... ```language ... ```
|
||||
// This handles cases like:
|
||||
// - "file named `hello.sh` ... should have the following content: ```bash ... ```"
|
||||
// - "Create `script.py` with this content: ```python ... ```"
|
||||
pattern2 := regexp.MustCompile("`([^`]+)`[^`]*?(?:content|code)[^`]*?```([a-z]+)?\\s*\\n([^`]+)```")
|
||||
matches2 := pattern2.FindAllStringSubmatch(response, -1)
|
||||
for _, match := range matches2 {
|
||||
if len(match) >= 4 {
|
||||
blocks = append(blocks, FileBlock{
|
||||
Filename: strings.TrimSpace(match[1]),
|
||||
Language: match[2],
|
||||
Content: strings.TrimSpace(match[3]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: File header notation
|
||||
// --- filename: path/to/file.ext ---
|
||||
// content
|
||||
// --- end ---
|
||||
pattern3 := regexp.MustCompile("(?s)---\\s*(?:filename|file):\\s*([^\\n]+)\\s*---\\s*\\n(.*?)\\n---\\s*(?:end)?\\s*---")
|
||||
matches3 := pattern3.FindAllStringSubmatch(response, -1)
|
||||
for _, match := range matches3 {
|
||||
if len(match) >= 3 {
|
||||
blocks = append(blocks, FileBlock{
|
||||
Filename: strings.TrimSpace(match[1]),
|
||||
Language: rp.detectLanguage(match[1]),
|
||||
Content: strings.TrimSpace(match[2]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 4: Shell script style file creation
|
||||
// cat > filename.ext << 'EOF'
|
||||
// content
|
||||
// EOF
|
||||
pattern4 := regexp.MustCompile("(?s)cat\\s*>\\s*([^\\s<]+)\\s*<<\\s*['\"]?EOF['\"]?\\s*\\n(.*?)\\nEOF")
|
||||
matches4 := pattern4.FindAllStringSubmatch(response, -1)
|
||||
for _, match := range matches4 {
|
||||
if len(match) >= 3 {
|
||||
blocks = append(blocks, FileBlock{
|
||||
Filename: strings.TrimSpace(match[1]),
|
||||
Language: rp.detectLanguage(match[1]),
|
||||
Content: strings.TrimSpace(match[2]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
// extractCommands extracts shell commands from response
|
||||
func (rp *ResponseParser) extractCommands(response string) []string {
|
||||
var commands []string
|
||||
|
||||
// Pattern: Markdown code blocks marked as bash/sh
|
||||
pattern := regexp.MustCompile("(?s)```(?:bash|sh|shell)\\s*\\n(.*?)```")
|
||||
matches := pattern.FindAllStringSubmatch(response, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) >= 2 {
|
||||
lines := strings.Split(strings.TrimSpace(match[1]), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
// Skip comments and empty lines
|
||||
if line != "" && !strings.HasPrefix(line, "#") {
|
||||
commands = append(commands, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
// detectLanguage attempts to detect language from filename extension
|
||||
func (rp *ResponseParser) detectLanguage(filename string) string {
|
||||
ext := ""
|
||||
if idx := strings.LastIndex(filename, "."); idx != -1 {
|
||||
ext = strings.ToLower(filename[idx+1:])
|
||||
}
|
||||
|
||||
languageMap := map[string]string{
|
||||
"go": "go",
|
||||
"py": "python",
|
||||
"js": "javascript",
|
||||
"ts": "typescript",
|
||||
"java": "java",
|
||||
"cpp": "cpp",
|
||||
"c": "c",
|
||||
"rs": "rust",
|
||||
"sh": "bash",
|
||||
"bash": "bash",
|
||||
"yaml": "yaml",
|
||||
"yml": "yaml",
|
||||
"json": "json",
|
||||
"xml": "xml",
|
||||
"html": "html",
|
||||
"css": "css",
|
||||
"md": "markdown",
|
||||
"txt": "text",
|
||||
"sql": "sql",
|
||||
"rb": "ruby",
|
||||
"php": "php",
|
||||
}
|
||||
|
||||
if lang, ok := languageMap[ext]; ok {
|
||||
return lang
|
||||
}
|
||||
return "text"
|
||||
}
|
||||
@@ -24,6 +24,7 @@ type Config struct {
|
||||
Slurp SlurpConfig `yaml:"slurp"`
|
||||
Security SecurityConfig `yaml:"security"`
|
||||
WHOOSHAPI WHOOSHAPIConfig `yaml:"whoosh_api"`
|
||||
LightRAG LightRAGConfig `yaml:"lightrag"`
|
||||
}
|
||||
|
||||
// AgentConfig defines agent-specific settings
|
||||
@@ -161,6 +162,15 @@ type WHOOSHAPIConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
// LightRAGConfig defines LightRAG RAG service settings
|
||||
type LightRAGConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
BaseURL string `yaml:"base_url"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
APIKey string `yaml:"api_key"`
|
||||
DefaultMode string `yaml:"default_mode"` // naive, local, global, hybrid
|
||||
}
|
||||
|
||||
// LoadFromEnvironment loads configuration from environment variables
|
||||
func LoadFromEnvironment() (*Config, error) {
|
||||
cfg := &Config{
|
||||
@@ -270,6 +280,13 @@ func LoadFromEnvironment() (*Config, error) {
|
||||
Token: os.Getenv("WHOOSH_API_TOKEN"),
|
||||
Enabled: getEnvBoolOrDefault("WHOOSH_API_ENABLED", false),
|
||||
},
|
||||
LightRAG: LightRAGConfig{
|
||||
Enabled: getEnvBoolOrDefault("CHORUS_LIGHTRAG_ENABLED", false),
|
||||
BaseURL: getEnvOrDefault("CHORUS_LIGHTRAG_BASE_URL", "http://127.0.0.1:9621"),
|
||||
Timeout: getEnvDurationOrDefault("CHORUS_LIGHTRAG_TIMEOUT", 30*time.Second),
|
||||
APIKey: os.Getenv("CHORUS_LIGHTRAG_API_KEY"),
|
||||
DefaultMode: getEnvOrDefault("CHORUS_LIGHTRAG_DEFAULT_MODE", "hybrid"),
|
||||
},
|
||||
}
|
||||
|
||||
// Validate required configuration
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chorus/pkg/ai"
|
||||
"chorus/pkg/prompt"
|
||||
)
|
||||
|
||||
// TaskExecutionEngine provides AI-powered task execution with isolated sandboxes
|
||||
@@ -20,12 +22,12 @@ type TaskExecutionEngine interface {
|
||||
|
||||
// TaskExecutionRequest represents a task to be executed
|
||||
type TaskExecutionRequest struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Context map[string]interface{} `json:"context,omitempty"`
|
||||
Requirements *TaskRequirements `json:"requirements,omitempty"`
|
||||
Timeout time.Duration `json:"timeout,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Context map[string]interface{} `json:"context,omitempty"`
|
||||
Requirements *TaskRequirements `json:"requirements,omitempty"`
|
||||
Timeout time.Duration `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// TaskRequirements specifies execution environment needs
|
||||
@@ -51,54 +53,54 @@ type TaskExecutionResult struct {
|
||||
|
||||
// TaskArtifact represents a file or data produced during execution
|
||||
type TaskArtifact struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Content []byte `json:"content,omitempty"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Content []byte `json:"content,omitempty"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ExecutionMetrics tracks resource usage and performance
|
||||
type ExecutionMetrics struct {
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
AIProviderTime time.Duration `json:"ai_provider_time"`
|
||||
SandboxTime time.Duration `json:"sandbox_time"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
AIProviderTime time.Duration `json:"ai_provider_time"`
|
||||
SandboxTime time.Duration `json:"sandbox_time"`
|
||||
ResourceUsage *ResourceUsage `json:"resource_usage,omitempty"`
|
||||
CommandsExecuted int `json:"commands_executed"`
|
||||
FilesGenerated int `json:"files_generated"`
|
||||
CommandsExecuted int `json:"commands_executed"`
|
||||
FilesGenerated int `json:"files_generated"`
|
||||
}
|
||||
|
||||
// EngineConfig configures the task execution engine
|
||||
type EngineConfig struct {
|
||||
AIProviderFactory *ai.ProviderFactory `json:"-"`
|
||||
SandboxDefaults *SandboxConfig `json:"sandbox_defaults"`
|
||||
DefaultTimeout time.Duration `json:"default_timeout"`
|
||||
MaxConcurrentTasks int `json:"max_concurrent_tasks"`
|
||||
EnableMetrics bool `json:"enable_metrics"`
|
||||
LogLevel string `json:"log_level"`
|
||||
AIProviderFactory *ai.ProviderFactory `json:"-"`
|
||||
SandboxDefaults *SandboxConfig `json:"sandbox_defaults"`
|
||||
DefaultTimeout time.Duration `json:"default_timeout"`
|
||||
MaxConcurrentTasks int `json:"max_concurrent_tasks"`
|
||||
EnableMetrics bool `json:"enable_metrics"`
|
||||
LogLevel string `json:"log_level"`
|
||||
}
|
||||
|
||||
// EngineMetrics tracks overall engine performance
|
||||
type EngineMetrics struct {
|
||||
TasksExecuted int64 `json:"tasks_executed"`
|
||||
TasksSuccessful int64 `json:"tasks_successful"`
|
||||
TasksFailed int64 `json:"tasks_failed"`
|
||||
AverageTime time.Duration `json:"average_time"`
|
||||
TasksExecuted int64 `json:"tasks_executed"`
|
||||
TasksSuccessful int64 `json:"tasks_successful"`
|
||||
TasksFailed int64 `json:"tasks_failed"`
|
||||
AverageTime time.Duration `json:"average_time"`
|
||||
TotalExecutionTime time.Duration `json:"total_execution_time"`
|
||||
ActiveTasks int `json:"active_tasks"`
|
||||
ActiveTasks int `json:"active_tasks"`
|
||||
}
|
||||
|
||||
// DefaultTaskExecutionEngine implements the TaskExecutionEngine interface
|
||||
type DefaultTaskExecutionEngine struct {
|
||||
config *EngineConfig
|
||||
aiFactory *ai.ProviderFactory
|
||||
metrics *EngineMetrics
|
||||
activeTasks map[string]context.CancelFunc
|
||||
logger *log.Logger
|
||||
config *EngineConfig
|
||||
aiFactory *ai.ProviderFactory
|
||||
metrics *EngineMetrics
|
||||
activeTasks map[string]context.CancelFunc
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewTaskExecutionEngine creates a new task execution engine
|
||||
@@ -192,26 +194,49 @@ func (e *DefaultTaskExecutionEngine) ExecuteTask(ctx context.Context, request *T
|
||||
|
||||
// executeTaskInternal performs the actual task execution
|
||||
func (e *DefaultTaskExecutionEngine) executeTaskInternal(ctx context.Context, request *TaskExecutionRequest, result *TaskExecutionResult) error {
|
||||
// Step 1: Determine AI model and get provider
|
||||
if request == nil {
|
||||
return fmt.Errorf("task execution request cannot be nil")
|
||||
}
|
||||
|
||||
aiStartTime := time.Now()
|
||||
|
||||
role := e.determineRoleFromTask(request)
|
||||
|
||||
provider, providerConfig, err := e.aiFactory.GetProviderForRole(role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get AI provider for role %s: %w", role, err)
|
||||
}
|
||||
|
||||
// Step 2: Create AI request
|
||||
roleConfig, _ := e.aiFactory.GetRoleConfig(role)
|
||||
|
||||
aiRequest := &ai.TaskRequest{
|
||||
TaskID: request.ID,
|
||||
TaskTitle: request.Type,
|
||||
TaskDescription: request.Description,
|
||||
Context: request.Context,
|
||||
ModelName: providerConfig.DefaultModel,
|
||||
AgentRole: role,
|
||||
TaskID: request.ID,
|
||||
TaskTitle: extractTaskTitle(request),
|
||||
TaskDescription: request.Description,
|
||||
Context: request.Context,
|
||||
AgentRole: role,
|
||||
AgentID: extractAgentID(request.Context),
|
||||
Repository: extractRepository(request.Context),
|
||||
TaskLabels: extractTaskLabels(request.Context),
|
||||
Priority: extractContextInt(request.Context, "priority"),
|
||||
Complexity: extractContextInt(request.Context, "complexity"),
|
||||
ModelName: providerConfig.DefaultModel,
|
||||
Temperature: providerConfig.Temperature,
|
||||
MaxTokens: providerConfig.MaxTokens,
|
||||
WorkingDirectory: extractWorkingDirectory(request.Context),
|
||||
EnableTools: providerConfig.EnableTools || roleConfig.EnableTools,
|
||||
MCPServers: combineStringSlices(providerConfig.MCPServers, roleConfig.MCPServers),
|
||||
AllowedTools: combineStringSlices(roleConfig.AllowedTools, nil),
|
||||
}
|
||||
|
||||
if aiRequest.AgentID == "" {
|
||||
aiRequest.AgentID = request.ID
|
||||
}
|
||||
|
||||
if systemPrompt := e.resolveSystemPrompt(role, roleConfig, request.Context); systemPrompt != "" {
|
||||
aiRequest.SystemPrompt = systemPrompt
|
||||
}
|
||||
|
||||
// Step 3: Get AI response
|
||||
aiResponse, err := provider.ExecuteTask(ctx, aiRequest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("AI provider execution failed: %w", err)
|
||||
@@ -219,14 +244,20 @@ func (e *DefaultTaskExecutionEngine) executeTaskInternal(ctx context.Context, re
|
||||
|
||||
result.Metrics.AIProviderTime = time.Since(aiStartTime)
|
||||
|
||||
// Step 4: Parse AI response for executable commands
|
||||
commands, artifacts, err := e.parseAIResponse(aiResponse)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse AI response: %w", err)
|
||||
}
|
||||
|
||||
// Step 5: Execute commands in sandbox if needed
|
||||
if len(commands) > 0 {
|
||||
// Only execute sandbox if sandbox type is not explicitly disabled (empty string or "none")
|
||||
sandboxType := ""
|
||||
if request.Requirements != nil {
|
||||
sandboxType = request.Requirements.SandboxType
|
||||
}
|
||||
|
||||
shouldExecuteSandbox := len(commands) > 0 && sandboxType != "" && sandboxType != "none"
|
||||
|
||||
if shouldExecuteSandbox {
|
||||
sandboxStartTime := time.Now()
|
||||
|
||||
sandboxResult, err := e.executeSandboxCommands(ctx, request, commands)
|
||||
@@ -238,16 +269,13 @@ func (e *DefaultTaskExecutionEngine) executeTaskInternal(ctx context.Context, re
|
||||
result.Metrics.CommandsExecuted = len(commands)
|
||||
result.Metrics.ResourceUsage = sandboxResult.ResourceUsage
|
||||
|
||||
// Merge sandbox artifacts
|
||||
artifacts = append(artifacts, sandboxResult.Artifacts...)
|
||||
}
|
||||
|
||||
// Step 6: Process results and artifacts
|
||||
result.Output = e.formatOutput(aiResponse, artifacts)
|
||||
result.Artifacts = artifacts
|
||||
result.Metrics.FilesGenerated = len(artifacts)
|
||||
|
||||
// Add metadata
|
||||
result.Metadata = map[string]interface{}{
|
||||
"ai_provider": providerConfig.Type,
|
||||
"ai_model": providerConfig.DefaultModel,
|
||||
@@ -260,26 +288,365 @@ func (e *DefaultTaskExecutionEngine) executeTaskInternal(ctx context.Context, re
|
||||
|
||||
// determineRoleFromTask analyzes the task to determine appropriate AI role
|
||||
func (e *DefaultTaskExecutionEngine) determineRoleFromTask(request *TaskExecutionRequest) string {
|
||||
taskType := strings.ToLower(request.Type)
|
||||
description := strings.ToLower(request.Description)
|
||||
|
||||
// Determine role based on task type and description keywords
|
||||
if strings.Contains(taskType, "code") || strings.Contains(description, "program") ||
|
||||
strings.Contains(description, "script") || strings.Contains(description, "function") {
|
||||
if request == nil {
|
||||
return "developer"
|
||||
}
|
||||
|
||||
if strings.Contains(taskType, "analysis") || strings.Contains(description, "analyze") ||
|
||||
strings.Contains(description, "review") {
|
||||
return "analyst"
|
||||
if role := extractRoleFromContext(request.Context); role != "" {
|
||||
return role
|
||||
}
|
||||
|
||||
if strings.Contains(taskType, "test") || strings.Contains(description, "test") {
|
||||
return "tester"
|
||||
typeLower := strings.ToLower(request.Type)
|
||||
descriptionLower := strings.ToLower(request.Description)
|
||||
|
||||
switch {
|
||||
case strings.Contains(typeLower, "security") || strings.Contains(descriptionLower, "security"):
|
||||
return normalizeRole("security")
|
||||
case strings.Contains(typeLower, "test") || strings.Contains(descriptionLower, "test"):
|
||||
return normalizeRole("tester")
|
||||
case strings.Contains(typeLower, "review") || strings.Contains(descriptionLower, "review"):
|
||||
return normalizeRole("reviewer")
|
||||
case strings.Contains(typeLower, "design") || strings.Contains(typeLower, "architecture") || strings.Contains(descriptionLower, "architecture") || strings.Contains(descriptionLower, "design"):
|
||||
return normalizeRole("architect")
|
||||
case strings.Contains(typeLower, "analysis") || strings.Contains(descriptionLower, "analysis") || strings.Contains(descriptionLower, "analyz"):
|
||||
return normalizeRole("systems analyst")
|
||||
case strings.Contains(typeLower, "doc") || strings.Contains(descriptionLower, "documentation") || strings.Contains(descriptionLower, "docs"):
|
||||
return normalizeRole("technical writer")
|
||||
default:
|
||||
return normalizeRole("developer")
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DefaultTaskExecutionEngine) resolveSystemPrompt(role string, roleConfig ai.RoleConfig, ctx map[string]interface{}) string {
|
||||
if promptText := extractSystemPromptFromContext(ctx); promptText != "" {
|
||||
return promptText
|
||||
}
|
||||
if strings.TrimSpace(roleConfig.SystemPrompt) != "" {
|
||||
return strings.TrimSpace(roleConfig.SystemPrompt)
|
||||
}
|
||||
if role != "" {
|
||||
if composed, err := prompt.ComposeSystemPrompt(role); err == nil && strings.TrimSpace(composed) != "" {
|
||||
return composed
|
||||
}
|
||||
}
|
||||
if defaultInstr := prompt.GetDefaultInstructions(); strings.TrimSpace(defaultInstr) != "" {
|
||||
return strings.TrimSpace(defaultInstr)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractRoleFromContext(ctx map[string]interface{}) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Default to general purpose
|
||||
return "general"
|
||||
if rolesVal, ok := ctx["required_roles"]; ok {
|
||||
if roles := convertToStringSlice(rolesVal); len(roles) > 0 {
|
||||
for _, role := range roles {
|
||||
if normalized := normalizeRole(role); normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
extractStringFromContext(ctx, "required_role"),
|
||||
extractStringFromContext(ctx, "role"),
|
||||
extractStringFromContext(ctx, "agent_role"),
|
||||
extractStringFromNestedMap(ctx, "agent_info", "role"),
|
||||
extractStringFromNestedMap(ctx, "task_metadata", "required_role"),
|
||||
extractStringFromNestedMap(ctx, "task_metadata", "role"),
|
||||
extractStringFromNestedMap(ctx, "council", "role"),
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if normalized := normalizeRole(candidate); normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractSystemPromptFromContext(ctx map[string]interface{}) string {
|
||||
if promptText := extractStringFromContext(ctx, "system_prompt"); promptText != "" {
|
||||
return promptText
|
||||
}
|
||||
if promptText := extractStringFromNestedMap(ctx, "task_metadata", "system_prompt"); promptText != "" {
|
||||
return promptText
|
||||
}
|
||||
if promptText := extractStringFromNestedMap(ctx, "council", "system_prompt"); promptText != "" {
|
||||
return promptText
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractTaskTitle(request *TaskExecutionRequest) string {
|
||||
if request == nil {
|
||||
return ""
|
||||
}
|
||||
if title := extractStringFromContext(request.Context, "task_title"); title != "" {
|
||||
return title
|
||||
}
|
||||
if title := extractStringFromNestedMap(request.Context, "task_metadata", "title"); title != "" {
|
||||
return title
|
||||
}
|
||||
if request.Type != "" {
|
||||
return request.Type
|
||||
}
|
||||
return request.ID
|
||||
}
|
||||
|
||||
func extractRepository(ctx map[string]interface{}) string {
|
||||
if repo := extractStringFromContext(ctx, "repository"); repo != "" {
|
||||
return repo
|
||||
}
|
||||
if repo := extractStringFromNestedMap(ctx, "task_metadata", "repository"); repo != "" {
|
||||
return repo
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractAgentID(ctx map[string]interface{}) string {
|
||||
if id := extractStringFromContext(ctx, "agent_id"); id != "" {
|
||||
return id
|
||||
}
|
||||
if id := extractStringFromNestedMap(ctx, "agent_info", "id"); id != "" {
|
||||
return id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractWorkingDirectory(ctx map[string]interface{}) string {
|
||||
if dir := extractStringFromContext(ctx, "working_directory"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
if dir := extractStringFromNestedMap(ctx, "task_metadata", "working_directory"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractTaskLabels(ctx map[string]interface{}) []string {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
labels := convertToStringSlice(ctx["labels"])
|
||||
if meta, ok := ctx["task_metadata"].(map[string]interface{}); ok {
|
||||
labels = append(labels, convertToStringSlice(meta["labels"])...)
|
||||
}
|
||||
|
||||
return uniqueStrings(labels)
|
||||
}
|
||||
|
||||
func convertToStringSlice(value interface{}) []string {
|
||||
switch v := value.(type) {
|
||||
case []string:
|
||||
result := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
item = strings.TrimSpace(item)
|
||||
if item != "" {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
case []interface{}:
|
||||
result := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if str, ok := item.(string); ok {
|
||||
str = strings.TrimSpace(str)
|
||||
if str != "" {
|
||||
result = append(result, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
case string:
|
||||
trimmed := strings.TrimSpace(v)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(trimmed, ",")
|
||||
if len(parts) == 1 {
|
||||
return []string{trimmed}
|
||||
}
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
p := strings.TrimSpace(part)
|
||||
if p != "" {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueStrings(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[trimmed]; exists {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func extractContextInt(ctx map[string]interface{}, key string) int {
|
||||
if ctx == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
if value, ok := ctx[key]; ok {
|
||||
if intVal, ok := toInt(value); ok {
|
||||
return intVal
|
||||
}
|
||||
}
|
||||
|
||||
if meta, ok := ctx["task_metadata"].(map[string]interface{}); ok {
|
||||
if value, ok := meta[key]; ok {
|
||||
if intVal, ok := toInt(value); ok {
|
||||
return intVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func toInt(value interface{}) (int, bool) {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v, true
|
||||
case int32:
|
||||
return int(v), true
|
||||
case int64:
|
||||
return int(v), true
|
||||
case float64:
|
||||
return int(v), true
|
||||
case float32:
|
||||
return int(v), true
|
||||
case string:
|
||||
trimmed := strings.TrimSpace(v)
|
||||
if trimmed == "" {
|
||||
return 0, false
|
||||
}
|
||||
parsed, err := strconv.Atoi(trimmed)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return parsed, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func extractStringFromContext(ctx map[string]interface{}, key string) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if value, ok := ctx[key]; ok {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(v)
|
||||
case fmt.Stringer:
|
||||
return strings.TrimSpace(v.String())
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractStringFromNestedMap(ctx map[string]interface{}, parentKey, key string) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
nested, ok := ctx[parentKey].(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return getStringFromMap(nested, key)
|
||||
}
|
||||
|
||||
func getStringFromMap(m map[string]interface{}, key string) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if value, ok := m[key]; ok {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(v)
|
||||
case fmt.Stringer:
|
||||
return strings.TrimSpace(v.String())
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func combineStringSlices(base []string, extra []string) []string {
|
||||
if len(base) == 0 && len(extra) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
combined := make([]string, 0, len(base)+len(extra))
|
||||
|
||||
appendValues := func(values []string) {
|
||||
for _, value := range values {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[trimmed]; exists {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
combined = append(combined, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
appendValues(base)
|
||||
appendValues(extra)
|
||||
|
||||
if len(combined) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return combined
|
||||
}
|
||||
|
||||
func normalizeRole(role string) string {
|
||||
role = strings.TrimSpace(role)
|
||||
if role == "" {
|
||||
return ""
|
||||
}
|
||||
role = strings.ToLower(role)
|
||||
role = strings.ReplaceAll(role, "_", "-")
|
||||
role = strings.ReplaceAll(role, " ", "-")
|
||||
role = strings.ReplaceAll(role, "--", "-")
|
||||
return role
|
||||
}
|
||||
|
||||
// parseAIResponse extracts executable commands and artifacts from AI response
|
||||
@@ -501,4 +868,4 @@ func (e *DefaultTaskExecutionEngine) Shutdown() error {
|
||||
|
||||
e.logger.Printf("TaskExecutionEngine shutdown complete")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
265
pkg/mcp/lightrag_client.go
Normal file
265
pkg/mcp/lightrag_client.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LightRAGClient provides access to LightRAG MCP server
|
||||
type LightRAGClient struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
apiKey string // Optional API key for authentication
|
||||
}
|
||||
|
||||
// LightRAGConfig holds configuration for LightRAG client
|
||||
type LightRAGConfig struct {
|
||||
BaseURL string // e.g., "http://127.0.0.1:9621"
|
||||
Timeout time.Duration // HTTP timeout
|
||||
APIKey string // Optional API key
|
||||
}
|
||||
|
||||
// QueryMode represents LightRAG query modes
|
||||
type QueryMode string
|
||||
|
||||
const (
|
||||
QueryModeNaive QueryMode = "naive" // Simple semantic search
|
||||
QueryModeLocal QueryMode = "local" // Local graph traversal
|
||||
QueryModeGlobal QueryMode = "global" // Global graph analysis
|
||||
QueryModeHybrid QueryMode = "hybrid" // Combined approach
|
||||
)
|
||||
|
||||
// QueryRequest represents a LightRAG query request
|
||||
type QueryRequest struct {
|
||||
Query string `json:"query"`
|
||||
Mode QueryMode `json:"mode"`
|
||||
OnlyNeedContext bool `json:"only_need_context,omitempty"`
|
||||
}
|
||||
|
||||
// QueryResponse represents a LightRAG query response
|
||||
type QueryResponse struct {
|
||||
Response string `json:"response"`
|
||||
Context string `json:"context,omitempty"`
|
||||
}
|
||||
|
||||
// InsertRequest represents a LightRAG document insertion request
|
||||
type InsertRequest struct {
|
||||
Text string `json:"text"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// InsertResponse represents a LightRAG insertion response
|
||||
type InsertResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// HealthResponse represents LightRAG health check response
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
WorkingDirectory string `json:"working_directory"`
|
||||
InputDirectory string `json:"input_directory"`
|
||||
Configuration map[string]interface{} `json:"configuration"`
|
||||
AuthMode string `json:"auth_mode"`
|
||||
PipelineBusy bool `json:"pipeline_busy"`
|
||||
KeyedLocks map[string]interface{} `json:"keyed_locks"`
|
||||
CoreVersion string `json:"core_version"`
|
||||
APIVersion string `json:"api_version"`
|
||||
WebUITitle string `json:"webui_title"`
|
||||
WebUIDescription string `json:"webui_description"`
|
||||
}
|
||||
|
||||
// NewLightRAGClient creates a new LightRAG MCP client
|
||||
func NewLightRAGClient(config LightRAGConfig) *LightRAGClient {
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
return &LightRAGClient{
|
||||
baseURL: config.BaseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: config.Timeout,
|
||||
},
|
||||
apiKey: config.APIKey,
|
||||
}
|
||||
}
|
||||
|
||||
// Query performs a RAG query against LightRAG
|
||||
func (c *LightRAGClient) Query(ctx context.Context, query string, mode QueryMode) (*QueryResponse, error) {
|
||||
req := QueryRequest{
|
||||
Query: query,
|
||||
Mode: mode,
|
||||
}
|
||||
|
||||
respData, err := c.post(ctx, "/query", req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query failed: %w", err)
|
||||
}
|
||||
|
||||
var response QueryResponse
|
||||
if err := json.Unmarshal(respData, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// QueryWithContext performs a RAG query and returns both response and context
|
||||
func (c *LightRAGClient) QueryWithContext(ctx context.Context, query string, mode QueryMode) (*QueryResponse, error) {
|
||||
req := QueryRequest{
|
||||
Query: query,
|
||||
Mode: mode,
|
||||
OnlyNeedContext: false, // Get both response and context
|
||||
}
|
||||
|
||||
respData, err := c.post(ctx, "/query", req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query with context failed: %w", err)
|
||||
}
|
||||
|
||||
var response QueryResponse
|
||||
if err := json.Unmarshal(respData, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// GetContext retrieves context without generating a response
|
||||
func (c *LightRAGClient) GetContext(ctx context.Context, query string, mode QueryMode) (string, error) {
|
||||
req := QueryRequest{
|
||||
Query: query,
|
||||
Mode: mode,
|
||||
OnlyNeedContext: true,
|
||||
}
|
||||
|
||||
respData, err := c.post(ctx, "/query", req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get context failed: %w", err)
|
||||
}
|
||||
|
||||
var response QueryResponse
|
||||
if err := json.Unmarshal(respData, &response); err != nil {
|
||||
return "", fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
return response.Context, nil
|
||||
}
|
||||
|
||||
// Insert adds a document to the LightRAG knowledge base
|
||||
func (c *LightRAGClient) Insert(ctx context.Context, text, description string) error {
|
||||
req := InsertRequest{
|
||||
Text: text,
|
||||
Description: description,
|
||||
}
|
||||
|
||||
respData, err := c.post(ctx, "/insert", req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert failed: %w", err)
|
||||
}
|
||||
|
||||
var response InsertResponse
|
||||
if err := json.Unmarshal(respData, &response); err != nil {
|
||||
return fmt.Errorf("failed to parse insert response: %w", err)
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return fmt.Errorf("insert failed: %s", response.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Health checks the health of the LightRAG server
|
||||
func (c *LightRAGClient) Health(ctx context.Context) (*HealthResponse, error) {
|
||||
respData, err := c.get(ctx, "/health")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("health check failed: %w", err)
|
||||
}
|
||||
|
||||
var response HealthResponse
|
||||
if err := json.Unmarshal(respData, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse health response: %w", err)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// IsHealthy checks if LightRAG server is healthy
|
||||
func (c *LightRAGClient) IsHealthy(ctx context.Context) bool {
|
||||
health, err := c.Health(ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return health.Status == "healthy"
|
||||
}
|
||||
|
||||
// post performs an HTTP POST request
|
||||
func (c *LightRAGClient) post(ctx context.Context, endpoint string, body interface{}) ([]byte, error) {
|
||||
jsonData, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+endpoint, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if c.apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(respData))
|
||||
}
|
||||
|
||||
return respData, nil
|
||||
}
|
||||
|
||||
// get performs an HTTP GET request
|
||||
func (c *LightRAGClient) get(ctx context.Context, endpoint string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
if c.apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(respData))
|
||||
}
|
||||
|
||||
return respData, nil
|
||||
}
|
||||
243
pkg/mcp/lightrag_client_test.go
Normal file
243
pkg/mcp/lightrag_client_test.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestLightRAGClient_NewClient tests client creation
|
||||
func TestLightRAGClient_NewClient(t *testing.T) {
|
||||
config := LightRAGConfig{
|
||||
BaseURL: "http://127.0.0.1:9621",
|
||||
Timeout: 10 * time.Second,
|
||||
APIKey: "",
|
||||
}
|
||||
|
||||
client := NewLightRAGClient(config)
|
||||
if client == nil {
|
||||
t.Fatal("expected non-nil client")
|
||||
}
|
||||
|
||||
if client.baseURL != config.BaseURL {
|
||||
t.Errorf("expected baseURL %s, got %s", config.BaseURL, client.baseURL)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLightRAGClient_Health tests health check
|
||||
// NOTE: This test requires a running LightRAG server at 127.0.0.1:9621
|
||||
func TestLightRAGClient_Health(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := LightRAGConfig{
|
||||
BaseURL: "http://127.0.0.1:9621",
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
client := NewLightRAGClient(config)
|
||||
ctx := context.Background()
|
||||
|
||||
health, err := client.Health(ctx)
|
||||
if err != nil {
|
||||
t.Logf("Health check failed (server may not be running): %v", err)
|
||||
t.Skip("skipping test - lightrag server not available")
|
||||
return
|
||||
}
|
||||
|
||||
if health.Status != "healthy" {
|
||||
t.Errorf("expected status 'healthy', got '%s'", health.Status)
|
||||
}
|
||||
|
||||
t.Logf("LightRAG Health: %s", health.Status)
|
||||
t.Logf("Core Version: %s", health.CoreVersion)
|
||||
t.Logf("API Version: %s", health.APIVersion)
|
||||
}
|
||||
|
||||
// TestLightRAGClient_IsHealthy tests the convenience health check
|
||||
func TestLightRAGClient_IsHealthy(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := LightRAGConfig{
|
||||
BaseURL: "http://127.0.0.1:9621",
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
client := NewLightRAGClient(config)
|
||||
ctx := context.Background()
|
||||
|
||||
healthy := client.IsHealthy(ctx)
|
||||
if !healthy {
|
||||
t.Log("Server not healthy (may not be running)")
|
||||
t.Skip("skipping test - lightrag server not available")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLightRAGClient_Query tests querying with different modes
|
||||
func TestLightRAGClient_Query(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := LightRAGConfig{
|
||||
BaseURL: "http://127.0.0.1:9621",
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client := NewLightRAGClient(config)
|
||||
ctx := context.Background()
|
||||
|
||||
// First check if server is available
|
||||
if !client.IsHealthy(ctx) {
|
||||
t.Skip("skipping test - lightrag server not available")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
query string
|
||||
mode QueryMode
|
||||
}{
|
||||
{
|
||||
name: "naive mode",
|
||||
query: "What is CHORUS?",
|
||||
mode: QueryModeNaive,
|
||||
},
|
||||
{
|
||||
name: "local mode",
|
||||
query: "How does P2P networking work?",
|
||||
mode: QueryModeLocal,
|
||||
},
|
||||
{
|
||||
name: "global mode",
|
||||
query: "What are the main components?",
|
||||
mode: QueryModeGlobal,
|
||||
},
|
||||
{
|
||||
name: "hybrid mode",
|
||||
query: "Explain the architecture",
|
||||
mode: QueryModeHybrid,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
response, err := client.Query(ctx, tc.query, tc.mode)
|
||||
if err != nil {
|
||||
t.Logf("Query failed: %v", err)
|
||||
return // Non-fatal - may just have empty knowledge base
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
t.Error("expected non-nil response")
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("Query: %s", tc.query)
|
||||
t.Logf("Mode: %s", tc.mode)
|
||||
t.Logf("Response length: %d chars", len(response.Response))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLightRAGClient_GetContext tests context retrieval
|
||||
func TestLightRAGClient_GetContext(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := LightRAGConfig{
|
||||
BaseURL: "http://127.0.0.1:9621",
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client := NewLightRAGClient(config)
|
||||
ctx := context.Background()
|
||||
|
||||
if !client.IsHealthy(ctx) {
|
||||
t.Skip("skipping test - lightrag server not available")
|
||||
}
|
||||
|
||||
context, err := client.GetContext(ctx, "distributed systems", QueryModeHybrid)
|
||||
if err != nil {
|
||||
t.Logf("GetContext failed: %v", err)
|
||||
return // Non-fatal
|
||||
}
|
||||
|
||||
t.Logf("Context length: %d chars", len(context))
|
||||
}
|
||||
|
||||
// TestLightRAGClient_Insert tests document insertion
|
||||
func TestLightRAGClient_Insert(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := LightRAGConfig{
|
||||
BaseURL: "http://127.0.0.1:9621",
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client := NewLightRAGClient(config)
|
||||
ctx := context.Background()
|
||||
|
||||
if !client.IsHealthy(ctx) {
|
||||
t.Skip("skipping test - lightrag server not available")
|
||||
}
|
||||
|
||||
text := `CHORUS is a distributed task coordination system built on P2P networking.
|
||||
It uses libp2p for peer-to-peer communication and implements democratic leader election.
|
||||
Tasks are executed in Docker sandboxes for security and isolation.`
|
||||
|
||||
description := "CHORUS system overview"
|
||||
|
||||
err := client.Insert(ctx, text, description)
|
||||
if err != nil {
|
||||
t.Errorf("Insert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("Document inserted successfully")
|
||||
|
||||
// Give time for indexing
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Try to query the inserted document
|
||||
response, err := client.Query(ctx, "What is CHORUS?", QueryModeHybrid)
|
||||
if err != nil {
|
||||
t.Logf("Query after insert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("Query response after insert: %s", response.Response)
|
||||
}
|
||||
|
||||
// TestLightRAGClient_QueryWithContext tests retrieving both response and context
|
||||
func TestLightRAGClient_QueryWithContext(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
config := LightRAGConfig{
|
||||
BaseURL: "http://127.0.0.1:9621",
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
client := NewLightRAGClient(config)
|
||||
ctx := context.Background()
|
||||
|
||||
if !client.IsHealthy(ctx) {
|
||||
t.Skip("skipping test - lightrag server not available")
|
||||
}
|
||||
|
||||
response, err := client.QueryWithContext(ctx, "distributed coordination", QueryModeHybrid)
|
||||
if err != nil {
|
||||
t.Logf("QueryWithContext failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("Response: %s", response.Response)
|
||||
t.Logf("Context: %s", response.Context)
|
||||
}
|
||||
@@ -102,6 +102,7 @@ const (
|
||||
StatusCollaborating AgentStatus = "collaborating"
|
||||
StatusEscalating AgentStatus = "escalating"
|
||||
StatusTerminating AgentStatus = "terminating"
|
||||
StatusOffline AgentStatus = "offline"
|
||||
)
|
||||
|
||||
// AgentTask represents a task being worked on by an agent
|
||||
@@ -427,7 +428,7 @@ func (s *McpServer) processMCPMessage(message map[string]interface{}) (map[strin
|
||||
case "tools/call":
|
||||
return s.callTool(params)
|
||||
case "resources/list":
|
||||
return s.listResources(), nil
|
||||
return s.listResources()
|
||||
case "resources/read":
|
||||
return s.readResource(params)
|
||||
default:
|
||||
@@ -625,4 +626,347 @@ type Relation struct {
|
||||
Type string
|
||||
Strength float64
|
||||
Evidence []string
|
||||
}
|
||||
|
||||
// REST API handlers
|
||||
|
||||
func (s *McpServer) handleAgentsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
s.agentsMutex.RLock()
|
||||
defer s.agentsMutex.RUnlock()
|
||||
|
||||
agents := make([]map[string]interface{}, 0, len(s.agents))
|
||||
for _, agent := range s.agents {
|
||||
agent.mutex.RLock()
|
||||
agents = append(agents, map[string]interface{}{
|
||||
"id": agent.ID,
|
||||
"role": agent.Role,
|
||||
"status": agent.Status,
|
||||
"specialization": agent.Specialization,
|
||||
"capabilities": agent.Capabilities,
|
||||
"current_tasks": len(agent.CurrentTasks),
|
||||
"max_tasks": agent.MaxTasks,
|
||||
})
|
||||
agent.mutex.RUnlock()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"agents": agents,
|
||||
"total": len(agents),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *McpServer) handleConversationsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
// Collect all active conversation threads from agents
|
||||
conversations := make([]map[string]interface{}, 0)
|
||||
|
||||
s.agentsMutex.RLock()
|
||||
for _, agent := range s.agents {
|
||||
agent.mutex.RLock()
|
||||
for threadID, thread := range agent.ActiveThreads {
|
||||
conversations = append(conversations, map[string]interface{}{
|
||||
"id": threadID,
|
||||
"topic": thread.Topic,
|
||||
"state": thread.State,
|
||||
"participants": len(thread.Participants),
|
||||
"created_at": thread.CreatedAt,
|
||||
})
|
||||
}
|
||||
agent.mutex.RUnlock()
|
||||
}
|
||||
s.agentsMutex.RUnlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"conversations": conversations,
|
||||
"total": len(conversations),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *McpServer) handleStatsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
s.stats.mutex.RLock()
|
||||
defer s.stats.mutex.RUnlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"start_time": s.stats.StartTime,
|
||||
"uptime_seconds": time.Since(s.stats.StartTime).Seconds(),
|
||||
"total_requests": s.stats.TotalRequests,
|
||||
"active_agents": s.stats.ActiveAgents,
|
||||
"messages_processed": s.stats.MessagesProcessed,
|
||||
"tokens_consumed": s.stats.TokensConsumed,
|
||||
"average_cost_per_task": s.stats.AverageCostPerTask,
|
||||
"error_rate": s.stats.ErrorRate,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *McpServer) handleHealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
s.agentsMutex.RLock()
|
||||
agentCount := len(s.agents)
|
||||
s.agentsMutex.RUnlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"active_agents": agentCount,
|
||||
"uptime": time.Since(s.stats.StartTime).String(),
|
||||
})
|
||||
}
|
||||
|
||||
// Message handlers
|
||||
|
||||
func (s *McpServer) handleBzzzMessages() {
|
||||
// Subscribe to BZZZ messages via pubsub
|
||||
if s.pubsub == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Listen for BZZZ coordination messages
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
// Process BZZZ messages from pubsub
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *McpServer) handleHmmmMessages() {
|
||||
// Subscribe to HMMM messages via pubsub
|
||||
if s.pubsub == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Listen for HMMM discussion messages
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
// Process HMMM messages from pubsub
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *McpServer) periodicTasks() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Update agent statistics
|
||||
s.agentsMutex.RLock()
|
||||
s.stats.mutex.Lock()
|
||||
s.stats.ActiveAgents = len(s.agents)
|
||||
s.stats.mutex.Unlock()
|
||||
s.agentsMutex.RUnlock()
|
||||
|
||||
// Re-announce agents periodically
|
||||
s.agentsMutex.RLock()
|
||||
for _, agent := range s.agents {
|
||||
if time.Since(agent.LastAnnouncement) > 5*time.Minute {
|
||||
s.announceAgent(agent)
|
||||
}
|
||||
}
|
||||
s.agentsMutex.RUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Agent management
|
||||
|
||||
func (s *McpServer) stopAgent(agent *GPTAgent) {
|
||||
agent.mutex.Lock()
|
||||
defer agent.mutex.Unlock()
|
||||
|
||||
// Update status
|
||||
agent.Status = StatusOffline
|
||||
|
||||
// Clean up active tasks
|
||||
for taskID := range agent.CurrentTasks {
|
||||
delete(agent.CurrentTasks, taskID)
|
||||
}
|
||||
|
||||
// Clean up active threads
|
||||
for threadID := range agent.ActiveThreads {
|
||||
delete(agent.ActiveThreads, threadID)
|
||||
}
|
||||
|
||||
s.hlog.Append(logging.PeerLeft, map[string]interface{}{
|
||||
"agent_id": agent.ID,
|
||||
"role": string(agent.Role),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *McpServer) initiateCollaboration(thread *ConversationThread) error {
|
||||
// Send collaboration invitation to all participants
|
||||
for _, participant := range thread.Participants {
|
||||
s.agentsMutex.RLock()
|
||||
agent, exists := s.agents[participant.AgentID]
|
||||
s.agentsMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update participant status
|
||||
agent.mutex.Lock()
|
||||
participant.Status = ParticipantStatusActive
|
||||
agent.mutex.Unlock()
|
||||
|
||||
// Log collaboration start
|
||||
s.hlog.Append(logging.Collaboration, map[string]interface{}{
|
||||
"event": "collaboration_started",
|
||||
"thread_id": thread.ID,
|
||||
"agent_id": agent.ID,
|
||||
"role": string(agent.Role),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MCP tool listing
|
||||
|
||||
func (s *McpServer) listTools() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"tools": []map[string]interface{}{
|
||||
{
|
||||
"name": "chorus_announce",
|
||||
"description": "Announce agent availability to CHORUS network",
|
||||
"parameters": map[string]interface{}{
|
||||
"agent_id": "string",
|
||||
"capabilities": "array",
|
||||
"specialization": "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "chorus_lookup",
|
||||
"description": "Look up available agents by capability or role",
|
||||
"parameters": map[string]interface{}{
|
||||
"capability": "string",
|
||||
"role": "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "chorus_get",
|
||||
"description": "Retrieve context or data from CHORUS DHT",
|
||||
"parameters": map[string]interface{}{
|
||||
"key": "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "chorus_store",
|
||||
"description": "Store data in CHORUS DHT",
|
||||
"parameters": map[string]interface{}{
|
||||
"key": "string",
|
||||
"value": "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "chorus_collaborate",
|
||||
"description": "Request multi-agent collaboration on a task",
|
||||
"parameters": map[string]interface{}{
|
||||
"task": "object",
|
||||
"required_roles": "array",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MCP resource handling
|
||||
|
||||
func (s *McpServer) listResources() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"resources": []map[string]interface{}{
|
||||
{
|
||||
"uri": "chorus://agents",
|
||||
"name": "Available Agents",
|
||||
"description": "List of all available CHORUS agents",
|
||||
"mimeType": "application/json",
|
||||
},
|
||||
{
|
||||
"uri": "chorus://dht",
|
||||
"name": "DHT Storage",
|
||||
"description": "Access to distributed hash table storage",
|
||||
"mimeType": "application/json",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *McpServer) readResource(params map[string]interface{}) (map[string]interface{}, error) {
|
||||
uri, ok := params["uri"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing uri parameter")
|
||||
}
|
||||
|
||||
switch uri {
|
||||
case "chorus://agents":
|
||||
s.agentsMutex.RLock()
|
||||
defer s.agentsMutex.RUnlock()
|
||||
|
||||
agents := make([]map[string]interface{}, 0, len(s.agents))
|
||||
for _, agent := range s.agents {
|
||||
agents = append(agents, map[string]interface{}{
|
||||
"id": agent.ID,
|
||||
"role": agent.Role,
|
||||
"status": agent.Status,
|
||||
})
|
||||
}
|
||||
return map[string]interface{}{"agents": agents}, nil
|
||||
|
||||
case "chorus://dht":
|
||||
return map[string]interface{}{"message": "DHT access not implemented"}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown resource: %s", uri)
|
||||
}
|
||||
}
|
||||
|
||||
// BZZZ tool handlers
|
||||
|
||||
func (s *McpServer) handleBzzzLookup(params map[string]interface{}) (map[string]interface{}, error) {
|
||||
// Stub: Lookup agents or resources via BZZZ
|
||||
return map[string]interface{}{
|
||||
"results": []interface{}{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *McpServer) handleBzzzGet(params map[string]interface{}) (map[string]interface{}, error) {
|
||||
// Stub: Get data from BZZZ system
|
||||
return map[string]interface{}{
|
||||
"data": nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *McpServer) handleBzzzPost(params map[string]interface{}) (map[string]interface{}, error) {
|
||||
// Stub: Post data to BZZZ system
|
||||
return map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "not implemented",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *McpServer) handleBzzzThread(params map[string]interface{}) (map[string]interface{}, error) {
|
||||
// Stub: Handle BZZZ thread operations
|
||||
return map[string]interface{}{
|
||||
"thread": nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *McpServer) handleBzzzSubscribe(params map[string]interface{}) (map[string]interface{}, error) {
|
||||
// Stub: Subscribe to BZZZ events
|
||||
return map[string]interface{}{
|
||||
"subscribed": false,
|
||||
"message": "not implemented",
|
||||
}, nil
|
||||
}
|
||||
218
pkg/slurp/context/lightrag.go
Normal file
218
pkg/slurp/context/lightrag.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"chorus/pkg/mcp"
|
||||
"chorus/pkg/ucxl"
|
||||
)
|
||||
|
||||
// LightRAGEnricher enriches context nodes with RAG-retrieved information
|
||||
type LightRAGEnricher struct {
|
||||
client *mcp.LightRAGClient
|
||||
defaultMode mcp.QueryMode
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewLightRAGEnricher creates a new LightRAG context enricher
|
||||
func NewLightRAGEnricher(client *mcp.LightRAGClient, defaultMode string) *LightRAGEnricher {
|
||||
if client == nil {
|
||||
return &LightRAGEnricher{enabled: false}
|
||||
}
|
||||
|
||||
mode := mcp.QueryModeHybrid // Default to hybrid
|
||||
switch defaultMode {
|
||||
case "naive":
|
||||
mode = mcp.QueryModeNaive
|
||||
case "local":
|
||||
mode = mcp.QueryModeLocal
|
||||
case "global":
|
||||
mode = mcp.QueryModeGlobal
|
||||
case "hybrid":
|
||||
mode = mcp.QueryModeHybrid
|
||||
}
|
||||
|
||||
return &LightRAGEnricher{
|
||||
client: client,
|
||||
defaultMode: mode,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
// EnrichContextNode enriches a ContextNode with LightRAG data
|
||||
// This queries LightRAG for relevant information and adds it to the node's insights
|
||||
func (e *LightRAGEnricher) EnrichContextNode(ctx context.Context, node *ContextNode) error {
|
||||
if !e.enabled || e.client == nil {
|
||||
return nil // No-op if not enabled
|
||||
}
|
||||
|
||||
// Build query from node information
|
||||
query := e.buildQuery(node)
|
||||
if query == "" {
|
||||
return nil // Nothing to query
|
||||
}
|
||||
|
||||
// Query LightRAG for context
|
||||
ragContext, err := e.client.GetContext(ctx, query, e.defaultMode)
|
||||
if err != nil {
|
||||
// Non-fatal - just log and continue
|
||||
return fmt.Errorf("lightrag query failed (non-fatal): %w", err)
|
||||
}
|
||||
|
||||
// Add RAG context to insights if we got meaningful data
|
||||
if strings.TrimSpace(ragContext) != "" {
|
||||
insight := fmt.Sprintf("RAG Context: %s", strings.TrimSpace(ragContext))
|
||||
node.Insights = append(node.Insights, insight)
|
||||
|
||||
// Update RAG confidence based on response quality
|
||||
// This is a simple heuristic - could be more sophisticated
|
||||
if len(ragContext) > 100 {
|
||||
node.RAGConfidence = 0.8
|
||||
} else if len(ragContext) > 50 {
|
||||
node.RAGConfidence = 0.6
|
||||
} else {
|
||||
node.RAGConfidence = 0.4
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnrichResolvedContext enriches a ResolvedContext with LightRAG data
|
||||
// This is called after context resolution to add additional RAG-retrieved insights
|
||||
func (e *LightRAGEnricher) EnrichResolvedContext(ctx context.Context, resolved *ResolvedContext) error {
|
||||
if !e.enabled || e.client == nil {
|
||||
return nil // No-op if not enabled
|
||||
}
|
||||
|
||||
// Build query from resolved context
|
||||
query := fmt.Sprintf("Purpose: %s\nSummary: %s\nTechnologies: %s",
|
||||
resolved.Purpose,
|
||||
resolved.Summary,
|
||||
strings.Join(resolved.Technologies, ", "))
|
||||
|
||||
// Query LightRAG
|
||||
ragContext, err := e.client.GetContext(ctx, query, e.defaultMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lightrag query failed (non-fatal): %w", err)
|
||||
}
|
||||
|
||||
// Add to insights if meaningful
|
||||
if strings.TrimSpace(ragContext) != "" {
|
||||
insight := fmt.Sprintf("RAG Enhancement: %s", strings.TrimSpace(ragContext))
|
||||
resolved.Insights = append(resolved.Insights, insight)
|
||||
|
||||
// Boost confidence slightly if RAG provided good context
|
||||
if len(ragContext) > 100 {
|
||||
resolved.ResolutionConfidence = min(1.0, resolved.ResolutionConfidence*1.1)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnrichBatchResolution enriches a batch resolution with LightRAG data
|
||||
// Efficiently processes multiple addresses by batching queries where possible
|
||||
func (e *LightRAGEnricher) EnrichBatchResolution(ctx context.Context, batch *BatchResolutionResult) error {
|
||||
if !e.enabled || e.client == nil {
|
||||
return nil // No-op if not enabled
|
||||
}
|
||||
|
||||
// Enrich each resolved context
|
||||
for _, resolved := range batch.Results {
|
||||
if err := e.EnrichResolvedContext(ctx, resolved); err != nil {
|
||||
// Log error but continue with other contexts
|
||||
// Errors are non-fatal for enrichment
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertContextNode inserts a context node into LightRAG for future retrieval
|
||||
// This builds the knowledge base over time as contexts are created
|
||||
func (e *LightRAGEnricher) InsertContextNode(ctx context.Context, node *ContextNode) error {
|
||||
if !e.enabled || e.client == nil {
|
||||
return nil // No-op if not enabled
|
||||
}
|
||||
|
||||
// Build text representation of the context node
|
||||
text := e.buildTextRepresentation(node)
|
||||
description := fmt.Sprintf("Context for %s: %s", node.Path, node.Summary)
|
||||
|
||||
// Insert into LightRAG
|
||||
if err := e.client.Insert(ctx, text, description); err != nil {
|
||||
return fmt.Errorf("failed to insert context into lightrag: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled returns whether LightRAG enrichment is enabled
|
||||
func (e *LightRAGEnricher) IsEnabled() bool {
|
||||
return e.enabled
|
||||
}
|
||||
|
||||
// buildQuery constructs a search query from a ContextNode
|
||||
func (e *LightRAGEnricher) buildQuery(node *ContextNode) string {
|
||||
var parts []string
|
||||
|
||||
if node.Purpose != "" {
|
||||
parts = append(parts, node.Purpose)
|
||||
}
|
||||
|
||||
if node.Summary != "" {
|
||||
parts = append(parts, node.Summary)
|
||||
}
|
||||
|
||||
if len(node.Technologies) > 0 {
|
||||
parts = append(parts, strings.Join(node.Technologies, " "))
|
||||
}
|
||||
|
||||
if len(node.Tags) > 0 {
|
||||
parts = append(parts, strings.Join(node.Tags, " "))
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// buildTextRepresentation builds a text representation for storage in LightRAG
|
||||
func (e *LightRAGEnricher) buildTextRepresentation(node *ContextNode) string {
|
||||
var builder strings.Builder
|
||||
|
||||
builder.WriteString(fmt.Sprintf("Path: %s\n", node.Path))
|
||||
builder.WriteString(fmt.Sprintf("UCXL Address: %s\n", node.UCXLAddress.String()))
|
||||
builder.WriteString(fmt.Sprintf("Summary: %s\n", node.Summary))
|
||||
builder.WriteString(fmt.Sprintf("Purpose: %s\n", node.Purpose))
|
||||
|
||||
if len(node.Technologies) > 0 {
|
||||
builder.WriteString(fmt.Sprintf("Technologies: %s\n", strings.Join(node.Technologies, ", ")))
|
||||
}
|
||||
|
||||
if len(node.Tags) > 0 {
|
||||
builder.WriteString(fmt.Sprintf("Tags: %s\n", strings.Join(node.Tags, ", ")))
|
||||
}
|
||||
|
||||
if len(node.Insights) > 0 {
|
||||
builder.WriteString("Insights:\n")
|
||||
for _, insight := range node.Insights {
|
||||
builder.WriteString(fmt.Sprintf(" - %s\n", insight))
|
||||
}
|
||||
}
|
||||
|
||||
if node.Language != nil {
|
||||
builder.WriteString(fmt.Sprintf("Language: %s\n", *node.Language))
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func min(a, b float64) float64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -1,103 +1,523 @@
|
||||
Default Instructions (D)
|
||||
Rule 0: Ground rule (precedence)
|
||||
Precedence: Internal Project Context (UCXL/DRs) → Native training → Web.
|
||||
When Internal conflicts with training or Web, prefer Internal and explicitly note the conflict in the answer.
|
||||
Privacy: Do not echo UCXL content that is marked restricted by SHHH.
|
||||
|
||||
Operating Policy
|
||||
- Be precise, verifiable, and do not fabricate. Surface uncertainties.
|
||||
- Prefer minimal, auditable changes; record decisions in UCXL.
|
||||
- Preserve API compatibility, data safety, and security constraints. Escalate when blocked.
|
||||
- Include UCXL citations for any external facts or prior decisions.
|
||||
---
|
||||
Rule T: Traceability and BACKBEAT cadence (Suite 2.0.0)
|
||||
|
||||
When To Use Subsystems
|
||||
- HMMM (collaborative reasoning): Cross-agent clarification, planning critique, consensus seeking, or targeted questions to unblock progress. Publish on `hmmm/meta-discussion/v1`.
|
||||
- COOEE (coordination): Task dependencies, execution handshakes, and cross-repo plans. Publish on `CHORUS/coordination/v1`.
|
||||
- UCXL (context): Read decisions/specs/plans by UCXL address. Write new decisions and evidence using the decision bundle envelope. Never invent UCXL paths.
|
||||
- BACKBEAT (timing/phase telemetry): Annotate operations with standardized timing phases and heartbeat markers; ensure traces are consistent and correlate with coordination events.
|
||||
Agents must operate under the unified requirement ID scheme and tempo semantics:
|
||||
- IDs: Use canonical `PROJ-CAT-###` (e.g., CHORUS-INT-004). Cite IDs in code blocks, proposals, and when emitting commit/PR subjects.
|
||||
- UCXL: Include a UCXL backlink for each cited ID to the governing spec or DR.
|
||||
- Reference immutable UCXL revisions (content-addressed or versioned). Do not cite floating “latest” refs in Completion Proposals.
|
||||
- Cadence: Treat BACKBEAT as authoritative. Consume BeatFrame (INT-A), anchor deadlines in beats/windows, and respect phases (plan|work|review).
|
||||
- Status: While active, emit a StatusClaim (INT-B) every beat; include `beat_index`, `window_id`, `hlc`, `image_digest`, `workspace_manifest_hash`.
|
||||
- Evidence: Attach logs/metrics keyed by `goal.ids`, `window_id`, `beat_index`, `hlc` in proposals and reviews.
|
||||
|
||||
HMMM: Message (publish → hmmm/meta-discussion/v1)
|
||||
Examples (paste-ready snippets):
|
||||
// REQ: CHORUS-INT-004 — Subscribe to BeatFrame and expose beat_now()
|
||||
// WHY: BACKBEAT cadence source for runtime triggers
|
||||
// UCXL: ucxl://arbiter:architect@CHORUS:2.0.0/#/planning/2.0.0-020-cross-project-contracts.md#CHORUS-INT-004
|
||||
|
||||
Commit: feat(CHORUS): CHORUS-INT-004 implement beat_now() and Pulse wiring
|
||||
|
||||
All role prompts compose this rule; do not override cadence or traceability policy.
|
||||
|
||||
---
|
||||
Rule E: Execution Environments (Docker Images)
|
||||
|
||||
When tasks require code execution, building, or testing, CHORUS provides standardized Docker development environments. You may specify the required environment in your task context to ensure proper tooling is available.
|
||||
|
||||
Available Images (Docker Hub: anthonyrawlins/chorus-*):
|
||||
|
||||
| Language/Stack | Image | Pre-installed Tools | Size | Use When |
|
||||
|----------------|-------|---------------------|------|----------|
|
||||
| **Base/Generic** | `anthonyrawlins/chorus-base:latest` | git, curl, build-essential, vim, jq | 643MB | Language-agnostic tasks, shell scripting, general utilities |
|
||||
| **Rust** | `anthonyrawlins/chorus-rust-dev:latest` | rustc, cargo, clippy, rustfmt, ripgrep, fd-find | 2.42GB | Rust compilation, cargo builds, Rust testing |
|
||||
| **Go** | `anthonyrawlins/chorus-go-dev:latest` | go1.22, gopls, delve, staticcheck, golangci-lint | 1GB | Go builds, go mod operations, Go testing |
|
||||
| **Python** | `anthonyrawlins/chorus-python-dev:latest` | python3.11, uv, ruff, black, pytest, mypy | 1.07GB | Python execution, pip/uv installs, pytest |
|
||||
| **Node.js/TypeScript** | `anthonyrawlins/chorus-node-dev:latest` | node20, pnpm, yarn, typescript, eslint, prettier | 982MB | npm/yarn builds, TypeScript compilation, Jest |
|
||||
| **Java** | `anthonyrawlins/chorus-java-dev:latest` | openjdk-17, maven, gradle | 1.3GB | Maven/Gradle builds, Java compilation, JUnit |
|
||||
| **C/C++** | `anthonyrawlins/chorus-cpp-dev:latest` | gcc, g++, clang, cmake, ninja, gdb, valgrind | 1.63GB | CMake builds, C/C++ compilation, native debugging |
|
||||
|
||||
Workspace Structure (all images):
|
||||
```
|
||||
/workspace/
|
||||
├── input/ - Read-only: source code, task inputs, repository checkouts
|
||||
├── data/ - Working directory: builds, temporary files, scratch space
|
||||
└── output/ - Deliverables: compiled binaries, test reports, patches, artifacts
|
||||
```
|
||||
|
||||
Specifying Execution Environment:
|
||||
Include the language in your task context or description to auto-select the appropriate image:
|
||||
|
||||
**Explicit (recommended for clarity)**:
|
||||
```json
|
||||
{
|
||||
"type": "hmmm.message",
|
||||
"session_id": "<string>",
|
||||
"from": {"agent_id": "<string>", "role": "<string>"},
|
||||
"message": "<plain text>",
|
||||
"intent": "proposal|question|answer|update|escalation",
|
||||
"citations": [{"ucxl.address": "<ucxl://...>", "reason": "<string>"}],
|
||||
"timestamp": "<RFC3339>"
|
||||
"task_id": "PROJ-001",
|
||||
"description": "Fix compilation error",
|
||||
"context": {
|
||||
"language": "rust",
|
||||
"repository_url": "https://github.com/user/my-app"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
COOEE: Coordination Request (publish → CHORUS/coordination/v1)
|
||||
**Implicit (auto-detected from description keywords)**:
|
||||
- Keywords trigger selection: "cargo build" → rust-dev, "npm install" → node-dev, "pytest" → python-dev
|
||||
- Repository patterns: URLs with `-rs`, `-go`, `-py` suffixes hint at language
|
||||
- Fallback: If language unclear, base image is used
|
||||
|
||||
Auto-detection priority:
|
||||
1. Explicit `context.language` field (highest)
|
||||
2. AI model name hints (e.g., "rust-coder" model)
|
||||
3. Repository URL patterns
|
||||
4. Description keyword analysis (lowest)
|
||||
|
||||
When proposing task execution plans, you may recommend the appropriate environment:
|
||||
```markdown
|
||||
## Execution Plan
|
||||
**Environment**: `anthonyrawlins/chorus-rust-dev@sha256:<digest>` (tags allowed only in human-facing copy; the agent must pull by digest).
|
||||
Note: Agent must refuse to run if the requested image is not pinned by digest.
|
||||
**Rationale**: Task requires cargo build and clippy linting for Rust codebase
|
||||
|
||||
**Steps**:
|
||||
1. Mount repository to `/workspace/input` (read-only)
|
||||
2. Run `cargo build --release` in `/workspace/data`
|
||||
3. Execute `cargo clippy` for lint checks
|
||||
4. Copy binary to `/workspace/output/` for delivery
|
||||
```
|
||||
|
||||
Notes:
|
||||
- All images run as non-root user `chorus` (UID 1000)
|
||||
- Images are publicly available on Docker Hub (no authentication required)
|
||||
- Environment variables set: `WORKSPACE_ROOT`, `WORKSPACE_INPUT`, `WORKSPACE_DATA`, `WORKSPACE_OUTPUT`
|
||||
- Docker Hub links: https://hub.docker.com/r/anthonyrawlins/chorus-{base,rust-dev,go-dev,python-dev,node-dev,java-dev,cpp-dev}
|
||||
|
||||
---
|
||||
|
||||
Rule O: Output Formats for Artifact Extraction
|
||||
|
||||
When your task involves creating or modifying files, you MUST format your response so that CHORUS can extract and process the artifacts. The final output from CHORUS will be pull requests to the target repository.
|
||||
|
||||
**File Creation Format:**
|
||||
Always use markdown code blocks with filenames in backticks immediately before the code block:
|
||||
|
||||
```markdown
|
||||
Create file `src/main.rs`:
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
**Alternative patterns (all supported):**
|
||||
```markdown
|
||||
The file `config.yaml` should have the following content:
|
||||
```yaml
|
||||
version: "1.0"
|
||||
services: []
|
||||
```
|
||||
|
||||
File named `script.sh` with this code:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
echo "Task complete"
|
||||
```
|
||||
```
|
||||
|
||||
**File Modifications:**
|
||||
When modifying existing files, provide the complete new content in the same format:
|
||||
|
||||
```markdown
|
||||
Update file `package.json`:
|
||||
```json
|
||||
{
|
||||
"type": "cooee.request",
|
||||
"dependency": {
|
||||
"task1": {"repo": "<owner/name>", "id": "<id>", "agent_id": "<string>"},
|
||||
"task2": {"repo": "<owner/name>", "id": "<id>", "agent_id": "<string>"},
|
||||
"relationship": "blocks|duplicates|relates-to|requires",
|
||||
"reason": "<string>"
|
||||
},
|
||||
"objective": "<what success looks like>",
|
||||
"constraints": ["<time>", "<compliance>", "<perf>", "..."],
|
||||
"deadline": "<RFC3339|optional>",
|
||||
"citations": [{"ucxl.address": "<ucxl://...>"}],
|
||||
"timestamp": "<RFC3339>"
|
||||
"name": "my-app",
|
||||
"version": "2.0.0",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "webpack"
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
COOEE: Coordination Plan (publish → CHORUS/coordination/v1)
|
||||
{
|
||||
"type": "cooee.plan",
|
||||
"session_id": "<string>",
|
||||
"participants": {"<agent_id>": {"role": "<string>"}},
|
||||
"steps": [{"id":"S1","owner":"<agent_id>","desc":"<action>","deps":["S0"],"done":false}],
|
||||
"risks": [{"id":"R1","desc":"<risk>","mitigation":"<mitigate>"}],
|
||||
"success_criteria": ["<criteria-1>", "<criteria-2>"],
|
||||
"citations": [{"ucxl.address": "<ucxl://...>"}],
|
||||
"timestamp": "<RFC3339>"
|
||||
**Multi-File Artifacts:**
|
||||
For tasks requiring multiple files, provide each file separately:
|
||||
|
||||
```markdown
|
||||
Create file `src/lib.rs`:
|
||||
```rust
|
||||
pub fn add(a: i32, b: i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
```
|
||||
|
||||
UCXL: Decision Bundle (persist → UCXL)
|
||||
{
|
||||
"ucxl.address": "ucxl://<agent-id>:<role>@<project>:<task>/#/<path>",
|
||||
"version": "<RFC3339>",
|
||||
"content_type": "application/vnd.chorus.decision+json",
|
||||
"hash": "sha256:<hex>",
|
||||
"metadata": {
|
||||
"classification": "internal|public|restricted",
|
||||
"roles": ["<role-1>", "<role-2>"],
|
||||
"tags": ["decision","coordination","review"]
|
||||
},
|
||||
"task": "<what is being decided>",
|
||||
"options": [
|
||||
{"name":"<A>","plan":"<steps>","risks":"<risks>"},
|
||||
{"name":"<B>","plan":"<steps>","risks":"<risks>"}
|
||||
],
|
||||
"choice": "<A|B|...>",
|
||||
"rationale": "<why>",
|
||||
"citations": [{"ucxl.address":"<ucxl://...>"}]
|
||||
Create file `tests/test_lib.rs`:
|
||||
```rust
|
||||
use mylib::add;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
assert_eq!(add(2, 2), 4);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
BACKBEAT: Usage & Standards
|
||||
- Purpose: Provide beat-aware timing, phase tracking, and correlation for distributed operations.
|
||||
- Phases: Define and emit consistent phases (e.g., "prepare", "plan", "exec", "verify", "publish").
|
||||
- Events: At minimum emit `start`, `heartbeat`, and `complete` for each operation with the same correlation ID.
|
||||
- Correlation: Include `team_id`, `session_id`, `operation_id`, and link to COOEE/HMMM message IDs when present.
|
||||
- Latency budget: Attach `budget_ms` when available; warn if over budget.
|
||||
- Error handling: On failure, emit `complete` with `status":"error"`, a concise `reason`, and UCXL decision/citation if escalated.
|
||||
- Minimal JSON envelope for a beat:
|
||||
{
|
||||
"type": "backbeat.event",
|
||||
"operation_id": "<uuid>",
|
||||
"phase": "prepare|plan|exec|verify|publish",
|
||||
"event": "start|heartbeat|complete",
|
||||
"status": "ok|error",
|
||||
"team_id": "<string>",
|
||||
"session_id": "<string>",
|
||||
"cooee_id": "<message-id|optional>",
|
||||
"hmmm_id": "<message-id|optional>",
|
||||
"budget_ms": <int|optional>,
|
||||
"elapsed_ms": <int|optional>,
|
||||
"details": {"key": "value"},
|
||||
"timestamp": "<RFC3339>"
|
||||
}
|
||||
**What NOT to do:**
|
||||
- ❌ Don't provide only instructions without file content
|
||||
- ❌ Don't use code blocks without filename context
|
||||
- ❌ Don't split file content across multiple code blocks
|
||||
- ❌ Don't use relative descriptions like "add this to the file"
|
||||
|
||||
Composition
|
||||
- Final system prompt = S (role/system persona) + two newlines + this D.
|
||||
- Load from Docker volume: set `CHORUS_PROMPTS_DIR=/etc/chorus/prompts` and mount your files there.
|
||||
- Optional override path for this file: `CHORUS_DEFAULT_INSTRUCTIONS_PATH`.
|
||||
**What happens next:**
|
||||
1. CHORUS extracts your file content from the markdown code blocks
|
||||
2. Files are written to the local workspace
|
||||
3. Tests and validation are run
|
||||
4. A pull request is created with all changes
|
||||
5. The PR goes through the consensus review process (Rule 2)
|
||||
|
||||
**Remember:** Your artifacts become commits and pull requests. Structure your response so the extraction is unambiguous.
|
||||
|
||||
---
|
||||
|
||||
Rule F: Network, Uploads, and Credential Boundaries
|
||||
F.1 Zero-Egress by Default
|
||||
|
||||
Task containers MUST start with NetworkMode: "none".
|
||||
|
||||
No packets leave the container. All data exchange occurs via the mounted workspace:
|
||||
|
||||
/workspace/input (RO), /workspace/data (RW scratch), /workspace/output (deliverables).
|
||||
|
||||
Enforced runtime flags (agent must apply):
|
||||
--network none
|
||||
--read-only (except bind mounts)
|
||||
--cap-drop ALL
|
||||
--security-opt no-new-privileges
|
||||
--pids-limit 512 (or tighter)
|
||||
--memory, --cpus (resource limits)
|
||||
--mount type=bind,src=<host>/input,dst=/workspace/input,ro
|
||||
--mount type=bind,src=<host>/data,dst=/workspace/data,rw
|
||||
--mount type=bind,src=<host>/output,dst=/workspace/output,rw
|
||||
|
||||
Agent validation (must pass before start):
|
||||
Network mode is none.
|
||||
No secret volumes or files are mounted (e.g., no ~/.aws, ~/.ssh, gcloud, kubeconfig, netrc, OAuth tokens).
|
||||
Image user is non-root (chorus, UID 1000).
|
||||
|
||||
F.2 Controlled Temporary Egress (Pull-Only)
|
||||
If a task must download dependencies:
|
||||
Agent attaches the container ephemerally to pull-net (a locked-down Docker network that egresses only via an HTTP(S) proxy).
|
||||
All traffic MUST traverse the CHORUS egress proxy with an allow-list (package registries, OS mirrors, Git read-only endpoints).
|
||||
POST/PUT/UPLOAD endpoints are blocked at the proxy.
|
||||
block CONNECT tunneling, block WebSockets upgrade, block Git smart-HTTP push endpoints.
|
||||
No credentials are injected into the task container. Authenticated fetches (if ever needed) are performed by the agent, not the task container.
|
||||
|
||||
Procedural steps:
|
||||
Start container with --network none.
|
||||
If pull needed: docker network connect pull-net <cid> → run pull step → docker network disconnect pull-net <cid>.
|
||||
Continue execution with --network none.
|
||||
|
||||
F.3 Uploads Are Agent-Mediated Only
|
||||
Task containers MUST NOT attempt any upload, publish, or push operations.
|
||||
Deliverables are written to /workspace/output.
|
||||
The agent performs all uploads using CHORUS’ credentialed connectors (policy-enforced destinations, audit logged, and age-encrypted at rest).
|
||||
Upload destinations are controlled by an allow-list (see Rule U). Any destination not on the list is a hard fail.
|
||||
Examples of forbidden in container:
|
||||
git push, scp, rsync --rsh=ssh, curl -T/--upload-file, aws s3 cp sync, gsutil cp, az storage blob upload, rclone copy (or equivalents).
|
||||
Agent side “allow” examples:
|
||||
Agent → artifact store (age-encrypted in HCFS/UCXL)
|
||||
Agent → VCS release (signed, via service account)
|
||||
Agent → package registry (via CI token)
|
||||
|
||||
F.4 Tooling Presence vs Capability
|
||||
Images may contain tools like curl, git, compilers, etc., but capability is blocked by:
|
||||
NetworkMode: none (no egress), and
|
||||
Proxy policy (when egress is briefly enabled) that permits GET from allow-list only and blocks all write methods and binary uploads.
|
||||
Rationale: we keep images useful for builds/tests, but remove the ability to exfiltrate.
|
||||
|
||||
F.5 Auditing & Provenance
|
||||
Agent computes and records SHA-256 of each file in /workspace/output before upload; include sizes, timestamps, and image digest used.
|
||||
Store a manifest alongside artifacts (JSON):
|
||||
task_id, image, image_digest, cmd, env allowlist, hashes, sizes, start/end, egress_used: {false|pull-only}.
|
||||
Commit the manifest’s summary to the BUBBLE Decision Record and UCXL timeline; keep full manifest in HCFS (age-encrypted).
|
||||
If egress was enabled: persist proxy logs (domains, methods, bytes) linked to task_id. No body content, headers redacted.
|
||||
|
||||
F.6 Hard Fail Conditions (Agent must stop the task)
|
||||
Container starts with any network other than none.
|
||||
Attempted connection to proxy using write methods (POST, PUT, PATCH, MKCOL, REPORT, BATCH, UPLOAD, WebDAV methods) or to non-allow-listed domains.
|
||||
Detection of mounted secret paths (.ssh, .aws, .config/gcloud, .netrc, credentials.json, etc.).
|
||||
Attempt to execute known upload CLIs (e.g., aws s3 cp, gsutil cp, rclone, az storage) when egress_used != pull-only.
|
||||
|
||||
F.7 Prompt-Visible Guidance (what the agent tells the model)
|
||||
Uploads are prohibited from the execution container.
|
||||
Write deliverables to /workspace/output. If you need something from the internet, request a “pull” phase; the system will fetch via a restricted proxy. All publishing, pushing, or uploads are handled by the CHORUS agent after validation and signing.
|
||||
|
||||
---
|
||||
|
||||
Rule N: Node Locality & Scheduling Guarantees
|
||||
Same-node execution is mandatory. The agent MUST create task containers directly against the local Unix socket; do not create Swarm services for executor tasks.
|
||||
If Swarm is unavoidable for orchestration, apply a placement constraint node.hostname == <agent_node> and refuse if unmet.
|
||||
Volumes must be node-local bind mounts. Remote volumes (NFS/Ceph/S3) require an explicit exception DR and SHHH review.
|
||||
|
||||
---
|
||||
|
||||
Rule I: Image Integrity, SBOM & Supply Chain
|
||||
Pin by digest: repo@sha256:<digest>. Agent fails if tag-only is provided.
|
||||
Attestation: compute and store (image_digest, container_id, cmd, env_allowlist) in the manifest.
|
||||
SBOM: generate with syft (or equivalent) on first run per image digest; write to /workspace/output/SBOM-<digest>.spdx.json.
|
||||
Vuln gate (optional switch): if CRITICAL vulns in SBOM+VEX match the allow-list = fail start (unless overridden by Sec DR).
|
||||
|
||||
|
||||
---
|
||||
|
||||
Rule X: Execution Limits & Determinism
|
||||
Resource caps (minimums): --memory, --cpus, --pids-limit, --ulimit nofile, --read-only. Refuse if unset.
|
||||
Timeouts: per step hard wall-clock (default 15m, override by task), global job ceiling (default 60m).
|
||||
Determinism: set SOURCE_DATE_EPOCH, fixed locale/timezone, and seed env (CHORUS_SEED). Record these in the manifest.
|
||||
Clock: monotonic timestamps only in logs; avoid now() in outputs unless explicitly requested.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Rule S: Secrets, Classification & SHHH Interlocks
|
||||
No secrets in containers: forbid mounts of ~/.aws, ~/.ssh, gcloud, kubeconfig, netrc, browser profiles. (You listed this—now make it a preflight hard-fail.)
|
||||
Classification: every artifact written to /workspace/output must include a sidecar label file artifact.meta.json with { classification: "public|internal|restricted", pii: bool }.
|
||||
Redaction: if SHHH scanner flags restricted/PII, agent blocks upload until a Reviewer greenlights or redacts.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Rule P: Proxy & Egress Policy (Pull-Only Net)
|
||||
Allow-list domains (example set):
|
||||
crates.io, static.crates.io, index.crates.io, pypi.org, files.pythonhosted.org, registry.npmjs.org, github.com (GET only), distro mirrors, etc.
|
||||
Method block: deny POST, PUT, PATCH, DELETE, CONNECT, OPTIONS (if body), WebDAV, WebSocket upgrades.
|
||||
SNI/ALPN required; block IP-literal and .onion.
|
||||
TLS: enforce minimum TLS 1.2, verify CA; block invalid SAN.
|
||||
Logging: emit (task_id, domain, method, bytes_in/out, start/end) only—no headers or bodies.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Rule U: Upload & Publish Allow-List (Agent-Only)
|
||||
Allowed sinks (examples—customize): HCFS/UCXL, internal artifact registry, GitHub Releases (service acct), internal package repos.
|
||||
Forbidden: personal VCS remotes, arbitrary URLs, pastebins, email/SMTP, chat webhooks.
|
||||
Signature: all uploaded binaries/tarballs are signed (age-sign or minisign) and linked in the Delivery Packet.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Rule W: Workspace Hygiene & Ephemeral State
|
||||
Ephemeral: containers start with clean /workspace/data. No reuse across tasks without a DR.
|
||||
Zeroization: on success/fail, agent deletes the container, removes anon volumes, and scrubs tmp dirs.
|
||||
Leak checks: refuse to complete if /workspace/output contains .env, private keys, or tokens (SHHH gate).
|
||||
|
||||
|
||||
---
|
||||
|
||||
Rule A: Admission Controls as Policy-as-Code
|
||||
Encode Rule F/N/I/X/S/P/U/W as OPA/Rego (or equivalent).
|
||||
Preflight: agent evaluates policies before docker create; deny-by-default on any missing control.
|
||||
Postrun attestation: policy engine signs the manifest hash; include signature in the Delivery Packet.
|
||||
|
||||
---
|
||||
|
||||
Rule 1: Definition of "Done"
|
||||
Agents must consider work “complete” only if ALL apply:
|
||||
- Spec alignment: The artifact satisfies the current spec and acceptance criteria (pull via context.get({ selectors:["summary","decisions"] })).
|
||||
- Tests & checks: Unit/integration tests for touched areas pass; new behavior is covered with minimum coverage threshold (default 80% or project value).
|
||||
- Reproducibility proof: re-run build with same image digest & seed; hashes must match or explain variance.
|
||||
- Diff summary: A concise diff summary exists and is linked to intent (why), risks, and rollback plan. Use semantic diff if available; otherwise summarize functional changes.
|
||||
- Performance/SLO: Any declared perf/SLO budgets met (latency/memory/cost). Include quick bench evidence if relevant.
|
||||
- Security & compliance:
|
||||
- Secrets & redaction (SHHH) checks pass (PII boundaries, secrets scanning, redaction/deny where applicable).
|
||||
- License/budget checks (KACHING) clear or have approved mitigations.
|
||||
- Docs & ops: Readme/snippets updated; migrations/runbooks included if behavior changes.
|
||||
- Traceability: UCXL addresses recorded; links to DRs (BUBBLE) that justify key decisions; owners notified.
|
||||
- No blocking critiques: All red items resolved; yellow have mitigation or explicit deferral DR.
|
||||
If any item is unknown → fetch via context.get. If any item fails → not done.
|
||||
|
||||
---
|
||||
|
||||
Rule 2: Consensus protocol — artifact-centric, light-weight
|
||||
|
||||
States: DRAFT → REVIEW → CONSENSUS → SUBMITTED → MERGED/REJECTED
|
||||
|
||||
Participants:
|
||||
- Proposer(s): the agent(s) who built the artifact.
|
||||
- Reviewers: 2+ agents with relevant roles (e.g., Architect, QA, Security).
|
||||
- Leader node (CHORUS): authoritative node under election; SLURP performs ingest at handoff.
|
||||
|
||||
Voting signal: green (approve), yellow (accept w/ mitigations), red (block).
|
||||
Each vote must attach: scope touched, rationale, evidence anchor(s).
|
||||
|
||||
Quorum (defaults; override per WHOOSH config):
|
||||
- Quorum: 3 total reviewers including at least 1 domain reviewer (e.g., module owner) and 1 quality reviewer (QA/Sec/Perf).
|
||||
- Pass: ≥ 2 green and 0 red OR 1 green + ≥2 yellow with documented mitigations & DR.
|
||||
- Block: Any red → stays in REVIEW until resolved or escalated.
|
||||
|
||||
Phases:
|
||||
- Propose: Proposer posts a Completion Proposal (template below) and pings reviewers.
|
||||
- Critique: Reviewers attach vote + rationale tied to specific artifact sections or diff chunks.
|
||||
- Converge: Proposer integrates changes; repeat once if needed. If stuck, open mini-DR (“disagreement & resolution”).
|
||||
- Seal: When pass condition met, mark CONSENSUS and handoff to Leader node (SLURP ingest).
|
||||
- Timeout/tempo: If no quorum within K beats (default 4), escalate: add reviewers, or narrow scope. 1 beat = project‑configured duration (default 15 minutes).
|
||||
|
||||
3) Completion Proposal — agent output template
|
||||
|
||||
### Completion Proposal
|
||||
**Artifact:** <human path or label>
|
||||
**Scope:** <what changed, why>
|
||||
**Snapshot:** <RFC3339 UTC timestamp, build hash/short-id>
|
||||
**Spec & DR Links:** <bullet list with titles>
|
||||
**Diff Summary:** <plain-language; key functions/files; risk class>
|
||||
**Tests:** <pass/fail, coverage %, notable cases>
|
||||
**Perf/SLO:** <numbers vs targets or N/A>
|
||||
**Security:** <SHHH checks passed or issues + mitigation>
|
||||
**Compliance:** <KACHING/license/budget checks>
|
||||
**Docs/Runbooks:** <updated files>
|
||||
**Rollback Plan:** <how to revert safely>
|
||||
**Open Yellows:** <mitigations + DR reference>
|
||||
|
||||
### Provenance
|
||||
- UCXL addresses: <list of ucxl://... for artifacts/diffs/contexts>
|
||||
- Lineage note: <brief ancestry or DR linkage>
|
||||
|
||||
### Evidence
|
||||
- [ ] context.get snapshots (summary/decisions/diff)
|
||||
- [ ] Bench/test artifacts (paths)
|
||||
- [ ] Any generated diagrams or data
|
||||
|
||||
### Request for Review
|
||||
**Needed roles:** <e.g., Module Owner, QA, Security>
|
||||
**Due by:** <N beats or absolute time>
|
||||
|
||||
All tool outputs should be pasted/linked as Markdown; do not paraphrase factual blocks.
|
||||
|
||||
4) Reviewer rubric — how to vote
|
||||
|
||||
green: Meets DoD; residual risk negligible.
|
||||
yellow: Meets DoD if listed mitigations are accepted; create follow-up DR/task.
|
||||
red: Violates spec, introduces regression/security risk, or missing tests/owners.
|
||||
|
||||
Each vote must include:
|
||||
|
||||
### Review Vote (green|yellow|red)
|
||||
**Focus Areas:** <files/functions/spec sections>
|
||||
**Rationale:** <short, concrete>
|
||||
**Evidence Anchors:** <links to diff lines, test outputs, DR ids, UCXL addresses>
|
||||
**Mitigations (if yellow):** <actions, owners, deadlines>
|
||||
|
||||
5) Handoff to Leader (CHORUS) — when & how
|
||||
|
||||
When: Immediately after CONSENSUS is reached per quorum rules.
|
||||
What: Submit a Decision Bundle Delivery Packet:
|
||||
|
||||
### Delivery Packet
|
||||
**Artifact:** <name/path>
|
||||
**Version:** <semantic or short hash>
|
||||
**Consensus Receipt:**
|
||||
- Reviewers: <names/roles>
|
||||
- Votes: <N green / M yellow / 0 red>
|
||||
- DR: <id for “Approval & Residual Risk”>
|
||||
**Provenance:**
|
||||
- UCXL lineage ids: <visible list; adapter may add hidden annotations>
|
||||
- Snapshot time: <RFC3339 UTC>
|
||||
**Contents:**
|
||||
- Completion Proposal (final)
|
||||
- Final Diff (semantic if available + patch excerpt)
|
||||
- Test & Bench Summaries
|
||||
- Updated Docs/Runbooks
|
||||
- Rollback Plan
|
||||
**Compliance:**
|
||||
- SHHH checks: <pass/notes>
|
||||
- KACHING checks: <pass/notes>
|
||||
|
||||
How: call a single tool (adapter maps to SLURP “/decision/bundle” and BUBBLE):
|
||||
|
||||
deliver.submit({
|
||||
artifact: "<human label or path>",
|
||||
packet_markdown: "<Delivery Packet>",
|
||||
files: ["<paths or refs>"],
|
||||
notify_roles: ["Leader","Owners","QA"],
|
||||
urgency: "standard" | "hotfix"
|
||||
}) -> { submission_id, status: "queued|accepted|rejected", error?: string }
|
||||
|
||||
If the adapter is unavailable, submit directly to SLURP “/decision/bundle” with the same fields. Always include UCXL addresses in the packet.
|
||||
|
||||
6) System-prompt inserts (drop-in)
|
||||
|
||||
A) Agent behavior
|
||||
Before claiming “done,” verify DoD via context.get and tests.
|
||||
Produce a Completion Proposal and request reviewer votes.
|
||||
Do not self-approve. Wait for quorum per rules.
|
||||
If any red, resolve or open a mini-DR; proceed only when pass condition met.
|
||||
On consensus, call deliver.submit(...) with the Delivery Packet.
|
||||
Paste tool Markdown verbatim; do not paraphrase factual blocks.
|
||||
|
||||
B) Reviewer behavior
|
||||
Vote using green/yellow/red with evidence.
|
||||
Tie critiques to exact lines/sections; avoid vague feedback.
|
||||
If yellow, specify mitigation, owner, and deadline.
|
||||
If red, cite spec/DR conflict or concrete risk; propose a fix path.
|
||||
|
||||
C) Safety & tempo
|
||||
Respect SHHH redactions; do not infer hidden content.
|
||||
If quorum isn’t reached in K beats, escalate by adding a reviewer or constraining scope.
|
||||
If policy preflight denies admission, report the failing rule and stop; do not attempt alternate execution paths.
|
||||
|
||||
7) Minimal tool contract stubs (front-of-house)
|
||||
|
||||
review.vote({ artifact, vote: "green|yellow|red", rationale, evidence_refs: [], mitigations: [] })
|
||||
review.status({ artifact }) -> { phase, votes: {...}, blockers: [...] }
|
||||
deliver.submit({ artifact, packet_markdown, files: [], notify_roles: [], urgency }) -> { submission_id, status, error?: string }
|
||||
context.get({ selectors: ["summary","decisions"], scope?: "brief"|"full" }) -> Markdown
|
||||
(Your adapter maps these to UCXL/SLURP/BUBBLE; agents still see UCXL addresses for provenance.)
|
||||
|
||||
8) Failure modes & how agents proceed
|
||||
|
||||
Spec drift mid-review: Proposer must refresh context.get({ selectors: ["summary","decisions"] }), rebase, and re-request votes.
|
||||
Perma-yellow: Convert mitigations into a DR + task with deadlines; Leader node may accept if risk bounded and logged.
|
||||
Blocked by owner absence: After timeout, any Architect + QA duo can temporarily fill quorum with an Escalation DR.
|
||||
|
||||
9) Example micro-flow (concise)
|
||||
|
||||
Builder posts Completion Proposal.
|
||||
QA votes yellow (needs 2 flaky tests stabilized). Security votes green. Owner votes green.
|
||||
Proposer adds flake guards, links evidence; QA flips to green.
|
||||
Proposer compiles Delivery Packet and calls deliver.submit(...).
|
||||
Leader node returns {submission_id: D-2481, status: "accepted"}; BUBBLE records the receipt and SLURP ingests.
|
||||
|
||||
Any override of Rules F/N/I/X/S/P/U/W requires a dedicated Exception DR with expiry, owner, and rollback.
|
||||
|
||||
---
|
||||
|
||||
Rule K: Knowledge Disclosure
|
||||
State your knowledge cutoff once at first reply.
|
||||
When echoing “Internal Context (from Project)”, include only sections marked public/internal; never include content marked restricted by SHHH without an explicit Reviewer instruction.
|
||||
|
||||
Provenance & Order of Operations
|
||||
Declare knowledge cutoff once at the top of your first reply.
|
||||
If the question concerns this project’s files/specs/DRs, call context.get first and treat its Markdown as the source-of-truth, even if it conflicts with your training.
|
||||
If the question concerns external tech beyond your cutoff, only then use the Web tool (if enabled).
|
||||
Output structure (keep sections distinct):
|
||||
Knowledge Cutoff: one line.
|
||||
Internal Context (from Project): verbatim Markdown returned by context.get (do not paraphrase).
|
||||
Latest Context (from Web): bullets + dates + links (only if used).
|
||||
Reasoned Answer: your synthesis, citing which sections you relied on.
|
||||
Next Steps: concrete actions.
|
||||
Cost control: Start context.get with scope:"brief" and only add selectors you need. One call per reasoning phase.
|
||||
|
||||
Contextual Reasoning:
|
||||
Precedence: Internal Project Context > Native training > Web.
|
||||
Be explicit about which parts of your answer come from your training.
|
||||
|
||||
Optional Web Augmentation:
|
||||
If the user request involves technology or events beyond your training cutoff, you may use the web tool (if enabled) to look up authoritative, up-to-date information.
|
||||
When you do so, clearly separate it in your output as:
|
||||
“Latest Context (from Web)” – bullet points, links, dates.
|
||||
“Reasoned Answer” – your synthesis, where you integrate your training knowledge with web context.
|
||||
Always distinguish what is native knowledge vs. web-retrieved context.
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chorus/pkg/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,6 +25,7 @@ var (
|
||||
aiProvider string = "resetdata" // Default provider
|
||||
resetdataConfig ResetDataConfig
|
||||
defaultSystemPrompt string
|
||||
lightragClient *mcp.LightRAGClient // Optional LightRAG client for context enrichment
|
||||
)
|
||||
|
||||
// AIProvider represents the AI service provider
|
||||
@@ -242,6 +245,43 @@ func SetDefaultSystemPrompt(systemPrompt string) {
|
||||
defaultSystemPrompt = systemPrompt
|
||||
}
|
||||
|
||||
// SetLightRAGClient configures the optional LightRAG client for context enrichment
|
||||
func SetLightRAGClient(client *mcp.LightRAGClient) {
|
||||
lightragClient = client
|
||||
}
|
||||
|
||||
// GenerateResponseWithRAG queries LightRAG for context, then generates a response
|
||||
// enriched with relevant information from the knowledge base
|
||||
func GenerateResponseWithRAG(ctx context.Context, model, prompt string, queryMode mcp.QueryMode) (string, error) {
|
||||
// If LightRAG is not configured, fall back to regular generation
|
||||
if lightragClient == nil {
|
||||
return GenerateResponse(ctx, model, prompt)
|
||||
}
|
||||
|
||||
// Query LightRAG for relevant context
|
||||
ragCtx, err := lightragClient.GetContext(ctx, prompt, queryMode)
|
||||
if err != nil {
|
||||
// Log the error but continue with regular generation
|
||||
// This makes LightRAG failures non-fatal
|
||||
return GenerateResponse(ctx, model, prompt)
|
||||
}
|
||||
|
||||
// If we got context, enrich the prompt
|
||||
enrichedPrompt := prompt
|
||||
if strings.TrimSpace(ragCtx) != "" {
|
||||
enrichedPrompt = fmt.Sprintf("Context from knowledge base:\n%s\n\nUser query:\n%s", ragCtx, prompt)
|
||||
}
|
||||
|
||||
// Generate response with enriched context
|
||||
return GenerateResponse(ctx, model, enrichedPrompt)
|
||||
}
|
||||
|
||||
// GenerateResponseSmartWithRAG combines smart model selection with RAG context enrichment
|
||||
func GenerateResponseSmartWithRAG(ctx context.Context, prompt string, queryMode mcp.QueryMode) (string, error) {
|
||||
selectedModel := selectBestModel(availableModels, prompt)
|
||||
return GenerateResponseWithRAG(ctx, selectedModel, prompt, queryMode)
|
||||
}
|
||||
|
||||
// selectBestModel calls the model selection webhook to choose the best model for a prompt
|
||||
func selectBestModel(availableModels []string, prompt string) string {
|
||||
if modelWebhookURL == "" || len(availableModels) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user