├── .gitignore ├── Makefile ├── cmd └── api │ ├── Dockerfile │ ├── config │ └── main.yaml │ └── main.go ├── go.mod ├── go.sum ├── internal ├── api │ └── http │ │ ├── analysis_handler.go │ │ └── handlers.go └── core │ ├── domain │ ├── agent │ │ └── stat_analyzer │ │ │ ├── agent.go │ │ │ └── prompt │ │ │ ├── analyze_stats.tmpl │ │ │ ├── improvement_plan.tmpl │ │ │ └── loader.go │ └── user │ │ ├── model.go │ │ ├── service.go │ │ └── storage.go │ └── ext │ └── storage │ └── user.go ├── pkg └── llm │ ├── claude │ ├── claude_client.go │ └── model.go │ ├── llm.go │ ├── model.go │ └── openai │ ├── model.go │ └── openai_client.go └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .gitlab 4 | local.yaml 5 | k8s/* -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for compiai-engine 2 | 3 | # Binary name 4 | BINARY_NAME := compiai-engine 5 | # Configuration file 6 | CONFIG_FILE := config.yaml 7 | 8 | # Go settings 9 | GO := go 10 | GOCMD := $(GO) 11 | GOBUILD := $(GO) build 12 | GOCLEAN := $(GO) clean 13 | GOTEST := $(GO) test 14 | GOFMT := $(GO) fmt 15 | GOVET := $(GO) vet 16 | GOMOD := $(GO) mod 17 | 18 | # Directories 19 | PKG := ./... 20 | BIN_DIR := bin 21 | 22 | # Docker settings 23 | DOCKER_IMAGE := compiai/engine:latest 24 | DOCKERFILE := Dockerfile 25 | 26 | .PHONY: all build run test fmt vet lint tidy clean docker-build help 27 | 28 | all: build 29 | 30 | ## build: compile the binary 31 | build: 32 | @echo "==> Building $(BINARY_NAME)..." 33 | @mkdir -p $(BIN_DIR) 34 | $(GOBUILD) -o $(BIN_DIR)/$(BINARY_NAME) -ldflags="-s -w" main.go 35 | 36 | ## run: run the application using the config file 37 | run: build 38 | @echo "==> Running $(BINARY_NAME)..." 39 | @$(BIN_DIR)/$(BINARY_NAME) -config $(CONFIG_FILE) 40 | 41 | ## test: run unit tests 42 | test: 43 | @echo "==> Running tests..." 44 | $(GOTEST) -v $(PKG) 45 | 46 | ## fmt: format Go code 47 | fmt: 48 | @echo "==> Formatting code..." 49 | $(GOFMT) $(PKG) 50 | 51 | ## vet: report potential issues 52 | vet: 53 | @echo "==> Vetting code..." 54 | $(GOVET) $(PKG) 55 | 56 | ## lint: run linter (requires golangci-lint) 57 | lint: 58 | @echo "==> Linting code..." 59 | golangci-lint run 60 | 61 | ## tidy: ensure go.mod matches imports 62 | tidy: 63 | @echo "==> Tidying modules..." 64 | $(GOMOD) tidy 65 | 66 | ## clean: remove build artifacts 67 | clean: 68 | @echo "==> Cleaning up..." 69 | @rm -rf $(BIN_DIR) 70 | $(GOCLEAN) 71 | 72 | ## docker-build: build Docker image 73 | docker-build: 74 | @echo "==> Building Docker image $(DOCKER_IMAGE)..." 75 | docker build -t $(DOCKER_IMAGE) -f $(DOCKERFILE) . 76 | 77 | ## help: display this help 78 | help: 79 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%]()*_]() 80 | -------------------------------------------------------------------------------- /cmd/api/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # --- Builder Stage --- 4 | FROM golang:1.20-alpine AS builder 5 | 6 | # Set working directory 7 | WORKDIR /app 8 | 9 | # Cache dependencies 10 | COPY go.mod go.sum ./ 11 | RUN go mod download 12 | 13 | # Copy source code 14 | COPY . . 15 | 16 | # Build the binary 17 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 18 | go build -o compiai-engine main.go 19 | 20 | 21 | # --- Runtime Stage --- 22 | FROM alpine:latest 23 | 24 | # Install certs for HTTPS 25 | RUN apk add --no-cache ca-certificates 26 | 27 | # Create app directory 28 | WORKDIR /root/ 29 | 30 | # Copy the compiled binary and config file 31 | COPY --from=builder /app/compiai-engine ./compiai-engine 32 | COPY config.yaml ./config.yaml 33 | 34 | # Expose the application port 35 | EXPOSE 8080 36 | 37 | # Default entrypoint 38 | ENTRYPOINT ["./compiai-engine", "-config", "config.yaml"] 39 | -------------------------------------------------------------------------------- /cmd/api/config/main.yaml: -------------------------------------------------------------------------------- 1 | # this is just a placeholder for configs 2 | # real ones are in k8s 3 | 4 | # every config needs to be fully initialized 5 | # application is not gonna start without it 6 | 7 | application: 8 | version: 0.1.0-snapshot 9 | 10 | database: 11 | postgres: 12 | addr: ??? 13 | auth: 14 | username: ??? 15 | password: ??? 16 | tlsEnabled: false 17 | 18 | # TODO: add timeouts for LLM API calls (15 seconds MAX should be enough) 19 | clients: 20 | claude: 21 | apiKey: ??? 22 | endpoint: ??? 23 | model: ??? 24 | temperature: ??? 25 | maxTokensToSample: ??? 26 | 27 | openai: 28 | apiKey: ??? 29 | endpoint: ??? 30 | model: ??? 31 | temperature: ??? 32 | maxTokensToSample: ??? 33 | 34 | server: 35 | public: 36 | addr: localhost:8080 37 | timeout: 5s 38 | 39 | auth: 40 | privateKey: ??? 41 | publicKey: ??? 42 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "flag" 7 | "fmt" 8 | http2 "github.com/compiai/engine/internal/api/http" 9 | "github.com/compiai/engine/internal/core/domain/agent/stat_analyzer" 10 | "github.com/compiai/engine/pkg/llm" 11 | "github.com/compiai/engine/pkg/llm/claude" 12 | "github.com/compiai/engine/pkg/llm/openai" 13 | "io/ioutil" 14 | "log" 15 | "net/http" 16 | "os" 17 | "time" 18 | 19 | "github.com/go-chi/chi/v5" 20 | _ "github.com/lib/pq" 21 | "gopkg.in/yaml.v2" 22 | 23 | promptloader "github.com/compiai/engine/internal/core/domain/agent/stat_analyzer/prompt" 24 | domainuser "github.com/compiai/engine/internal/core/domain/user" 25 | "github.com/compiai/engine/internal/core/ext/storage" 26 | "log/slog" 27 | ) 28 | 29 | // AppConfig holds application configuration loaded from YAML. 30 | type AppConfig struct { 31 | Application struct { 32 | Version string `yaml:"version"` 33 | 34 | Database struct { 35 | Postgres struct { 36 | Addr string `yaml:"addr"` 37 | Auth struct { 38 | Username string `yaml:"username"` 39 | Password string `yaml:"password"` 40 | TLSEnabled bool `yaml:"tlsEnabled"` 41 | } `yaml:"auth"` 42 | } `yaml:"postgres"` 43 | } `yaml:"database"` 44 | 45 | Clients struct { 46 | Claude claude.Config `yaml:"claude"` 47 | OpenAI openai.Config `yaml:"openai"` 48 | } `yaml:"clients"` 49 | 50 | Server struct { 51 | Public struct { 52 | Addr string `yaml:"addr"` 53 | Timeout time.Duration `yaml:"timeout"` 54 | } `yaml:"public"` 55 | } `yaml:"server"` 56 | 57 | Auth struct { 58 | PrivateKey string `yaml:"privateKey"` 59 | PublicKey string `yaml:"publicKey"` 60 | } `yaml:"auth"` 61 | } `yaml:"application"` 62 | } 63 | 64 | // loadConfig reads and parses the YAML config file at the given path. 65 | func loadConfig(path string) (*AppConfig, error) { 66 | data, err := ioutil.ReadFile(path) 67 | if err != nil { 68 | return nil, fmt.Errorf("read config: %w", err) 69 | } 70 | var cfg AppConfig 71 | if err := yaml.UnmarshalStrict(data, &cfg); err != nil { 72 | return nil, fmt.Errorf("unmarshal config: %w", err) 73 | } 74 | return &cfg, nil 75 | } 76 | 77 | func main() { 78 | // Parse config file path 79 | configPath := flag.String("config", "config.yaml", "path to YAML config file") 80 | flag.Parse() 81 | 82 | // Load configuration 83 | cfg, err := loadConfig(*configPath) 84 | if err != nil { 85 | log.Fatalf("configuration error: %v", err) 86 | } 87 | 88 | // Initialize logger 89 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) 90 | logger.Info("starting application", "version", cfg.Application.Version) 91 | 92 | // Build Postgres DSN 93 | sslMode := "disable" 94 | if cfg.Application.Database.Postgres.Auth.TLSEnabled { 95 | sslMode = "require" 96 | } 97 | dsn := fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", 98 | cfg.Application.Database.Postgres.Auth.Username, 99 | cfg.Application.Database.Postgres.Auth.Password, 100 | cfg.Application.Database.Postgres.Addr, 101 | sslMode, 102 | ) 103 | 104 | // Connect to Postgres 105 | db, err := sql.Open("postgres", dsn) 106 | if err != nil { 107 | logger.Error("database connection failed", "err", err) 108 | os.Exit(1) 109 | } 110 | defer db.Close() 111 | if err := db.PingContext(context.Background()); err != nil { 112 | logger.Error("database ping failed", "err", err) 113 | os.Exit(1) 114 | } 115 | 116 | // Initialize storage and domain services 117 | userStorage := storage.NewPostgresStorage(db) 118 | userService := domainuser.NewService(logger, userStorage) 119 | 120 | // Initialize LLM clients 121 | openaiClient := openai.NewClient(logger, cfg.Application.Clients.OpenAI) 122 | // claudeClient := claude.NewClaudeClient(logger, cfg.Application.Clients.Claude) 123 | 124 | // Choose streamer; here using OpenAI but could swap to Claude 125 | var llmStreamer llm.Streamer = openaiClient 126 | 127 | // Initialize prompt loader 128 | pl, err := promptloader.NewPromptLoader() 129 | if err != nil { 130 | logger.Error("prompt loader init failed", "err", err) 131 | os.Exit(1) 132 | } 133 | 134 | // Initialize agent 135 | statAgent := stat_analyzer.NewAgent(logger, llmStreamer, *pl, userService) 136 | 137 | // Setup HTTP router 138 | r := chi.NewRouter() 139 | http2.RegisterRoutes(r, statAgent, logger) 140 | 141 | // Start HTTP server 142 | srv := &http.Server{ 143 | Addr: cfg.Application.Server.Public.Addr, 144 | Handler: r, 145 | ReadTimeout: cfg.Application.Server.Public.Timeout, 146 | WriteTimeout: cfg.Application.Server.Public.Timeout, 147 | IdleTimeout: cfg.Application.Server.Public.Timeout, 148 | } 149 | logger.Info("listening on", "addr", srv.Addr) 150 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 151 | logger.Error("server error", "err", err) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/compiai/engine 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.2.1 // indirect 7 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 8 | github.com/google/uuid v1.6.0 // indirect 9 | github.com/lib/pq v1.10.9 // indirect 10 | golang.org/x/crypto v0.37.0 // indirect 11 | gopkg.in/yaml.v2 v2.4.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 2 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 3 | github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= 4 | github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 5 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 6 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 8 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 9 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 10 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 13 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 14 | -------------------------------------------------------------------------------- /internal/api/http/analysis_handler.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/go-chi/chi/v5" 10 | "github.com/google/uuid" 11 | "log/slog" 12 | 13 | "github.com/compiai/engine/internal/core/domain/agent/stat_analyzer" 14 | ) 15 | 16 | // AnalysisRequest defines the payload for analysis. 17 | type AnalysisRequest struct { 18 | UserID uuid.UUID `json:"userId"` 19 | } 20 | 21 | // Validate checks required fields in AnalysisRequest. 22 | func (r *AnalysisRequest) Validate() error { 23 | if r.UserID == uuid.Nil { 24 | return errors.New("userId is required") 25 | } 26 | return nil 27 | } 28 | 29 | // AnalysisResponse is sent for each analysis segment. 30 | type AnalysisResponse struct { 31 | ID string `json:"id"` 32 | Content string `json:"content"` 33 | Error string `json:"error,omitempty"` 34 | } 35 | 36 | // RegisterRoutes mounts stat analyzer endpoints onto the router. 37 | func RegisterRoutes(r chi.Router, agent stat_analyzer.Agent, logger *slog.Logger) { 38 | r.Route("/analysis", func(r chi.Router) { 39 | r.Post("/", makeAnalysisHandler(agent, logger)) 40 | }) 41 | } 42 | 43 | // makeAnalysisHandler streams analysis via Server-Sent Events. 44 | func makeAnalysisHandler(agent stat_analyzer.Agent, logger *slog.Logger) http.HandlerFunc { 45 | return func(w http.ResponseWriter, req *http.Request) { 46 | // Decode and validate JSON request body. 47 | var reqModel AnalysisRequest 48 | if err := json.NewDecoder(req.Body).Decode(&reqModel); err != nil { 49 | logger.Error("invalid request body", "err", err) 50 | http.Error(w, "invalid request body", http.StatusBadRequest) 51 | return 52 | } 53 | if err := reqModel.Validate(); err != nil { 54 | logger.Error("validation error", "err", err) 55 | http.Error(w, err.Error(), http.StatusBadRequest) 56 | return 57 | } 58 | 59 | // Set SSE headers. 60 | w.Header().Set("Content-Type", "text/event-stream") 61 | w.Header().Set("Cache-Control", "no-cache, no-transform") 62 | w.Header().Set("Connection", "keep-alive") 63 | flusher, ok := w.(http.Flusher) 64 | if !ok { 65 | http.Error(w, "streaming unsupported", http.StatusInternalServerError) 66 | return 67 | } 68 | 69 | // Initiate analysis stream. 70 | ctx := req.Context() 71 | stream, err := agent.BuildAnalysis(ctx, stat_analyzer.BuildAnalysisRequest{UserID: reqModel.UserID}) 72 | if err != nil { 73 | logger.Error("analysis build error", "err", err) 74 | http.Error(w, "analysis error", http.StatusInternalServerError) 75 | return 76 | } 77 | 78 | // Stream SSE events. 79 | for resp := range stream { 80 | // Build and marshal response. 81 | res := AnalysisResponse{ID: resp.ID, Content: resp.Content} 82 | if resp.Error != nil { 83 | res.Error = resp.Error.Error() 84 | } 85 | payload, err := json.Marshal(res) 86 | if err != nil { 87 | logger.Error("marshal response failed", "err", err) 88 | continue 89 | } 90 | 91 | // Send SSE event. 92 | w.Write([]byte("event: analysis\n")) 93 | w.Write([]byte("data: ")) 94 | w.Write(payload) 95 | w.Write([]byte("\n\n")) 96 | flusher.Flush() 97 | 98 | // Small delay to regulate stream. 99 | time.Sleep(10 * time.Millisecond) 100 | } 101 | 102 | // Final end event. 103 | w.Write([]byte("event: end\n")) 104 | w.Write([]byte("data: {}\n\n")) 105 | flusher.Flush() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /internal/api/http/handlers.go: -------------------------------------------------------------------------------- 1 | package http 2 | -------------------------------------------------------------------------------- /internal/core/domain/agent/stat_analyzer/agent.go: -------------------------------------------------------------------------------- 1 | package stat_analyzer 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | prompts "github.com/compiai/engine/internal/core/domain/agent/stat_analyzer/prompt" 8 | "github.com/compiai/engine/internal/core/domain/user" 9 | "github.com/compiai/engine/pkg/llm" 10 | "github.com/google/uuid" 11 | "log/slog" 12 | "math" 13 | "time" 14 | ) 15 | 16 | type BuildAnalysisRequest struct { 17 | UserID uuid.UUID 18 | } 19 | 20 | type BuildAnalysisStreamResponse struct { 21 | ID string `json:"id"` 22 | Content string `json:"content"` 23 | Error error `json:"error"` 24 | } 25 | 26 | type Agent interface { 27 | BuildAnalysis(ctx context.Context, request BuildAnalysisRequest) (<-chan BuildAnalysisStreamResponse, error) 28 | } 29 | 30 | type agent struct { 31 | logger *slog.Logger 32 | llmStreamer llm.Streamer 33 | promptLoader prompts.PromptLoader 34 | userService user.Service 35 | } 36 | 37 | // Agent defines the streaming analysis interface 38 | func NewAgent( 39 | logger *slog.Logger, 40 | streamer llm.Streamer, 41 | loader prompts.PromptLoader, 42 | usrSvc user.Service, 43 | ) Agent { 44 | return &agent{ 45 | logger: logger.WithGroup("stat-analyzer-agent"), 46 | llmStreamer: streamer, 47 | promptLoader: loader, 48 | userService: usrSvc, 49 | } 50 | } 51 | 52 | // BuildAnalysis performs a multi-stage stats processing and streams an LLM-based analysis 53 | func (a *agent) BuildAnalysis(ctx context.Context, req BuildAnalysisRequest) (<-chan BuildAnalysisStreamResponse, error) { 54 | // Stage 1: Load raw user profile 55 | usr, err := a.userService.FindOne(ctx, user.SingleFilter{ID: &req.UserID}) 56 | if err != nil { 57 | a.logger.Error("user lookup failed", "err", err) 58 | return nil, fmt.Errorf("user lookup: %w", err) 59 | } 60 | 61 | // Stage 2: Derive advanced metrics 62 | advanced := deriveMetrics(usr) 63 | 64 | // Stage 3: Compose combined data for prompting 65 | data := struct { 66 | Profile user.User `json:"profile"` 67 | Advanced map[string]interface{} `json:"advancedMetrics"` 68 | Request BuildAnalysisRequest `json:"request"` 69 | }{usr, advanced, req} 70 | 71 | a.logger.Info("build started...", "data", data) 72 | 73 | // Stage 4: Render prompts 74 | sys := a.promptLoader.GetSystemPrompt() 75 | 76 | usrPr := a.promptLoader.GetUserPrompt() 77 | 78 | // Stage 5: Initiate LLM streaming 79 | genReq := llm.GenerateRequest{Prompt: llm.Prompt{System: sys, User: usrPr}} 80 | stream, err := a.llmStreamer.Stream(ctx, genReq) 81 | if err != nil { 82 | return nil, fmt.Errorf("LLM stream: %w", err) 83 | } 84 | 85 | // Stage 6: Process and enrich stream responses 86 | out := make(chan BuildAnalysisStreamResponse) 87 | go func() { 88 | defer close(out) 89 | for msg := range stream { 90 | // Inject timestamp metadata and segment tags 91 | segment := time.Now().Format(time.RFC3339Nano) 92 | meta := map[string]string{"segment": segment} 93 | if payload, err := json.Marshal(meta); err == nil { 94 | msg.Response = string(payload) + "\n" + msg.Response 95 | } 96 | out <- BuildAnalysisStreamResponse{ID: msg.ID, Content: msg.Response, Error: msg.Error} 97 | } 98 | }() 99 | 100 | return out, nil 101 | } 102 | 103 | // deriveMetrics computes advanced analytic metrics from user data 104 | func deriveMetrics(u user.User) map[string]interface{} { 105 | metrics := make(map[string]interface{}) 106 | 107 | // Example: Kill/Death Ratio 108 | kd := float64(countTotalKills(u.Games)) / math.Max(1, float64(countTotalDeaths(u.Games))) 109 | metrics["killDeathRatio"] = fmt.Sprintf("%.2f", kd) 110 | 111 | // Example: Engagement consistency score via variance 112 | scores := extractEngagementScores(u.Games) 113 | metrics["engagementConsistency"] = variance(scores) 114 | 115 | // Example: Popular role synergy cluster (mocked) 116 | metrics["synergyCluster"] = clusterRoleSynergy(u.Games) 117 | 118 | return metrics 119 | } 120 | 121 | // countTotalKills tallies kills (stub implementation) 122 | func countTotalKills(games []string) int { 123 | // placeholder: parse and sum 124 | return len(games) * 5 125 | } 126 | 127 | // countTotalDeaths tallies deaths (stub implementation) 128 | func countTotalDeaths(games []string) int { 129 | return len(games) * 3 130 | } 131 | 132 | // extractEngagementScores returns mock engagement values 133 | func extractEngagementScores(games []string) []float64 { 134 | var s []float64 135 | for i := range games { 136 | s = append(s, float64((i%5)+1)) 137 | } 138 | return s 139 | } 140 | 141 | // variance computes variance of a float64 slice 142 | func variance(data []float64) float64 { 143 | mean := 0.0 144 | for _, v := range data { 145 | mean += v 146 | } 147 | mean /= float64(len(data)) 148 | varSum := 0.0 149 | for _, v := range data { 150 | varSum += (v - mean) * (v - mean) 151 | } 152 | return varSum / float64(len(data)) 153 | } 154 | 155 | // clusterRoleSynergy returns a stub cluster name 156 | func clusterRoleSynergy(games []string) string { 157 | // pretend clustering logic 158 | return "Alpha-Synergy" 159 | } 160 | -------------------------------------------------------------------------------- /internal/core/domain/agent/stat_analyzer/prompt/analyze_stats.tmpl: -------------------------------------------------------------------------------- 1 | {{define "DetailedGamingPrompt"}} 2 | You are an elite-level competitive gaming coach, performance analyst, and strategic advisor with over ten years of experience working alongside top-tier professional esports organizations. Your assignment is to meticulously analyze a comprehensive dataset of a player’s in-game performance, encompassing: 3 | 4 | - **Raw Metrics:** kill/death/assist ratios, damage per minute, objective control percentages, vision placement efficiency, gold per minute, resource acquisition rates. 5 | - **Advanced Data:** positional heatmap distributions, play clustering for early/late game, clutch engagement success rate, teamfight initiation metrics, rotational speed, and decision latency under pressure. 6 | - **Contextual Indicators:** win/loss momentum swings, comeback proficiency, map-specific proficiency differentials, role-specific benchmark comparisons, and peer percentile rankings. 7 | 8 | Using this multidimensional information, structure your output as follows: 9 | 10 | 1. **Comprehensive Profile Summary:** Deliver a narrative overview (150–180 words) synthesizing mechanical prowess, strategic understanding, and psychological resilience. Reference at least three quantitative data points (e.g., “Your average headshot accuracy of 48% during high-pressure clutch rounds places you in the 85th percentile for your rank”). Highlight both standout strengths and emergent weaknesses by contrasting individual performance to normative role-based benchmarks. 11 | 12 | 2. **In-Depth Strengths & Weaknesses Analysis:** Construct two subsections: 13 | - **Core Strengths (3 items):** For each strength, provide the metric name, value, and percentile. For example, detail superior objective control (“Your objective capture rate is 72%, 2 standard deviations above the mean for diamond-level players”). 14 | - **Development Areas (3 items):** Identify critical improvement zones, each with precise data anomalies (e.g., “Your vision score drops below 20 before minute 10 in 70% of games, indicating a lack of early ward coverage”). Explain how these deficiencies impair performance. 15 | 16 | 3. **Targeted Improvement Plan:** Offer a tripartite roadmap of actionable interventions for each of the three weaknesses: 17 | - **Skill Drill:** Describe a specific, time-bound drill (e.g., “Complete 100 consecutive accuracy shots in training mode focusing on target transitions under induced timeout pressure”). 18 | - **Analytic Reflection Ritual:** Outline a structured review process (e.g., “Replay your last five ranked matches, annotate each death location, and identify decision inflection points—allocate 45 minutes for this analysis”). 19 | - **Mindset Conditioning:** Recommend mental exercises (e.g., “Implement a two-minute breathing and focus meditation before each match to lower decision latency by 15% and curb tilt-induced errors”). 20 | 21 | 4. **Practice Session Blueprint (2–3 hours):** Curate a sequenced practice itinerary: 22 | - **Module 1 (30 min):** Warm-up micro-mechanical drills, 23 | - **Module 2 (45 min):** Role-specific strategy scenarios, 24 | - **Module 3 (60 min):** Live scrimmage with post-round debrief, 25 | - **Module 4 (15 min):** Reflective cooldown and journaling of key takeaways. 26 | 27 | End your response with a motivating closing statement that reinforces growth mindset principles and encourages disciplined, data-driven practice. Use precise, constructive, and supportive language throughout to ensure the player feels empowered to transform these insights into measurable performance gains. 28 | {{end}} 29 | -------------------------------------------------------------------------------- /internal/core/domain/agent/stat_analyzer/prompt/improvement_plan.tmpl: -------------------------------------------------------------------------------- 1 | {{define "ImprovementPlanPrompt"}} 2 | Using the identified development areas from your performance data, craft a detailed **Improvement Plan** comprised of three distinct intervention categories per weakness. For each of the three weaknesses: 3 | 4 | 1. **Skill Drill**: Describe a targeted, measurable exercise that directly addresses the deficiency. Include: 5 | - **Duration** (e.g., 20–30 minutes) 6 | - **Specific Setup** (e.g., “use workshop mode with moving bots at varying speeds”) 7 | - **Performance Goal** (e.g., “achieve 90% hit accuracy on 100 sequential targets”) 8 | 9 | 2. **Analytic Reflection Ritual**: Outline a structured review protocol. Provide: 10 | - **Replay Scope** (e.g., “last five ranked matches”) 11 | - **Annotation Focus** (e.g., “note every death’s location, time, and teamfight impact”) 12 | - **Time Allocation** (e.g., “45 minutes total”) and **Deliverable** (e.g., “write a one-page summary of recurring decision points”). 13 | 14 | 3. **Mindset Conditioning**: Recommend a mental exercise to reinforce resilience and focus. Include: 15 | - **Pre-session Routine** (e.g., “two-minute breathing meditation with box breathing technique”) 16 | - **In-session Cue** (e.g., “visual trigger when tilt is detected”) 17 | - **Expected Outcome** (e.g., “reduce tilt incidents by 50% over next week”). 18 | 19 | Format the plan as a clear, numbered list grouping exercises by weakness, with each exercise titled and bullet-pointed under its category. Conclude with a motivating statement that reinforces consistency, measurement, and data-driven growth. 20 | {{end}} 21 | -------------------------------------------------------------------------------- /internal/core/domain/agent/stat_analyzer/prompt/loader.go: -------------------------------------------------------------------------------- 1 | package prompts 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "text/template" 7 | ) 8 | 9 | //go:embed *.tmpl 10 | var promptFS embed.FS 11 | 12 | var ( 13 | SystemPromptTemplate string 14 | UserPromptTemplate string 15 | ) 16 | 17 | // PromptLoader loads and executes prompt templates. 18 | type PromptLoader struct { 19 | templates *template.Template 20 | } 21 | 22 | // NewPromptLoader parses all .tmpl files in the package and returns a loader. 23 | func NewPromptLoader() (*PromptLoader, error) { 24 | tmpl, err := template.New("").ParseFS(promptFS, "*.tmpl") 25 | if err != nil { 26 | return nil, err 27 | } 28 | return &PromptLoader{templates: tmpl}, nil 29 | } 30 | 31 | // GetSystemPrompt returns the system prompt constant from gaming_prompts.go. 32 | func (pl *PromptLoader) GetSystemPrompt() string { 33 | return SystemPromptTemplate 34 | } 35 | 36 | // GetUserPrompt returns the user prompt constant from gaming_prompts.go. 37 | func (pl *PromptLoader) GetUserPrompt() string { 38 | return UserPromptTemplate 39 | } 40 | 41 | // GetDetailedGamingPrompt executes the DetailedGamingPrompt template with the given data. 42 | func (pl *PromptLoader) GetDetailedGamingPrompt(data interface{}) (string, error) { 43 | var buf bytes.Buffer 44 | if err := pl.templates.ExecuteTemplate(&buf, "DetailedGamingPrompt", data); err != nil { 45 | return "", err 46 | } 47 | return buf.String(), nil 48 | } 49 | 50 | // GetImprovementPlanPrompt executes the ImprovementPlanPrompt template with the given data. 51 | func (pl *PromptLoader) GetImprovementPlanPrompt(data interface{}) (string, error) { 52 | var buf bytes.Buffer 53 | if err := pl.templates.ExecuteTemplate(&buf, "ImprovementPlanPrompt", data); err != nil { 54 | return "", err 55 | } 56 | return buf.String(), nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/core/domain/user/model.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "github.com/google/uuid" 4 | 5 | type User struct { 6 | ID uuid.UUID 7 | Username string 8 | SolanaWalletPublicKey string 9 | PasswordHash string 10 | Games []string // list of games player's interested in 11 | } 12 | 13 | type NewUser struct { 14 | Username string 15 | SolanaWalletPublicKey string 16 | PasswordHash string 17 | } 18 | 19 | type Credentials struct { 20 | Username string 21 | Password string 22 | } 23 | 24 | type TokenPair struct { 25 | AccessToken string 26 | RefreshToken string 27 | } 28 | -------------------------------------------------------------------------------- /internal/core/domain/user/service.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "" 5 | "context" 6 | "errors" 7 | "github.com/golang-jwt/jwt/v4" 8 | "github.com/google/uuid" 9 | "golang.org/x/crypto/bcrypt" 10 | "log/slog" 11 | "time" 12 | ) 13 | 14 | type SingleFilter struct { 15 | ID *uuid.UUID 16 | Username *string 17 | SolanaWallet *string 18 | } 19 | 20 | type Filter struct { 21 | IDs []uuid.UUID 22 | Usernames []string 23 | SolanaWallets []string 24 | } 25 | 26 | type Service interface { 27 | Register(ctx context.Context, newUser NewUser) error 28 | FindOne(ctx context.Context, filter SingleFilter) (User, error) 29 | Find(ctx context.Context, filter Filter) ([]User, error) 30 | } 31 | 32 | type AuthService interface { 33 | GenerateTokenPair(ctx context.Context, creds Credentials) (TokenPair, error) 34 | Refresh(ctx context.Context, refreshToken string) (TokenPair, error) 35 | } 36 | 37 | type service struct { 38 | logger *slog.Logger 39 | userStorage Storage 40 | } 41 | 42 | func NewService(logger *slog.Logger, userStorage Storage) *service { 43 | return &service{ 44 | logger: logger.WithGroup("core-user-service"), 45 | userStorage: userStorage, 46 | } 47 | } 48 | 49 | // service implements Service and AuthService 50 | 51 | type authService struct { 52 | logger *slog.Logger 53 | userStorage Storage 54 | jwtSecret []byte 55 | accessTTL time.Duration 56 | refreshTTL time.Duration 57 | } 58 | 59 | // NewAuthService creates a new AuthService with given secret and storage 60 | func NewAuthService(logger *slog.Logger, storage Storage, jwtSecret []byte) AuthService { 61 | return &authService{ 62 | logger: logger.WithGroup("auth-service"), 63 | userStorage: storage, 64 | jwtSecret: jwtSecret, 65 | accessTTL: 15 * time.Minute, 66 | refreshTTL: 7 * 24 * time.Hour, 67 | } 68 | } 69 | 70 | func (a *authService) GenerateTokenPair(ctx context.Context, creds Credentials) (TokenPair, error) { 71 | // authenticate user 72 | user, err := a.userStorage.FindOneByUsername(ctx, creds.Username) 73 | if err != nil { 74 | return TokenPair{}, err 75 | } 76 | if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(creds.Password)); err != nil { 77 | return TokenPair{}, errors.New("invalid credentials") 78 | } 79 | // generate tokens 80 | accessToken, err := a.makeToken(user.ID, a.accessTTL) 81 | if err != nil { 82 | return TokenPair{}, err 83 | } 84 | refreshToken, err := a.makeToken(user.ID, a.refreshTTL) 85 | if err != nil { 86 | return TokenPair{}, err 87 | } 88 | return TokenPair{AccessToken: accessToken, RefreshToken: refreshToken}, nil 89 | } 90 | 91 | func (a *authService) Refresh(ctx context.Context, refreshToken string) (TokenPair, error) { 92 | // parse token 93 | claims := &jwt.RegisteredClaims{} 94 | tkn, err := jwt.ParseWithClaims(refreshToken, claims, func(token *jwt.Token) (interface{}, error) { 95 | return a.jwtSecret, nil 96 | }) 97 | if err != nil || !tkn.Valid { 98 | return TokenPair{}, errors.New("invalid refresh token") 99 | } 100 | id, err := uuid.Parse(claims.Subject) 101 | if err != nil { 102 | return TokenPair{}, err 103 | } 104 | // issue new tokens 105 | accessToken, err := a.makeToken(id, a.accessTTL) 106 | if err != nil { 107 | return TokenPair{}, err 108 | } 109 | newRefresh, err := a.makeToken(id, a.refreshTTL) 110 | if err != nil { 111 | return TokenPair{}, err 112 | } 113 | return TokenPair{AccessToken: accessToken, RefreshToken: newRefresh}, nil 114 | } 115 | 116 | func (a *authService) makeToken(userID uuid.UUID, ttl time.Duration) (string, error) { 117 | claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ 118 | Subject: userID.String(), 119 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)), 120 | IssuedAt: jwt.NewNumericDate(time.Now()), 121 | }) 122 | return claims.SignedString(a.jwtSecret) 123 | } 124 | 125 | // Service methods (Register, FindOne, Find) 126 | 127 | func (s *service) Register(ctx context.Context, newUser NewUser) error { 128 | id := uuid.New() 129 | user := User{ 130 | ID: id, 131 | Username: newUser.Username, 132 | SolanaWalletPublicKey: newUser.SolanaWalletPublicKey, 133 | PasswordHash: newUser.PasswordHash, 134 | Games: []string{}, 135 | } 136 | err := s.userStorage.Save(ctx, user) 137 | if err != nil { 138 | s.logger.Error("register failed", "error", err) 139 | } 140 | return err 141 | } 142 | 143 | func (s *service) FindOne(ctx context.Context, filter SingleFilter) (User, error) { 144 | return s.userStorage.FindOne(ctx, filter) 145 | } 146 | 147 | func (s *service) Find(ctx context.Context, filter Filter) ([]User, error) { 148 | return s.userStorage.Find(ctx, filter) 149 | } 150 | -------------------------------------------------------------------------------- /internal/core/domain/user/storage.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type Storage interface { 9 | Save(ctx context.Context, user User) error 10 | 11 | FindOne(ctx context.Context, filter SingleFilter) (User, error) 12 | FindOneByUsername(ctx context.Context, username string) (User, error) 13 | FindOneByID(ctx context.Context, id uuid.UUID) (User, error) 14 | FindOneBySolanaWallet(ctx context.Context, solanaWallet string) (User, error) 15 | 16 | Find(ctx context.Context, filter Filter) ([]User, error) 17 | } 18 | -------------------------------------------------------------------------------- /internal/core/ext/storage/user.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "github.com/compiai/engine/internal/core/domain/user" 8 | "github.com/google/uuid" 9 | "github.com/lib/pq" 10 | "strings" 11 | ) 12 | 13 | // PostgresStorage implements Storage using a PostgreSQL database. 14 | type PostgresStorage struct { 15 | db *sql.DB 16 | } 17 | 18 | // NewPostgresStorage creates a new PostgresStorage. 19 | func NewPostgresStorage(db *sql.DB) *PostgresStorage { 20 | return &PostgresStorage{db: db} 21 | } 22 | 23 | func (s *PostgresStorage) Save(ctx context.Context, usr user.User) error { 24 | query := ` 25 | INSERT INTO users (id, username, solana_wallet_public_key, password_hash, games) 26 | VALUES ($1, $2, $3, $4, $5) 27 | ON CONFLICT (id) DO UPDATE 28 | SET username = EXCLUDED.username, 29 | solana_wallet_public_key = EXCLUDED.solana_wallet_public_key, 30 | password_hash = EXCLUDED.password_hash, 31 | games = EXCLUDED.games 32 | ` 33 | _, err := s.db.ExecContext(ctx, query, 34 | usr.ID, usr.Username, usr.SolanaWalletPublicKey, usr.PasswordHash, pq.Array(usr.Games), 35 | ) 36 | return err 37 | } 38 | 39 | func (s *PostgresStorage) FindOne(ctx context.Context, filter user.SingleFilter) (user.User, error) { 40 | // delegate to specific methods 41 | if filter.ID != nil { 42 | return s.FindOneByID(ctx, *filter.ID) 43 | } 44 | if filter.Username != nil { 45 | return s.FindOneByUsername(ctx, *filter.Username) 46 | } 47 | if filter.SolanaWallet != nil { 48 | return s.FindOneBySolanaWallet(ctx, *filter.SolanaWallet) 49 | } 50 | return user.User{}, sql.ErrNoRows 51 | } 52 | 53 | func (s *PostgresStorage) FindOneByUsername(ctx context.Context, username string) (user.User, error) { 54 | var u user.User 55 | query := ` 56 | SELECT id, username, solana_wallet_public_key, password_hash, games 57 | FROM users WHERE username = $1 58 | ` 59 | err := s.db.QueryRowContext(ctx, query, username).Scan( 60 | &u.ID, &u.Username, &u.SolanaWalletPublicKey, &u.PasswordHash, pq.Array(&u.Games), 61 | ) 62 | return u, err 63 | } 64 | 65 | func (s *PostgresStorage) FindOneByID(ctx context.Context, id uuid.UUID) (user.User, error) { 66 | var u user.User 67 | query := ` 68 | SELECT id, username, solana_wallet_public_key, password_hash, games 69 | FROM users WHERE id = $1 70 | ` 71 | err := s.db.QueryRowContext(ctx, query, id).Scan( 72 | &u.ID, &u.Username, &u.SolanaWalletPublicKey, &u.PasswordHash, pq.Array(&u.Games), 73 | ) 74 | return u, err 75 | } 76 | 77 | func (s *PostgresStorage) FindOneBySolanaWallet(ctx context.Context, solanaWallet string) (user.User, error) { 78 | var u user.User 79 | query := ` 80 | SELECT id, username, solana_wallet_public_key, password_hash, games 81 | FROM users WHERE solana_wallet_public_key = $1 82 | ` 83 | err := s.db.QueryRowContext(ctx, query, solanaWallet).Scan( 84 | &u.ID, &u.Username, &u.SolanaWalletPublicKey, &u.PasswordHash, pq.Array(&u.Games), 85 | ) 86 | return u, err 87 | } 88 | 89 | func (s *PostgresStorage) Find(ctx context.Context, filter user.Filter) ([]user.User, error) { 90 | // build dynamic IN clauses 91 | clauses := []string{} 92 | args := []interface{}{} 93 | idx := 1 94 | if len(filter.IDs) > 0 { 95 | clauses = append(clauses, fmt.Sprintf("id = ANY($%d)", idx)) 96 | args = append(args, pq.Array(filter.IDs)) 97 | idx++ 98 | } 99 | if len(filter.Usernames) > 0 { 100 | clauses = append(clauses, fmt.Sprintf("username = ANY($%d)", idx)) 101 | args = append(args, pq.Array(filter.Usernames)) 102 | idx++ 103 | } 104 | if len(filter.SolanaWallets) > 0 { 105 | clauses = append(clauses, fmt.Sprintf("solana_wallet_public_key = ANY($%d)", idx)) 106 | args = append(args, pq.Array(filter.SolanaWallets)) 107 | idx++ 108 | } 109 | if len(clauses) == 0 { 110 | return nil, nil 111 | } 112 | query := "SELECT id, username, solana_wallet_public_key, password_hash, games FROM users WHERE " + strings.Join(clauses, " AND ") 113 | rows, err := s.db.QueryContext(ctx, query, args...) 114 | if err != nil { 115 | return nil, err 116 | } 117 | defer rows.Close() 118 | users := []user.User{} 119 | for rows.Next() { 120 | var u user.User 121 | err := rows.Scan(&u.ID, &u.Username, &u.SolanaWalletPublicKey, &u.PasswordHash, pq.Array(&u.Games)) 122 | if err != nil { 123 | return nil, err 124 | } 125 | users = append(users, u) 126 | } 127 | return users, rows.Err() 128 | } 129 | -------------------------------------------------------------------------------- /pkg/llm/claude/claude_client.go: -------------------------------------------------------------------------------- 1 | package claude 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "github.com/compiai/engine/pkg/llm" 10 | "io" 11 | "net/http" 12 | "strings" 13 | ) 14 | 15 | type Config struct { 16 | ApiKey string `yaml:"apiKey"` 17 | Endpoint string `yaml:"endpoint"` 18 | Model string `yaml:"model"` 19 | Temperature float64 `yaml:"temperature"` 20 | MaxTokensToSample int `yaml:"maxTokensToSample"` 21 | } 22 | 23 | // ClaudeClient implements llm.Streamer using Anthropic's Claude API. 24 | type ClaudeClient struct { 25 | config Config 26 | httpClient *http.Client 27 | } 28 | 29 | // NewClaudeClient creates a new Claude client using provided Config. 30 | func NewClaudeClient(cfg Config) *ClaudeClient { 31 | return &ClaudeClient{config: cfg, httpClient: http.DefaultClient} 32 | } 33 | 34 | func (c *ClaudeClient) Stream(ctx context.Context, request llm.GenerateRequest) (<-chan llm.GenerateStreamResponse, error) { 35 | 36 | // Build the Anthropic prompt 37 | var sb strings.Builder 38 | if request.Prompt.System != "" { 39 | sb.WriteString("\n\nHuman: ") 40 | sb.WriteString(request.Prompt.System) 41 | } 42 | for _, conv := range request.History { 43 | sb.WriteString("\n\nHuman: ") 44 | sb.WriteString(conv.Request) 45 | sb.WriteString("\n\nAssistant: ") 46 | sb.WriteString(conv.Response) 47 | } 48 | sb.WriteString("\n\nHuman: ") 49 | sb.WriteString(request.Prompt.User) 50 | sb.WriteString("\n\nAssistant:") 51 | 52 | reqBody := apiRequest{ 53 | Model: c.config.Model, 54 | Prompt: sb.String(), 55 | MaxTokensToSample: c.config.MaxTokensToSample, 56 | Temperature: c.config.Temperature, 57 | Stream: true, 58 | } 59 | data, err := json.Marshal(reqBody) 60 | if err != nil { 61 | return nil, fmt.Errorf("Claude marshal request: %w", err) 62 | } 63 | 64 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, 65 | strings.TrimRight(c.config.Endpoint, "/"), bytes.NewReader(data), 66 | ) 67 | if err != nil { 68 | return nil, fmt.Errorf("Claude new request: %w", err) 69 | } 70 | httpReq.Header.Set("Content-Type", "application/json") 71 | httpReq.Header.Set("X-API-Key", c.config.ApiKey) 72 | 73 | resp, err := c.httpClient.Do(httpReq) 74 | if err != nil { 75 | return nil, fmt.Errorf("Claude do request: %w", err) 76 | } 77 | if resp.StatusCode != http.StatusOK { 78 | defer resp.Body.Close() 79 | errBody, _ := io.ReadAll(resp.Body) 80 | return nil, fmt.Errorf("Claude error response: %s", string(errBody)) 81 | } 82 | 83 | ch := make(chan llm.GenerateStreamResponse) 84 | go func() { 85 | defer close(ch) 86 | reader := bufio.NewReader(resp.Body) 87 | for { 88 | line, err := reader.ReadString('\n') 89 | if err != nil { 90 | if err != io.EOF { 91 | ch <- llm.GenerateStreamResponse{Error: err} 92 | } 93 | return 94 | } 95 | line = strings.TrimSpace(line) 96 | if !strings.HasPrefix(line, "data:") { 97 | continue 98 | } 99 | payload := strings.TrimPrefix(line, "data: ") 100 | if payload == "[DONE]" { 101 | return 102 | } 103 | var event struct { 104 | Completion string `json:"completion"` 105 | } 106 | if err := json.Unmarshal([]byte(payload), &event); err != nil { 107 | ch <- llm.GenerateStreamResponse{Error: fmt.Errorf("Claude invalid stream: %w", err)} 108 | return 109 | } 110 | if event.Completion != "" { 111 | ch <- llm.GenerateStreamResponse{Response: event.Completion} 112 | } 113 | } 114 | }() 115 | 116 | return ch, nil 117 | } 118 | -------------------------------------------------------------------------------- /pkg/llm/claude/model.go: -------------------------------------------------------------------------------- 1 | package claude 2 | 3 | type apiRequest struct { 4 | Model string `json:"model"` 5 | Prompt string `json:"prompt"` 6 | MaxTokensToSample int `json:"max_tokens_to_sample"` 7 | Temperature float64 `json:"temperature"` 8 | Stream bool `json:"stream"` 9 | } 10 | -------------------------------------------------------------------------------- /pkg/llm/llm.go: -------------------------------------------------------------------------------- 1 | package llm 2 | 3 | import "context" 4 | 5 | type GenerateRequest struct { 6 | Prompt Prompt `json:"prompt"` 7 | History []Conversation `json:"conversation"` 8 | } 9 | 10 | type GenerateStreamResponse struct { 11 | ID string `json:"id"` 12 | Response string `json:"response"` 13 | Error error // if no error this should be null/nil 14 | } 15 | 16 | // GenerateResponse 17 | // aggregated response, if someone wants to get the result without streaming 18 | type GenerateResponse struct { 19 | Response string `json:"response"` 20 | } 21 | 22 | type Streamer interface { 23 | Stream(ctx context.Context, request GenerateRequest) (<-chan GenerateStreamResponse, error) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/llm/model.go: -------------------------------------------------------------------------------- 1 | package llm 2 | 3 | type Prompt struct { 4 | System string `json:"system"` 5 | User string `json:"user"` 6 | } 7 | 8 | type Conversation struct { 9 | Request string `json:"request"` 10 | Response string `json:"response"` 11 | } 12 | -------------------------------------------------------------------------------- /pkg/llm/openai/model.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | // Model represents an OpenAI model 4 | // https://platform.openai.com/docs/api-reference/models 5 | // https://platform.openai.com/docs/api-reference/models/retrieve 6 | 7 | type Model struct { 8 | ID string `json:"id"` 9 | Object string `json:"object"` 10 | OwnedBy string `json:"owned_by"` 11 | Permission []ModelPermission `json:"permission"` 12 | Root string `json:"root,omitempty"` 13 | Parent string `json:"parent,omitempty"` 14 | } 15 | 16 | type ModelPermission struct { 17 | ID string `json:"id"` 18 | Object string `json:"object"` 19 | Created int64 `json:"created"` 20 | AllowCreateEngine bool `json:"allow_create_engine"` 21 | AllowSampling bool `json:"allow_sampling"` 22 | AllowLogprobs bool `json:"allow_logprobs"` 23 | AllowSearchIndices bool `json:"allow_search_indices"` 24 | AllowView bool `json:"allow_view"` 25 | AllowFineTuning bool `json:"allow_fine_tuning"` 26 | Organization string `json:"organization"` 27 | Group string `json:"group,omitempty"` 28 | IsBlocking bool `json:"is_blocking"` 29 | } 30 | 31 | type ListModelsResponse struct { 32 | Data []Model `json:"data"` 33 | Object string `json:"object"` 34 | } 35 | 36 | type RetrieveModelResponse = Model 37 | 38 | // Usage represents token usage 39 | 40 | type Usage struct { 41 | PromptTokens int `json:"prompt_tokens"` 42 | CompletionTokens int `json:"completion_tokens"` 43 | TotalTokens int `json:"total_tokens"` 44 | } 45 | 46 | // Completion endpoint structs 47 | // https://platform.openai.com/docs/api-reference/completions 48 | 49 | // CompletionRequest is used to create a text completion 50 | 51 | type CompletionRequest struct { 52 | Model string `json:"model"` 53 | Prompt interface{} `json:"prompt,omitempty"` // string or []string 54 | Suffix string `json:"suffix,omitempty"` 55 | MaxTokens int `json:"max_tokens,omitempty"` 56 | Temperature float64 `json:"temperature,omitempty"` 57 | TopP float64 `json:"top_p,omitempty"` 58 | N int `json:"n,omitempty"` 59 | Stream bool `json:"stream,omitempty"` 60 | Logprobs int `json:"logprobs,omitempty"` 61 | Echo bool `json:"echo,omitempty"` 62 | Stop interface{} `json:"stop,omitempty"` // string or []string 63 | PresencePenalty float64 `json:"presence_penalty,omitempty"` 64 | FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` 65 | BestOf int `json:"best_of,omitempty"` 66 | LogitBias map[string]int `json:"logit_bias,omitempty"` 67 | User string `json:"user,omitempty"` 68 | } 69 | 70 | type CompletionResponse struct { 71 | ID string `json:"id"` 72 | Object string `json:"object"` 73 | Created int64 `json:"created"` 74 | Model string `json:"model"` 75 | Choices []CompletionChoice `json:"choices"` 76 | Usage Usage `json:"usage,omitempty"` 77 | } 78 | 79 | type CompletionChoice struct { 80 | Text string `json:"text"` 81 | Index int `json:"index"` 82 | Logprobs *Logprobs `json:"logprobs,omitempty"` 83 | FinishReason string `json:"finish_reason,omitempty"` 84 | } 85 | 86 | type Logprobs struct { 87 | Tokens []string `json:"tokens"` 88 | TokenLogprobs []float64 `json:"token_logprobs"` 89 | TopLogprobs []map[string]float64 `json:"top_logprobs"` 90 | TextOffset []int `json:"text_offset"` 91 | } 92 | 93 | // Chat endpoint structs 94 | // https://platform.openai.com/docs/api-reference/chat 95 | 96 | type ChatCompletionRequest struct { 97 | Model string `json:"model"` 98 | Messages []ChatMessage `json:"messages"` 99 | Temperature float64 `json:"temperature,omitempty"` 100 | TopP float64 `json:"top_p,omitempty"` 101 | N int `json:"n,omitempty"` 102 | Stream bool `json:"stream,omitempty"` 103 | Stop interface{} `json:"stop,omitempty"` 104 | MaxTokens int `json:"max_tokens,omitempty"` 105 | PresencePenalty float64 `json:"presence_penalty,omitempty"` 106 | FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` 107 | LogitBias map[string]int `json:"logit_bias,omitempty"` 108 | User string `json:"user,omitempty"` 109 | } 110 | 111 | type ChatMessage struct { 112 | Role string `json:"role"` 113 | Content string `json:"content"` 114 | Name string `json:"name,omitempty"` 115 | FunctionCall *ChatFunctionCall `json:"function_call,omitempty"` 116 | } 117 | 118 | type ChatFunctionCall struct { 119 | Name string `json:"name"` 120 | Arguments string `json:"arguments"` 121 | } 122 | 123 | type ChatCompletionResponse struct { 124 | ID string `json:"id"` 125 | Object string `json:"object"` 126 | Created int64 `json:"created"` 127 | Model string `json:"model"` 128 | Choices []ChatChoice `json:"choices"` 129 | Usage Usage `json:"usage,omitempty"` 130 | } 131 | 132 | type ChatChoice struct { 133 | Index int `json:"index"` 134 | Message ChatMessage `json:"message"` 135 | FinishReason string `json:"finish_reason,omitempty"` 136 | } 137 | 138 | // Edit endpoint structs 139 | // https://platform.openai.com/docs/api-reference/edits 140 | 141 | type EditRequest struct { 142 | Model string `json:"model"` 143 | Input string `json:"input,omitempty"` 144 | Instruction string `json:"instruction"` 145 | N int `json:"n,omitempty"` 146 | Temperature float64 `json:"temperature,omitempty"` 147 | TopP float64 `json:"top_p,omitempty"` 148 | } 149 | 150 | type EditResponse struct { 151 | ID string `json:"id"` 152 | Object string `json:"object"` 153 | Created int64 `json:"created"` 154 | Choices []EditChoice `json:"choices"` 155 | Usage Usage `json:"usage,omitempty"` 156 | } 157 | 158 | type EditChoice struct { 159 | Text string `json:"text"` 160 | Index int `json:"index"` 161 | } 162 | 163 | // Embedding endpoint structs 164 | // https://platform.openai.com/docs/api-reference/embeddings 165 | 166 | type EmbeddingRequest struct { 167 | Model string `json:"model"` 168 | Input []string `json:"input"` 169 | User string `json:"user,omitempty"` 170 | } 171 | 172 | type EmbeddingResponse struct { 173 | Object string `json:"object"` 174 | Data []EmbeddingData `json:"data"` 175 | Model string `json:"model"` 176 | Usage Usage `json:"usage,omitempty"` 177 | } 178 | 179 | type EmbeddingData struct { 180 | Index int `json:"index"` 181 | Embedding []float64 `json:"embedding"` 182 | } 183 | 184 | // Moderation endpoint structs 185 | // https://platform.openai.com/docs/api-reference/moderations 186 | 187 | type ModerationRequest struct { 188 | Input interface{} `json:"input"` // string or []string 189 | Model string `json:"model,omitempty"` 190 | } 191 | 192 | type ModerationResponse struct { 193 | ID string `json:"id"` 194 | Model string `json:"model"` 195 | Results []ModerationResult `json:"results"` 196 | } 197 | 198 | type ModerationResult struct { 199 | Categories map[string]bool `json:"categories"` 200 | CategoryScores map[string]float64 `json:"category_scores"` 201 | Flagged bool `json:"flagged"` 202 | } 203 | 204 | // Image endpoint structs 205 | // https://platform.openai.com/docs/api-reference/images 206 | 207 | type ImageCreateRequest struct { 208 | Prompt string `json:"prompt"` 209 | N int `json:"n,omitempty"` 210 | Size string `json:"size,omitempty"` 211 | ResponseFormat string `json:"response_format,omitempty"` 212 | User string `json:"user,omitempty"` 213 | } 214 | 215 | type ImageData struct { 216 | URL string `json:"url,omitempty"` 217 | B64JSON string `json:"b64_json,omitempty"` 218 | } 219 | 220 | type ImageCreateResponse struct { 221 | Created int64 `json:"created"` 222 | Data []ImageData `json:"data"` 223 | } 224 | 225 | type ImageEditRequest struct { 226 | Image string `json:"image"` 227 | Mask string `json:"mask,omitempty"` 228 | Prompt string `json:"prompt"` 229 | N int `json:"n,omitempty"` 230 | Size string `json:"size,omitempty"` 231 | ResponseFormat string `json:"response_format,omitempty"` 232 | User string `json:"user,omitempty"` 233 | } 234 | 235 | type ImageEditResponse struct { 236 | Created int64 `json:"created"` 237 | Data []ImageData `json:"data"` 238 | } 239 | 240 | type ImageVariationRequest struct { 241 | Image string `json:"image"` 242 | N int `json:"n,omitempty"` 243 | Size string `json:"size,omitempty"` 244 | ResponseFormat string `json:"response_format,omitempty"` 245 | User string `json:"user,omitempty"` 246 | } 247 | 248 | type ImageVariationResponse struct { 249 | Created int64 `json:"created"` 250 | Data []ImageData `json:"data"` 251 | } 252 | 253 | // File endpoint structs 254 | // https://platform.openai.com/docs/api-reference/files 255 | 256 | type File struct { 257 | ID string `json:"id"` 258 | Object string `json:"object"` 259 | Bytes int `json:"bytes"` 260 | CreatedAt int64 `json:"created_at"` 261 | Filename string `json:"filename"` 262 | Purpose string `json:"purpose"` 263 | } 264 | 265 | type FileListResponse struct { 266 | Object string `json:"object"` 267 | Data []File `json:"data"` 268 | } 269 | 270 | // Fine-tune endpoint structs 271 | // https://platform.openai.com/docs/api-reference/fine-tunes 272 | 273 | type FineTuneCreateRequest struct { 274 | TrainingFile string `json:"training_file"` 275 | ValidationFile string `json:"validation_file,omitempty"` 276 | Model string `json:"model,omitempty"` 277 | NEpochs int `json:"n_epochs,omitempty"` 278 | BatchSize int `json:"batch_size,omitempty"` 279 | LearningRateMultiplier float64 `json:"learning_rate_multiplier,omitempty"` 280 | PromptLossWeight float64 `json:"prompt_loss_weight,omitempty"` 281 | ComputeClassificationMetrics bool `json:"compute_classification_metrics,omitempty"` 282 | ClassificationNClasses int `json:"classification_n_classes,omitempty"` 283 | ClassificationPositiveClass string `json:"classification_positive_class,omitempty"` 284 | ClassificationBetas []float64 `json:"classification_betas,omitempty"` 285 | Suffix string `json:"suffix,omitempty"` 286 | } 287 | 288 | type FineTune struct { 289 | ID string `json:"id"` 290 | Object string `json:"object"` 291 | Model string `json:"model"` 292 | CreatedAt int64 `json:"created_at"` 293 | FineTunedModel string `json:"fine_tuned_model"` 294 | OrganizationID string `json:"organization_id"` 295 | Status string `json:"status"` 296 | Hyperparams FineTuneHyperparams `json:"hyperparams"` 297 | TrainingFiles []File `json:"training_files"` 298 | ValidationFiles []File `json:"validation_files"` 299 | ResultFiles []File `json:"result_files"` 300 | Events []FineTuneEvent `json:"events"` 301 | } 302 | 303 | type FineTuneHyperparams struct { 304 | NEpochs int `json:"n_epochs"` 305 | BatchSize int `json:"batch_size"` 306 | LearningRateMultiplier float64 `json:"learning_rate_multiplier"` 307 | } 308 | 309 | type FineTuneEvent struct { 310 | Object string `json:"object"` 311 | CreatedAt int64 `json:"created_at"` 312 | Level string `json:"level"` 313 | Message string `json:"message"` 314 | } 315 | 316 | type FineTuneListResponse struct { 317 | Object string `json:"object"` 318 | Data []FineTune `json:"data"` 319 | } 320 | -------------------------------------------------------------------------------- /pkg/llm/openai/openai_client.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "github.com/compiai/engine/pkg/llm" 10 | "io" 11 | "log/slog" 12 | "net/http" 13 | "strings" 14 | ) 15 | 16 | type Config struct { 17 | ApiKey string `yaml:"apiKey"` 18 | Endpoint string `yaml:"endpoint"` 19 | Model string `yaml:"model"` 20 | } 21 | 22 | // Client implements the llm.Streamer interface using OpenAI's Chat Completions API. 23 | type Client struct { 24 | logger *slog.Logger 25 | config Config 26 | httpClient *http.Client 27 | } 28 | 29 | // NewClient creates a new OpenAI client. Uses gpt-3.5-turbo by default. 30 | func NewClient(logger *slog.Logger, config Config) *Client { 31 | return &Client{ 32 | logger: logger.WithGroup("openai-client"), 33 | config: config, 34 | httpClient: http.DefaultClient, 35 | } 36 | } 37 | 38 | // Stream sends a streaming generation request and returns a channel of incremental responses. 39 | func (c *Client) Stream(ctx context.Context, request llm.GenerateRequest) (<-chan llm.GenerateStreamResponse, error) { 40 | // Internal types for OpenAI API payload 41 | type chatMessage struct { 42 | Role string `json:"role"` 43 | Content string `json:"content"` 44 | } 45 | type streamRequest struct { 46 | Model string `json:"model"` 47 | Messages []chatMessage `json:"messages"` 48 | Stream bool `json:"stream"` 49 | } 50 | 51 | // Build the messages sequence: system, history, then new user prompt 52 | msgs := []chatMessage{{Role: "system", Content: request.Prompt.System}} 53 | for _, conv := range request.History { 54 | msgs = append(msgs, chatMessage{Role: "user", Content: conv.Request}) 55 | msgs = append(msgs, chatMessage{Role: "assistant", Content: conv.Response}) 56 | } 57 | msgs = append(msgs, chatMessage{Role: "user", Content: request.Prompt.User}) 58 | 59 | reqBody := streamRequest{Model: c.config.Model, Messages: msgs, Stream: true} 60 | data, err := json.Marshal(reqBody) 61 | if err != nil { 62 | return nil, fmt.Errorf("failed to marshal request: %w", err) 63 | } 64 | 65 | completionsEndpoint := fmt.Sprintf("%s/chat/completions", c.config.Endpoint) 66 | 67 | httpReq, err := http.NewRequestWithContext(ctx, "POST", completionsEndpoint, bytes.NewReader(data)) 68 | if err != nil { 69 | return nil, fmt.Errorf("failed to create HTTP request: %w", err) 70 | } 71 | httpReq.Header.Set("Authorization", "Bearer "+c.config.ApiKey) 72 | httpReq.Header.Set("Content-Type", "application/json") 73 | 74 | resp, err := c.httpClient.Do(httpReq) 75 | if err != nil { 76 | return nil, fmt.Errorf("stream request error: %w", err) 77 | } 78 | if resp.StatusCode != http.StatusOK { 79 | defer resp.Body.Close() 80 | errBody, _ := io.ReadAll(resp.Body) 81 | return nil, fmt.Errorf("stream request failed: %s", string(errBody)) 82 | } 83 | 84 | ch := make(chan llm.GenerateStreamResponse) 85 | // Start goroutine to read the SSE stream 86 | go func() { 87 | defer close(ch) 88 | reader := bufio.NewReader(resp.Body) 89 | for { 90 | line, err := reader.ReadString('\n') 91 | if err != nil { 92 | if err != io.EOF { 93 | ch <- llm.GenerateStreamResponse{Error: err} 94 | } 95 | return 96 | } 97 | line = strings.TrimSpace(line) 98 | if line == "" || !strings.HasPrefix(line, "data:") { 99 | continue 100 | } 101 | payload := strings.TrimPrefix(line, "data: ") 102 | if payload == "[DONE]" { 103 | return 104 | } 105 | // Parse the streamed JSON chunk 106 | var event struct { 107 | ID string `json:"id"` 108 | Choices []struct { 109 | Delta struct { 110 | Content string `json:"content"` 111 | } `json:"delta"` 112 | } `json:"choices"` 113 | } 114 | if err := json.Unmarshal([]byte(payload), &event); err != nil { 115 | ch <- llm.GenerateStreamResponse{Error: fmt.Errorf("invalid stream response: %w", err)} 116 | return 117 | } 118 | 119 | // Emit each non-empty content delta 120 | for _, choice := range event.Choices { 121 | if choice.Delta.Content != "" { 122 | ch <- llm.GenerateStreamResponse{ID: event.ID, Response: choice.Delta.Content} 123 | } 124 | } 125 | } 126 | }() 127 | 128 | return ch, nil 129 | } 130 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | **COMPI AI** 2 | *Competitive Gaming Performance Analysis Platform* 3 | 4 | --- 5 | 6 | ## Socials 7 | 8 | - [Website](https://compiai.xyz) 9 | - [Twitter](https://x.com/CompiAI) 10 | 11 | ## Table of Contents 12 | 13 | 1. [Overview](#overview) 14 | 2. [Features](#features) 15 | 3. [Architecture](#architecture) 16 | 4. [Getting Started](#getting-started) 17 | 18 | * [Prerequisites](#prerequisites) 19 | * [Installation](#installation) 20 | * [Configuration](#configuration) 21 | * [Running the Server](#running-the-server) 22 | 5. [Usage](#usage) 23 | 24 | * [API Endpoints](#api-endpoints) 25 | * [Example Request](#example-request) 26 | 6. [Development](#development) 27 | 28 | * [Project Layout](#project-layout) 29 | * [Testing](#testing) 30 | 7. [Roadmap](#roadmap) 31 | 8. [Future Plans](#future-plans) 32 | 9. [Contributing](#contributing) 33 | 10. [License](#license) 34 | 35 | --- 36 | 37 | ## Overview 38 | 39 | **compiai-engine** is a Go-powered backend service that ingests player performance data, computes advanced analytics, and generates in-depth coaching feedback via large language models (OpenAI or Anthropic Claude). It exposes a Server-Sent Events (SSE) API for real-time streaming of step-by-step analysis, enabling integration into dashboards, coaching tools, or direct in-game overlays. 40 | 41 | --- 42 | 43 | ## Features 44 | 45 | * **Advanced Metrics Derivation** 46 | Calculates K/D ratio, variance-based consistency, and role-synergy clustering. 47 | * **LLM-Powered Narrative** 48 | Wraps data in tailored system/user prompts and streams expert analysis via Chat Completions. 49 | * **SSE Streaming API** 50 | Delivers incremental coaching advice in real time. 51 | * **Pluggable LLM Clients** 52 | Swap easily between OpenAI and Claude via a common `llm.Streamer` interface. 53 | * **PostgreSQL Storage** 54 | Secure, scalable user profile persistence with JSON/array support. 55 | * **Go Chi Router** 56 | Lightweight, idiomatic HTTP routing and middleware support. 57 | * **Config-Driven** 58 | All endpoints, timeouts, and LLM parameters defined via YAML for Kubernetes or local runs. 59 | 60 | --- 61 | 62 | ## Architecture 63 | 64 | ``` 65 | +-------------+ +---------------+ +-------------+ 66 | | HTTP Client| <---> | HTTP Server | <---> | Chi Router| 67 | +-------------+ +---------------+ +-------------+ 68 | | 69 | v 70 | +-----------------------+ 71 | | stat_analyzer | 72 | | (Agent + Prompts) | 73 | +-----------------------+ 74 | | | | 75 | v v v 76 | +--------+ +--------+ +----------------+ 77 | | User | | LLM | | AdvancedMetrics| 78 | | Service| | Stream | | Derivation | 79 | +--------+ +--------+ +----------------+ 80 | | | 81 | v | 82 | +-------------+ | 83 | | PostgreSQL |<---------------+ 84 | | Storage | 85 | +-------------+ 86 | ``` 87 | 88 | --- 89 | 90 | ## Getting Started 91 | 92 | ### Prerequisites 93 | 94 | * Go 1.20+ 95 | * PostgreSQL 12+ 96 | * Kubernetes (optional for production) 97 | 98 | ### Installation 99 | 100 | ```bash 101 | git clone https://github.com/compiai/engine.git 102 | cd engine 103 | go mod download 104 | ``` 105 | 106 | ### Configuration 107 | 108 | Copy the sample configuration and fill in real values: 109 | 110 | ```yaml 111 | # config.yaml 112 | application: 113 | version: 0.1.0-snapshot 114 | database: 115 | postgres: 116 | addr: db.example.com:5432 117 | auth: 118 | username: youruser 119 | password: yourpass 120 | tlsEnabled: false 121 | clients: 122 | openai: 123 | apiKey: YOUR_OPENAI_KEY 124 | endpoint: https://api.openai.com/v1 125 | model: gpt-4 126 | temperature: 0.7 127 | maxTokensToSample: 500 128 | claude: 129 | apiKey: YOUR_CLAUDE_KEY 130 | endpoint: https://api.anthropic.com/v1/complete 131 | model: claude-v1 132 | temperature: 1.0 133 | maxTokensToSample: 300 134 | server: 135 | public: 136 | addr: :8080 137 | timeout: 5s 138 | auth: 139 | privateKey: file://path/to/private.key 140 | publicKey: file://path/to/public.key 141 | ``` 142 | 143 | ### Running the Server 144 | 145 | ```bash 146 | go run main.go -config ./config.yaml 147 | ``` 148 | 149 | --- 150 | 151 | ## Usage 152 | 153 | ### API Endpoints 154 | 155 | * **POST** `/analysis/` 156 | 157 | * **Request Body**: 158 | 159 | ```json 160 | { "userId": "00000000-0000-0000-0000-000000000000" } 161 | ``` 162 | * **Response**: Server-Sent Events (`event: analysis`) streaming chunks of analysis JSON. 163 | 164 | ### Example Request 165 | 166 | ```bash 167 | curl -N -X POST http://localhost:8080/analysis/ \ 168 | -H "Content-Type: application/json" \ 169 | -d '{"userId":"123e4567-e89b-12d3-a456-426614174000"}' 170 | ``` 171 | 172 | --- 173 | 174 | ## Development 175 | 176 | ### Project Layout 177 | 178 | ``` 179 | . 180 | ├── cmd/ # main.go entrypoint 181 | ├── internal/ 182 | │ ├── core/ 183 | │ │ ├── ext/user/ # Postgres storage + user service 184 | │ │ └── domain/ 185 | │ │ └── agent/stat_analyzer/ 186 | │ │ ├── prompt/ # text/template files 187 | │ │ ├── agent.go 188 | │ │ └── http/ # Chi HTTP handlers 189 | ├── pkg/ 190 | │ └── llm/ # Streamer interface 191 | │ └── openai/ # OpenAI/Claude client implementations 192 | └── config.yaml 193 | ``` 194 | 195 | ### Testing 196 | 197 | ```bash 198 | go test ./... 199 | ``` 200 | 201 | --- 202 | 203 | ## Roadmap 204 | 205 | * **v0.2.0** 206 | 207 | * Add support for batch analysis of multiple users concurrently. 208 | * Implement real‐time WebSocket fallback. 209 | * Integrate optional GPU-accelerated metric pipelines. 210 | 211 | * **v0.3.0** 212 | 213 | * Dashboard UI with live graphs and session recordings. 214 | * Role‐based access control (coach vs. player). 215 | * Plugins for popular game titles (Dota 2, Valorant, League of Legends). 216 | 217 | --- 218 | 219 | ## Future Plans 220 | 221 | * **Adaptive Learning** 222 | Leverage user progress data to auto-tune prompt recommendations and drill difficulty over time. 223 | * **Multi-Modal Inputs** 224 | Incorporate gameplay video analysis (computer vision) alongside statistics. 225 | * **Cross-Platform SDKs** 226 | Provide JavaScript and Python SDKs for easy integration into third-party tools. 227 | * **Enterprise Features** 228 | SSO integration, analytics dashboards, and exportable PDF coaching reports. 229 | 230 | --- 231 | 232 | ## Contributing 233 | 234 | We welcome contributions! Please: 235 | 236 | 1. Fork the repository. 237 | 2. Create a feature branch (`git checkout -b feature/XYZ`). 238 | 3. Commit your changes (`git commit -m "Add feature XYZ"`). 239 | 4. Push to your fork (`git push origin feature/XYZ`). 240 | 5. Open a pull request and describe your changes. 241 | 242 | Please follow the existing code style and include tests for new functionality. 243 | 244 | --- 245 | 246 | ## License 247 | 248 | [MIT License](LICENSE) 249 | Feel free to use, modify, and distribute under the terms of the MIT license. 250 | --------------------------------------------------------------------------------