A Go-based Retrieval-Augmented Generation (RAG) backend that turns a resume / project history into an AI agent. Recruiters chat with the candidate's representative; the agent grounds every answer in indexed project chunks and a profile, never inventing experience.
LLM model:
google/gemini-2.5-flash(via OpenRouter) Embedding model:nomic-embed-text(via Ollama, 768-dim)
- Overview
- What Problem It Solves
- High-Level Architecture
- Technology Stack
- Project Structure
- Configuration
- The
agentkitPackage - The
configPackage - The
loggerPackage - The
cmd/serverPackage (HTTP API) - The
ingestPackage (Indexing Pipeline) - Data Files
- Request Lifecycle
- Agent Tool Loop
- API Reference
- Deployment
- Testing
- Security Considerations
- Extending the Project
- Glossary
Ragsume Core is a self-hosted backend service written in Go. It exposes a small
HTTP API built on the chi router. Behind the API sits an agent that uses
OpenRouter (an OpenAI-compatible LLM gateway) and a Qdrant vector store
to answer questions about a candidate's resume.
The system is a concrete implementation of the RAG pattern:
- Project descriptions are parsed from YAML, split into chunks, embedded with a local Ollama model, and stored in Qdrant.
- At query time the agent decides whether to call a tool (
search_profileormatch_job_description), retrieves relevant chunks, and composes a grounded answer streamed back to the client over Server-Sent Events (SSE).
The agent is constrained by a system prompt that forces it to speak as the candidate, cite its sources, and refuse to fabricate experience.
A traditional resume is static. Ragsume Core makes it conversational:
- A recruiter can ask "Tell me about your Go backend work" and receive an answer drawn from real indexed projects, with citations.
- A recruiter can paste a job description and get a tailored pitch matching the candidate's most relevant projects.
- The candidate controls the source of truth (YAML files + profile) and the agent cannot hallucinate beyond that material.
flowchart TB
Client["Client<br/>(browser / curl / ragsume-ui frontend)"]
Server["cmd/server (chi router)<br/>middleware: RequestID, RealIP, Recoverer,<br/>CORS, RequestLogger<br/>routes: GET /health, POST /chat"]
Agent["agentkit.Agent<br/>tool-calling loop (max 5 iterations)<br/>Run() → non-streaming<br/>RunStream() → tool rounds then SSE stream"]
OpenRouter["OpenRouterClient (ChatClient)<br/>Complete / Stream"]
ToolExec["ToolExecutor<br/>search_profile<br/>match_job_description"]
Embedder["OllamaEmbedder (Embedder)"]
Qdrant["QdrantClient (VectorStore)"]
Client -->|"HTTP POST /chat (SSE)"| Server
Server --> Agent
Agent --> OpenRouter
Agent --> ToolExec
ToolExec --> Embedder
ToolExec --> Qdrant
External dependencies:
| Service | Role | Default URL |
|---|---|---|
| Qdrant | Vector store (REST, port 6333) | http://localhost:6333 |
| Ollama | Local embedding model | http://localhost:11434 |
| OpenRouter | LLM chat completions gateway | https://openrouter.ai/... |
| Redis | Rate limiter backing store | redis://localhost:6379/0 |
| Component | Technology |
|---|---|
| Language | Go 1.25 (go.mod) |
| HTTP router | github.com/go-chi/chi/v5 |
| Env loading | github.com/joho/godotenv |
| YAML parsing | gopkg.in/yaml.v3 |
| UUID generation | github.com/google/uuid |
| Logging | Go stdlib log/slog (structured) |
| Vector store | Qdrant via REST API |
| Embeddings | Ollama (nomic-embed-text, 768-dim) |
| LLM | OpenRouter (google/gemini-2.5-flash default) |
| Rate limiter | Redis (github.com/redis/go-redis/v9) |
| Testing | Go testing package |
| Containerisation | Docker multi-stage build (Debian slim runtime) |
Note: the README mentions Viper/Zap/testify, but the actual implementation uses
godotenv+ manual env parsing, stdliblog/slog, and plaintesting. The stack table above reflects the real code.
The project is organised as a standard Go monorepo. Below is a breakdown of every top-level directory and its contents.
| Directory / File | Purpose |
|---|---|
agentkit/ |
Core agent library — tool-calling loop, LLM client (OpenRouter), vector store client (Qdrant), embedding, and type definitions |
cmd/server/ |
HTTP API entry point — chi router, middleware (CORS, logger, rate limiter), handlers (/health, /chat) |
config/ |
Environment-driven configuration — loads .env, parses typed values, provides defaults |
data/ |
Static data files — profile.yaml (candidate profile), projects/*.yaml (project descriptions) |
ingest/ |
Indexing pipeline — reads YAML projects, chunks text, generates embeddings, stores in Qdrant |
logger/ |
Structured logging wrapper around log/slog — supports file output, JSON/text format, debug levels |
go.mod |
Go module definition — declares module path ragsume-core and all dependencies |
go.sum |
Dependency checksum file — auto-generated, ensures reproducible builds |
Dockerfile |
Multi-stage Docker build — compiles binary, runs in Debian slim image |
.env.example |
Example environment variables — documents every required config key |
.gitignore |
Git ignore rules — excludes logs/, .env, tmp/, binary artifacts |
LICENSE |
Project license file |
README.md |
This file — full project documentation |
Configuration is environment-driven. config.Load()
optionally reads a .env file (via godotenv) and then reads typed values from
os.Getenv.
| Variable | Required | Type | Description |
|---|---|---|---|
APP_NAME |
yes | string | Application name attached to every log record |
PORT |
yes | int | HTTP server port |
DEBUG |
yes | bool | Enables source location in logs; sets log level=debug |
RATE |
yes | float | Generic rate value (currently unused beyond config) |
REDIS_URL |
yes | string | Redis URL for rate limiter backing store |
RATE_LIMIT_MAX |
yes | int | Max requests per IP per window |
RATE_LIMIT_WINDOW |
yes | int | Rate limit window in seconds |
LOG_LEVEL |
no | string | debug/info/warn/error (default info) |
LOG_FORMAT |
no | string | text or json (default text) |
LOG_FILE |
no | string | File path for logs; empty = stderr |
QDRANT_URL |
yes | string | Qdrant REST URL (default port 6333 inferred) |
QDRANT_API_KEY |
no | string | Optional Qdrant API key |
OPENROUTER_API_KEY |
yes | string | OpenRouter API key |
OLLAMA_URL |
yes | string | Ollama server URL (for embeddings) |
ALLOWED_ORIGIN |
yes | string | Single CORS origin allowed for the frontend |
REDIS_URL |
yes | string | Redis URL for rate limiter backing store |
RATE_LIMIT_MAX |
yes | int | Max requests per IP per window |
RATE_LIMIT_WINDOW |
yes | int | Rate limit window in seconds |
6.2 Defaults (config/defaults.go)
DefaultCollectionName = "projects"
DefaultEmbedModel = "nomic-embed-text"
DefaultVectorSize = 768
DefaultLLMModel = "google/gemini-2.5-flash"godotenv.Load()reads.envif present (host env vars still win).- Typed helpers in
config/env.goparse and validate each variable, returning descriptive errors on missing/invalid values. - The populated
config.Configis stored in the globalconfig.C.
See .env.example:
APP_NAME=ragsume-core
PORT=8080
DEBUG=true
RATE=1.5
LOG_LEVEL=debug
LOG_FORMAT=text
LOG_FILE=logs/app.log
QDRANT_URL=http://localhost:6333
QDRANT_API_KEY=
OPENROUTER_API_KEY=sk-or-v1-your-key-here
OLLAMA_URL=http://localhost:11434
ALLOWED_ORIGIN=http://localhost:3000
REDIS_URL=redis://localhost:6379/0
RATE_LIMIT_MAX=60
RATE_LIMIT_WINDOW=60agentkit is the heart of the system. It is package-documented in
agentkit/doc.go as providing "generic RAG and tool-calling
infrastructure."
7.1 Core Types (agentkit/types.go)
Message— OpenAI-compatible chat message (role,content,tool_calls,tool_call_id,name).ToolCall/FunctionCall— model-requested tool invocations.ToolDefinition/FunctionDefinition— schemas exposed to the model.Chunk— a retrieved project chunk with citation metadata (project_name,section,chunk_text,category,date_range,tech_stack,score).PointInput— a vector point to upsert.Filter/Condition— Qdrant-stylemustfilters.Filter.ToQdrantFilter()builds the REST JSON, returningnilwhen empty so the field is omitted.ParseQdrantURL()— normalizes a Qdrant URL, defaulting the port to 6333 (or 443 for https).
The package is built around three small interfaces, making it fully testable with mocks:
// agentkit/embed.go
type Embedder interface {
Embed(ctx context.Context, text string) ([]float32, error)
}
// agentkit/openrouter.go
type ChatClient interface {
Complete(ctx context.Context, req ChatCompletionRequest) (Message, error)
CompleteStream(ctx context.Context, req ChatCompletionRequest,
onToken func(string) error) (Message, error)
}
// agentkit/qdrant.go
type VectorStore interface {
EnsureCollection(ctx context.Context, name string, vectorSize uint64) error
EnsurePayloadIndexes(ctx context.Context, collection string, fields []string) error
Upsert(ctx context.Context, collection string, points []PointInput) error
Scroll(ctx context.Context, collection string, filter *Filter, limit uint64) ([]Chunk, error)
Query(ctx context.Context, collection string, vector []float32, filter *Filter, limit uint64) ([]Chunk, error)
Close() error
}7.3 OpenRouter Client (agentkit/openrouter.go)
OpenRouterClient implements ChatClient against
https://openrouter.ai/api/v1/chat/completions.
NewOpenRouterClient()defaults the model togoogle/gemini-2.5-flashand sets a 120s HTTP timeout.Complete()— non-streaming completion; returns the first choice's message (which may containtool_calls).CompleteStream()— SSE streaming; parsesdata:lines, invokesonTokenfor each delta, and returns the assembled assistant message.- Headers set:
Content-Type,Authorization: Bearer <key>, andHTTP-Referer(OpenRouter attribution).
7.4 Ollama Embedder (agentkit/embed.go)
OllamaEmbedder calls Ollama's /api/embeddings
endpoint. Defaults to model nomic-embed-text (768 dimensions). Returns an
error if the embedding is empty.
7.5 Qdrant Client (agentkit/qdrant.go)
QdrantClient implements VectorStore over Qdrant's
REST API (port 6333). Key methods:
| Method | REST endpoint | Purpose |
|---|---|---|
EnsureCollection |
GET/PUT /collections/{name} |
Create collection w/ Cosine dist |
EnsurePayloadIndexes |
PUT /collections/{name}/index |
Create keyword indexes on fields |
Upsert |
PUT /collections/{name}/points |
Insert/update points (wait=true) |
Scroll |
POST /collections/{name}/points/scroll |
Metadata-only listing via filter |
Query |
POST /collections/{name}/points/query |
Nearest-neighbor search |
All requests are logged via logger.Component("qdrant") with method, path,
status, elapsed ms, and a truncated body (max 4096 chars). Non-2xx responses
become errors with a body snippet. isNotFound()
detects 404s so EnsureCollection can create missing collections.
7.6 Tool Executor (agentkit/tools.go)
ToolExecutor holds a VectorStore, Embedder,
ChatClient, and collection name. Execute()
dispatches by tool name.
Searches project history chunks. Arguments:
{ "query": "semantic query", "filter": { "must": [{ "field": "tech_stack", "match": "go" }] } }Behavior (searchProfile()):
- Empty query + filter →
Store.Scroll(metadata-only listing). - Non-empty query → embed the query, then
Store.Query(semantic search), optionally combined with a filter. - Requires at least a query or a filter.
- Returns JSON
{ "chunks": [...] }.
Matches a job description to relevant projects and generates a pitch.
Arguments: { "jd_text": "..." }.
Behavior (matchJobDescription()):
- Embed the job description.
Store.Query(limit 8, no filter).aggregateMatches()groups chunks by project, collecting sections and keeping the max relevance score; results sorted by relevance descending.generatePitch()asks the LLM to write a concise 2–3 sentence recruiter pitch grounded only in the matched project names.- Returns JSON
{ "matches": [...], "pitch": "..." }.
defaultTools exposes both tools to the model with
JSON Schema parameters. DefaultTools() returns a
copy.
7.7 Agent (agentkit/agent.go)
Agent runs the tool-calling loop. Fields: LLM,
Tools, SystemPrompt, MaxIter (default 5), ToolDefs.
NewAgent()wires defaults.baseMessages()prepends the system prompt.Run()— non-streaming loop:- Call
LLM.Completewith messages + tool definitions. - If no tool calls → append assistant message, return
RunResult. - Otherwise append the assistant message, execute each tool call, append
tool-role messages with results, and repeat. - Error if iterations exceed
MaxIter.
- Call
RunStream()— same loop, but once the model stops calling tools it makes a finalLLM.CompleteStreamcall, invokingonTokenfor each streamed token. This is what powers SSE responses.
config.Config— the settings struct, exposed globally asconfig.C.config.Load()— loads.env, validates all required variables, populatesC. Returns wrapped errors with the offending variable name.config/env.go— typed helpers:getString(required, trims whitespace)getInt/getFloat/getBool(required, parse with clear errors)getOptionalString(returns value + ok flag)
config/defaults.go— shared constants used by both the server and the ingest CLI.
Tests in config/config_test.go and
config/env_test.go cover valid loads, .env file
loading, and each failure mode (missing/invalid variables).
A thin, thread-safe wrapper around Go's stdlib log/slog
(logger/logger.go).
Format—FormatTextorFormatJSON.Options—AppName,Level,Format,Debug(adds source file/line),LogFile(file path; empty = stderr),Output(test override).Init()— builds the handler, attaches the app name attribute, and manages the log file handle lifecycle.Close()— closes the log file if open.- Package-level helpers:
Debug,Info,Warn,Error,Fatal(exits 1), plus*Contextvariants,With(), andComponent()for scoped child loggers. parseLevel()maps strings toslog.Level(unknown → info).
Tests in logger/logger_test.go verify text/JSON
output, level filtering, component scoping, file logging, and level parsing.
10.1 Entrypoint (cmd/server/main.go)
main() bootstraps the service:
config.Load()— load configuration.logger.Init()— initialize structured logging.LoadProfile("data/profile.yaml")— load the candidate profile and render it to YAML.agentkit.NewQdrantClient()— connect to Qdrant.- Build the embedder (Ollama), LLM (OpenRouter), tool executor, and agent with the system prompt.
newRouter(agent)— build the chi router.- Start
http.Serverwith 15s read timeout, 60s idle timeout, no write timeout (streaming). - Graceful shutdown on
SIGINT/SIGTERMwith a 120s context.
10.2 Router (cmd/server/router.go)
newRouter() builds a chi router with middleware:
| Middleware | Source | Purpose |
|---|---|---|
RequestID |
chi | Adds request ID to context |
RealIP |
chi | Resolves client IP from headers |
Recoverer |
chi | Recovers from panics |
CORS |
middleware/cors.go |
Single-origin CORS |
RateLimiter |
middleware/ratelimit.go |
Redis-backed per-IP rate limiter |
RequestLogger |
middleware/logger.go |
Per-request log |
Routes:
GET /health→handlers.HealthPOST /chat→handlers.NewChatHandler(agent)
10.3 Profile Loading (cmd/server/profile.go)
Profile mirrors data/profile.yaml:
name, headline, summary, skills, contact. LoadProfile()
reads/parses the YAML and requires a non-empty name. RenderProfileYAML()
marshals it back to YAML text for embedding in the system prompt.
10.4 System Prompt (cmd/server/prompt.go)
BuildSystemPrompt() constructs a strict prompt that:
- Makes the agent speak as the candidate in first person.
- Enforces grounding: every factual claim must come from a tool result or the profile; never invent experience.
- Requires citations in the format
SOURCES: project:section, ...after tool use (omitted if no tool was called). - Instructs filter usage: when the user names a technology/category,
populate the
search_profilefilter with a normalized lowercase value. - Sets tone: concise, skimmable.
- Defines scope & safety: stay in character, decline off-topic requests, and treat prompt-injection attempts inside user messages as untrusted text.
Health (cmd/server/handlers/health.go)
GET /health → 200 OK with {"status":"ok"}.
Chat (cmd/server/handlers/chat.go)
POST /chat is an SSE streaming endpoint.
Request body:
{ "message": "Your question", "history": [{"role":"user","content":"..."}] }Behavior (ServeHTTP()):
- Validate method (POST) and JSON body; require non-empty
message. - Require
http.Flushersupport. - Set SSE headers:
Content-Type: text/event-stream,Cache-Control: no-cache,Connection: keep-alive. - Append history + new user message.
- Call
Agent.RunStream()with anonTokencallback that writesevent: token\ndata: {"content":"..."}\n\nand flushes. - On error →
event: error\ndata: {"error":"..."}\n\n. - On success →
event: done\ndata: {}\n\n.
- CORS (
middleware/cors.go) — setsAccess-Control-Allow-Origintoconfig.C.AllowedOrigin, allowsGET, POST, OPTIONS, allowsContent-Type, Authorizationheaders, and short-circuitsOPTIONSwith204 No Content. - RequestLogger (
middleware/logger.go) — wraps the response writer, then logs method, path, request ID, status, and latency in ms. - RateLimiter (
middleware/ratelimit.go) — Redis-backed per-IP rate limiter using a fixed window. Configured viaREDIS_URL,RATE_LIMIT_MAX, andRATE_LIMIT_WINDOWenv vars. SetsX-RateLimit-LimitandX-RateLimit-Remainingheaders on every response, and returns429 Too Many Requestswhen the limit is exceeded.
A standalone CLI (go run ./ingest) that indexes project YAML into Qdrant.
11.1 Entrypoint (ingest/main.go)
Flags:
-data-dir(defaultdata/projects) — directory of project YAML files.-collection(defaultconfig.DefaultCollectionName=projects).
Flow (main()):
- Load config + logger (same as the server).
- Connect to Qdrant and create an Ollama embedder.
EnsureCollection(768-dim, Cosine).EnsurePayloadIndexesontech_stackandcategory.- Glob
*.yamlin the data dir; fail if none found. - For each file: parse, chunk, embed each chunk, build
PointInputwith payload (project_name,category,tech_stack,date_range,chunk_text,section,tradeoffs), and upsert. - Log per-project chunk counts and a final summary.
11.2 Project Model & Chunking (ingest/project.go)
Project fields: project_name, category,
date_range, tech_stack, problem, decisions, tradeoffs, outcome.
parseProject()— YAML unmarshal; requiresproject_name.chunkProject()— splits into three sections (problem,decisions,outcome), skipping empty ones. (Note:tradeoffsis stored as payload metadata, not as a separate chunk.)normalizeTechStack()— lowercases and trims.pointID()— deterministic UUID v5 (uuid.NewSHA1overprojectName:section), so re-ingesting updates the same points rather than creating duplicates.
12.1 Profile (data/profile.yaml)
The candidate's summary-level identity, embedded verbatim into the system
prompt. Contains name, headline, summary, skills, licenses, and
contact (email, linkedin, phone).
12.2 Projects (data/projects/)
Each YAML file describes one project with the schema from §11.2. Indexed projects include:
gAuthCraft.yamlguardflux.yaml— a Node.js/NestJS validation & rate-limiting library.multipay.yamlragsume-core.yaml— this project itself.ragsume-ui.yaml
sequenceDiagram
participant Client as Client (browser/curl)
participant Server as cmd/server (chi router)
participant Agent as agentkit.Agent
participant LLM as OpenRouter (LLM)
participant Tools as ToolExecutor
participant Qdrant as Qdrant (VectorStore)
Client->>Server: POST /chat {"message":"Tell me about your Go backend work"}
Server->>Server: middleware: RequestID, RealIP, CORS, Logger
Server->>Agent: Agent.RunStream(system prompt + profile YAML)
Agent->>LLM: LLM.Complete(msgs, toolDefs)
LLM-->>Agent: tool_call: search_profile
Agent->>Tools: Execute search_profile(query="go backend work")
Tools->>Qdrant: Embed query via Ollama + Query with filter
Qdrant-->>Tools: matching chunks as JSON
Tools-->>Agent: tool result (chunks)
Agent->>LLM: LLM.Complete(msgs + tool result)
LLM-->>Agent: no more tool calls
Agent->>LLM: LLM.CompleteStream(onToken)
LLM-->>Agent: streamed tokens
Agent-->>Server: SSE event: token (flushed)
Agent-->>Server: SOURCES: ... citation line
Server-->>Client: event: token (SSE)
Server-->>Client: event: done
flowchart TD
Base["baseMessages:<br/>[system, ...history, user]"]
Loop["for i < MaxIter (5):<br/>LLM.Complete(msgs, toolDefs)"]
NoTools["no tool calls"]
HasTools["has tool calls"]
Exceeded["i >= MaxIter"]
Return["return result (Run)"]
Append["append assistant + tool results<br/>loop again"]
Error["error: exceeded"]
Stream["LLM.CompleteStream(onToken)"]
Result["return streamed result"]
Base --> Loop
Loop --> NoTools
Loop --> HasTools
Loop --> Exceeded
NoTools --> Return
HasTools --> Append
Append --> Loop
Exceeded --> Error
Return --> Stream
Stream --> Result
Returns service health.
Response 200 OK:
{ "status": "ok" }Streams an agent response over SSE.
Request body:
{
"message": "Tell me about your Go backend work",
"history": [
{ "role": "user", "content": "Hi" },
{ "role": "assistant", "content": "Hello!" }
]
}Response 200 OK (Content-Type: text/event-stream):
event: token
data: {"content":"I "}
event: token
data: {"content":"built "}
...
event: done
data: {}
On error:
event: error
data: {"error":"chat completion: ..."}
Errors:
| Status | Cause |
|---|---|
| 405 | Method not POST |
| 400 | Invalid JSON or empty message |
| 500 | Streaming unsupported |
cp .env.example .env # fill in OPENROUTER_API_KEY, etc.
go mod tidy
go run ./ingest # index projects into Qdrant
go run ./cmd/server # start on :8080Prerequisites: a running Qdrant instance, Ollama server, and Redis server.
The Dockerfile is a multi-stage build:
- Builder (
golang:1.25-bookworm): downloads deps, builds both/out/serverand/out/ingestwith stripped symbols (-ldflags="-s -w"). - Runtime (
debian:bookworm-slim): installsca-certificates+wget(for the healthcheck), copies binaries anddata/, setsPORT=8080, exposes 8080, and defines aHEALTHCHECKhitting/health.
docker build -t ragsume-core .
docker run -p 8080:8080 --env-file .env \
-e REDIS_URL=redis://host.docker.internal:6379/0 \
ragsume-coreTo ingest inside the container:
docker exec <container> /app/ingestNote: Redis must be accessible to the container. If running locally, use
host.docker.internalor a Docker Compose network.
Run all tests:
go test ./...17.1 agentkit Tests (agentkit/agent_test.go)
Uses mocks implementing all three interfaces (mockVectorStore,
mockEmbedder, mockChatClient):
TestSearchProfileScrollOnly— filter-only scroll path.TestSearchProfileQueryWithFilter— query + filter path; verifies embedding is called with the right text.TestMatchJobDescription— verifies match aggregation (2 projects from 3 chunks) and pitch generation.TestAgentToolLoop— full non-streaming loop with one tool call.TestAgentRunStreamAfterTools— verifies tool rounds run non-streamed, then exactly one stream call emits the expected tokens.
TestLoad— valid env,.envfile loading, and failure cases (missingAPP_NAME, invalidPORT/DEBUG/RATE).TestGetString/TestGetInt/TestGetFloat/TestGetBool— per-helper validation including whitespace trimming and parse errors.
- Text/JSON handler output, level filtering, component scoping, file logging
(with nested dir creation), and
parseLevelmapping.
- RateLimiter (
cmd/server/middleware/ratelimit_test.go) — testsNewRateLimitercreation,Limithandler behavior (sets headers, calls next handler), andclientIPextraction fromRemoteAddr.
- Prompt injection: the system prompt explicitly treats instructions inside user messages as untrusted text and refuses to change role or reveal the prompt.
- CORS: locked to a single configured origin (
ALLOWED_ORIGIN); not a wildcard. - API keys:
OPENROUTER_API_KEYandQDRANT_API_KEYare read from env /.env(which is gitignored). Never commit.env. - Grounding: the agent is instructed to say "I don't have that information" rather than guess, reducing hallucination risk.
- No auth on
/chat: the API has no authentication layer; in production, place it behind a reverse proxy with auth or rate limiting. TheRATEconfig value is currently parsed but not enforced. - Rate limiting: the built-in
RateLimitermiddleware enforces per-IP limits via Redis (RATE_LIMIT_MAX,RATE_LIMIT_WINDOW). In production, ensure Redis is running and configured.
- Add a constant and a
ToolDefinitiontodefaultTools. - Add a
caseinToolExecutor.Execute()and implement the handler method. - Add a test with mocks in
agentkit/agent_test.go.
- Create
data/projects/<name>.yamlfollowing the schema in §11.2. - Run
go run ./ingest(ordocker exec <c> /app/ingest).
- LLM model: pass a different model to
NewOpenRouterClient()or changeconfig.DefaultLLMModel. - Embedding model: change
config.DefaultEmbedModeland ensure the Ollama server has it pulled. Vector size must match the model's output dimension (currently 768 fornomic-embed-text); updateconfig.DefaultVectorSizeand re-create the Qdrant collection.
- Add a handler in
cmd/server/handlers/. - Register it in
newRouter().
| Term | Meaning |
|---|---|
| RAG | Retrieval-Augmented Generation — grounding LLM answers in |
| retrieved documents. | |
| Chunk | A section of a project (problem/decisions/outcome) stored as |
| a vector point with metadata. | |
| Embedding | A vector representation of text used for similarity search. |
| Vector store | A database optimized for nearest-neighbor search (here Qdrant). |
| Tool call | A request from the LLM to execute a named function with JSON |
| arguments. | |
| SSE | Server-Sent Events — one-way streaming over HTTP. |
| OpenRouter | A gateway to many LLM providers via an OpenAI-compatible API. |
| Ollama | A local server for running open-source models (used here for |
| embeddings). | |
| Scroll | Qdrant metadata-only listing via filter (no vector needed). |
| Query | Qdrant nearest-neighbor search against a query vector. |
| Redis | In-memory data store used as the rate limiter backing store. |
| Rate limiter | Middleware that limits requests per IP using a fixed window. |
This document reflects the codebase as of the latest commit. Generated from direct reading of every source file in the repository.