Compare commits

...

2 Commits

9 changed files with 203 additions and 7 deletions

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
# RAG Agent app. Build from repo root (clone git@git.lesha.spb.ru:alex/RagAgent.git then docker compose build).
FROM python:3.12-slim
WORKDIR /app
# Install git for optional in-image clone; app is usually COPY'd from build context
RUN apt-get update -qq && apt-get install -y --no-install-recommends git openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Copy repo (when built from cloned repo: docker compose build)
COPY pyproject.toml ./
COPY src ./src
COPY README.md ./
RUN pip install --no-cache-dir -e .
# Default: run CLI (override in compose or when running)
ENV RAG_DB_DSN=""
ENV RAG_REPO_PATH="/data"
ENTRYPOINT ["rag-agent"]
CMD ["ask", "--help"]

View File

@@ -6,15 +6,21 @@ and answers queries using retrieval + LLM generation. Commits are tied to
## Quick start ## Quick start
1. Configure environment variables: 1. (Optional) Run Postgres and the app via Docker (clone the repo first):
- `git clone git@git.lesha.spb.ru:alex/RagAgent.git && cd RagAgent`
- `docker compose up -d` — starts Postgres and the RAG app in one network `rag_net`; app connects to DB at host `postgres`.
- On first start (empty DB), scripts in `docker/postgres-init/` run automatically (extension + tables). To disable, comment out the init volume in `docker-compose.yml`.
- Default DSN inside the app: `postgresql://rag:rag_secret@postgres:5432/rag`. Override with `POSTGRES_*` and `RAG_REPO_PATH` (path to your knowledge-base repo, mounted into the app container).
- Run commands: `docker compose run --rm app index --story my-branch`, `docker compose run --rm app ask "Question?"`.
2. Configure environment variables:
- `RAG_REPO_PATH` — path to git repo with text files - `RAG_REPO_PATH` — path to git repo with text files
- `RAG_DB_DSN` — Postgres DSN (e.g. `postgresql://user:pass@localhost:5432/rag`) - `RAG_DB_DSN` — Postgres DSN (e.g. `postgresql://rag:rag_secret@localhost:5432/rag`)
- `RAG_EMBEDDINGS_DIM` — embedding vector dimension (e.g. `1536`) - `RAG_EMBEDDINGS_DIM` — embedding vector dimension (e.g. `1536`)
2. Create DB schema: 3. Create DB schema (only if not using Docker, or if init was disabled):
- `python scripts/create_db.py` (or `psql "$RAG_DB_DSN" -f scripts/schema.sql`) - `python scripts/create_db.py` (or `psql "$RAG_DB_DSN" -f scripts/schema.sql`)
3. Index files for a story (e.g. branch name as story slug): 4. Index files for a story (e.g. branch name as story slug):
- `rag-agent index --story my-branch --changed --base-ref HEAD~1 --head-ref HEAD` - `rag-agent index --story my-branch --changed --base-ref HEAD~1 --head-ref HEAD`
4. Ask a question (optionally scoped to a story): 5. Ask a question (optionally scoped to a story):
- `rag-agent ask "What is covered?"` - `rag-agent ask "What is covered?"`
- `rag-agent ask "What is covered?" --story my-branch` - `rag-agent ask "What is covered?" --story my-branch`
@@ -36,8 +42,11 @@ Story for the commit is taken from (in order): env `RAG_STORY`, file `.rag-story
Scripts: `scripts/create_db.py` (Python, uses `ensure_schema` and `RAG_*` env), `scripts/schema.sql` (raw SQL). Scripts: `scripts/create_db.py` (Python, uses `ensure_schema` and `RAG_*` env), `scripts/schema.sql` (raw SQL).
## Embeddings (GigaChat)
If `GIGACHAT_CREDENTIALS` is set (e.g. in `.env` for local runs), embeddings use GigaChat API; otherwise the stub client is used. Optional env: `GIGACHAT_EMBEDDINGS_MODEL` (default `Embeddings`), `GIGACHAT_VERIFY_SSL` (`true`/`false`). Ensure `RAG_EMBEDDINGS_DIM` matches the model output (see GigaChat docs).
## Notes ## Notes
- The default embedding/LLM clients are stubs. Replace them in - LLM client is still a stub; replace it in `src/rag_agent/agent/pipeline.py` for real answers.
`src/rag_agent/index/embeddings.py` and `src/rag_agent/agent/pipeline.py`.
- This project requires Postgres with the `pgvector` extension. - This project requires Postgres with the `pgvector` extension.

56
docker-compose.yml Normal file
View File

@@ -0,0 +1,56 @@
# Postgres with pgvector + RAG Agent app (from repo git@git.lesha.spb.ru:alex/RagAgent.git).
# Clone the repo, then: docker compose up -d
# App and DB share network "rag_net"; app uses RAG_DB_DSN with host=postgres.
# DB init: scripts in docker/postgres-init/ run on first start (empty volume); to disable, comment out the init volume.
services:
postgres:
image: pgvector/pgvector:pg16
container_name: rag-postgres
environment:
POSTGRES_USER: ${POSTGRES_USER:-rag}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rag_secret}
POSTGRES_DB: ${POSTGRES_DB:-rag}
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- rag_pgdata:/var/lib/postgresql/data
# Init scripts run once on first start (create extension, tables). Optional: comment out to skip.
- ./docker/postgres-init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rag} -d ${POSTGRES_DB:-rag}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- rag_net
app:
build:
context: .
dockerfile: Dockerfile
image: rag-agent:latest
container_name: rag-agent
restart: "no"
depends_on:
postgres:
condition: service_healthy
environment:
RAG_DB_DSN: "postgresql://${POSTGRES_USER:-rag}:${POSTGRES_PASSWORD:-rag_secret}@postgres:5432/${POSTGRES_DB:-rag}"
RAG_REPO_PATH: ${RAG_REPO_PATH:-/data}
RAG_EMBEDDINGS_DIM: ${RAG_EMBEDDINGS_DIM:-1536}
GIGACHAT_CREDENTIALS: ${GIGACHAT_CREDENTIALS:-}
GIGACHAT_EMBEDDINGS_MODEL: ${GIGACHAT_EMBEDDINGS_MODEL:-Embeddings}
volumes:
- ${RAG_REPO_PATH:-./data}:/data:ro
entrypoint: ["rag-agent"]
command: ["ask", "--help"]
networks:
- rag_net
networks:
rag_net:
driver: bridge
volumes:
rag_pgdata:

View File

@@ -0,0 +1,7 @@
-- Example: create an extra DB user (e.g. read-only). Not executed — rename to 00-create-extra-user.sql to enable.
-- Scripts in this folder run in alphabetical order; 00-* runs before 01-schema.sql.
-- CREATE USER rag_readonly WITH PASSWORD 'change_me';
-- GRANT CONNECT ON DATABASE rag TO rag_readonly;
-- GRANT USAGE ON SCHEMA public TO rag_readonly;
-- GRANT SELECT ON ALL TABLES IN SCHEMA public TO rag_readonly;

View File

@@ -0,0 +1,32 @@
-- RAG vector DB schema (runs automatically on first Postgres init).
-- If RAG_EMBEDDINGS_DIM is not 1536, change vector(1536) below.
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS stories (
id SERIAL PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc')
);
CREATE TABLE IF NOT EXISTS documents (
id SERIAL PRIMARY KEY,
story_id INTEGER NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
path TEXT NOT NULL,
version TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE(story_id, path)
);
CREATE TABLE IF NOT EXISTS chunks (
id SERIAL PRIMARY KEY,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
chunk_index INTEGER NOT NULL,
hash TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536) NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_documents_story_id ON documents(story_id);
CREATE INDEX IF NOT EXISTS idx_chunks_document_id ON chunks(document_id);
CREATE INDEX IF NOT EXISTS idx_chunks_embedding ON chunks USING ivfflat (embedding vector_cosine_ops);

View File

@@ -0,0 +1,9 @@
# Postgres init scripts (optional)
Files here are mounted into the Postgres container at `/docker-entrypoint-initdb.d/` and run **only on first startup** (when the data volume is empty), in alphabetical order.
- `01-schema.sql` — creates pgvector extension and RAG tables (stories, documents, chunks).
- To add more users or other setup, add scripts with names like `00-create-user.sql` (they run before `01-schema.sql`).
- To disable init: in `docker-compose.yml`, comment out the postgres volume that mounts this folder, or remove/rename the `.sql` files.
After the first run, these scripts are not executed again. To re-run them, remove the volume: `docker compose down -v` (this deletes DB data), then `docker compose up -d`.

View File

@@ -8,6 +8,8 @@ dependencies = [
"psycopg[binary]>=3.1.18", "psycopg[binary]>=3.1.18",
"pgvector>=0.2.5", "pgvector>=0.2.5",
"pydantic>=2.7.0", "pydantic>=2.7.0",
"python-dotenv>=1.0.0",
"gigachat>=0.2.0",
] ]
[project.scripts] [project.scripts]

View File

@@ -2,8 +2,15 @@ from __future__ import annotations
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Sequence from typing import Iterable, Sequence
from dotenv import load_dotenv
# Load .env from repo root when config is used (e.g. for local runs)
_repo_root = Path(__file__).resolve().parent.parent.parent
load_dotenv(_repo_root / ".env")
@dataclass(frozen=True) @dataclass(frozen=True)
class AppConfig: class AppConfig:

View File

@@ -1,9 +1,17 @@
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
import os
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Protocol from typing import Iterable, Protocol
from dotenv import load_dotenv
# Ensure .env is loaded when resolving embedding client (e.g. GIGACHAT_CREDENTIALS)
_repo_root = Path(__file__).resolve().parent.parent.parent.parent
load_dotenv(_repo_root / ".env")
class EmbeddingClient(Protocol): class EmbeddingClient(Protocol):
def embed_texts(self, texts: Iterable[str]) -> list[list[float]]: def embed_texts(self, texts: Iterable[str]) -> list[list[float]]:
@@ -25,5 +33,50 @@ class StubEmbeddingClient:
return vectors return vectors
_GIGACHAT_BATCH_SIZE = 50
class GigaChatEmbeddingClient:
"""Embeddings via GigaChat API. Credentials from env GIGACHAT_CREDENTIALS."""
def __init__(
self,
credentials: str,
model: str = "Embeddings",
verify_ssl_certs: bool = False,
) -> None:
self._credentials = credentials.strip()
self._model = model
self._verify_ssl_certs = verify_ssl_certs
def embed_texts(self, texts: Iterable[str]) -> list[list[float]]:
from gigachat import GigaChat
texts_list = list(texts)
if not texts_list:
return []
result: list[list[float]] = []
for i in range(0, len(texts_list), _GIGACHAT_BATCH_SIZE):
batch = texts_list[i : i + _GIGACHAT_BATCH_SIZE]
with GigaChat(
credentials=self._credentials,
verify_ssl_certs=self._verify_ssl_certs,
) as giga:
response = giga.embeddings(model=self._model, input=batch)
# Preserve order by index
by_index = {item.index: item.embedding for item in response.data}
result.extend(by_index[j] for j in range(len(batch)))
return result
def get_embedding_client(dim: int) -> EmbeddingClient: def get_embedding_client(dim: int) -> EmbeddingClient:
credentials = os.getenv("GIGACHAT_CREDENTIALS", "").strip()
if credentials:
return GigaChatEmbeddingClient(
credentials=credentials,
model=os.getenv("GIGACHAT_EMBEDDINGS_MODEL", "Embeddings"),
verify_ssl_certs=os.getenv("GIGACHAT_VERIFY_SSL", "false").lower()
in ("1", "true", "yes"),
)
return StubEmbeddingClient(dim=dim) return StubEmbeddingClient(dim=dim)