repoask 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. repoask-0.1.0/PKG-INFO +246 -0
  2. repoask-0.1.0/README.md +223 -0
  3. repoask-0.1.0/pyproject.toml +40 -0
  4. repoask-0.1.0/setup.cfg +4 -0
  5. repoask-0.1.0/src/repoask/__init__.py +1 -0
  6. repoask-0.1.0/src/repoask/chat/__init__.py +0 -0
  7. repoask-0.1.0/src/repoask/chat/assistant.py +124 -0
  8. repoask-0.1.0/src/repoask/cli.py +435 -0
  9. repoask-0.1.0/src/repoask/config/__init__.py +0 -0
  10. repoask-0.1.0/src/repoask/config/manager.py +61 -0
  11. repoask-0.1.0/src/repoask/config/schema.py +41 -0
  12. repoask-0.1.0/src/repoask/ingestion/__init__.py +0 -0
  13. repoask-0.1.0/src/repoask/ingestion/chunker.py +235 -0
  14. repoask-0.1.0/src/repoask/ingestion/scanner.py +68 -0
  15. repoask-0.1.0/src/repoask/ingestion/tracker.py +69 -0
  16. repoask-0.1.0/src/repoask/providers/__init__.py +0 -0
  17. repoask-0.1.0/src/repoask/providers/base.py +28 -0
  18. repoask-0.1.0/src/repoask/providers/embeddings/__init__.py +0 -0
  19. repoask-0.1.0/src/repoask/providers/embeddings/factory.py +18 -0
  20. repoask-0.1.0/src/repoask/providers/embeddings/huggingface.py +19 -0
  21. repoask-0.1.0/src/repoask/providers/embeddings/openai.py +17 -0
  22. repoask-0.1.0/src/repoask/providers/llm/__init__.py +0 -0
  23. repoask-0.1.0/src/repoask/providers/llm/anthropic.py +49 -0
  24. repoask-0.1.0/src/repoask/providers/llm/factory.py +41 -0
  25. repoask-0.1.0/src/repoask/providers/llm/groq.py +44 -0
  26. repoask-0.1.0/src/repoask/providers/llm/openai.py +44 -0
  27. repoask-0.1.0/src/repoask/retrieval/__init__.py +0 -0
  28. repoask-0.1.0/src/repoask/retrieval/context_builder.py +80 -0
  29. repoask-0.1.0/src/repoask/retrieval/retriever.py +33 -0
  30. repoask-0.1.0/src/repoask/store/__init__.py +0 -0
  31. repoask-0.1.0/src/repoask/store/chroma.py +95 -0
  32. repoask-0.1.0/src/repoask.egg-info/PKG-INFO +246 -0
  33. repoask-0.1.0/src/repoask.egg-info/SOURCES.txt +35 -0
  34. repoask-0.1.0/src/repoask.egg-info/dependency_links.txt +1 -0
  35. repoask-0.1.0/src/repoask.egg-info/entry_points.txt +2 -0
  36. repoask-0.1.0/src/repoask.egg-info/requires.txt +17 -0
  37. repoask-0.1.0/src/repoask.egg-info/top_level.txt +1 -0
repoask-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,246 @@
1
+ Metadata-Version: 2.4
2
+ Name: repoask
3
+ Version: 0.1.0
4
+ Summary: AI-powered repository assistant — ask questions about any codebase
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: typer>=0.12
9
+ Requires-Dist: rich>=13
10
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
11
+ Requires-Dist: tomli-w>=1.0
12
+ Requires-Dist: pydantic>=2
13
+ Requires-Dist: tree-sitter>=0.23
14
+ Requires-Dist: tree-sitter-python>=0.23
15
+ Requires-Dist: tree-sitter-javascript>=0.23
16
+ Requires-Dist: tree-sitter-typescript>=0.23
17
+ Requires-Dist: chromadb>=0.5
18
+ Requires-Dist: sentence-transformers>=3
19
+ Requires-Dist: openai>=1
20
+ Requires-Dist: anthropic>=0.28
21
+ Requires-Dist: groq>=0.9
22
+ Requires-Dist: pathspec>=0.12
23
+
24
+ # RepoAsk
25
+
26
+ Ask questions about any codebase using AI. RepoAsk indexes your repository with a RAG pipeline — chunking code by AST symbols, embedding them locally, and answering questions with citations to exact file paths and line numbers.
27
+
28
+ No server, no database setup. Everything is stored as local files inside your project.
29
+
30
+ ---
31
+
32
+ ## Requirements
33
+
34
+ - Python 3.10+
35
+ - An API key for at least one LLM provider (Groq, OpenAI, or Anthropic)
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ RepoAsk is a CLI tool — install it with `pipx` so it gets its own isolated environment and is available system-wide.
42
+
43
+ ```bash
44
+ # Install pipx if you don't have it
45
+ brew install pipx
46
+ pipx ensurepath
47
+
48
+ # Install repoask
49
+ pipx install repoask
50
+ ```
51
+
52
+ > **macOS / Homebrew users:** Do not use `pip install` directly — Homebrew manages its Python environment and will block it. `pipx` is the correct tool for installing Python CLI applications.
53
+
54
+ **Alternative — manual venv (for development or local builds):**
55
+
56
+ ```bash
57
+ python3 -m venv ~/.venvs/repoask
58
+ source ~/.venvs/repoask/bin/activate
59
+ pip install repoask
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Quick Start
65
+
66
+ **1. Configure your providers and API keys (once, globally)**
67
+
68
+ ```bash
69
+ repoask init
70
+ ```
71
+
72
+ This walks you through choosing an embedding provider and an LLM provider, then saves your config to `~/.repoask/config.toml`.
73
+
74
+ **2. Index a repository**
75
+
76
+ ```bash
77
+ cd /path/to/your/repo
78
+ repoask index
79
+ ```
80
+
81
+ RepoAsk scans all Python, JavaScript, and TypeScript files, extracts symbols (functions, classes, interfaces) using tree-sitter, generates embeddings, and stores everything locally under `.repoask/`.
82
+
83
+ **3. Ask a question**
84
+
85
+ ```bash
86
+ repoask ask "How does authentication work?"
87
+ ```
88
+
89
+ Or start an interactive session:
90
+
91
+ ```bash
92
+ repoask chat
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Commands
98
+
99
+ ### `repoask init`
100
+ Interactive setup wizard. Sets your embedding provider, LLM provider, models, and API keys. Saves to `~/.repoask/config.toml`.
101
+
102
+ ### `repoask index`
103
+ Scans and indexes the current repository. Only re-indexes files that have changed since the last run (incremental by default).
104
+
105
+ ```bash
106
+ repoask index # incremental (default)
107
+ repoask index --full # force a complete re-index
108
+ ```
109
+
110
+ ### `repoask ask "<question>"`
111
+ One-shot question. Retrieves relevant code chunks, builds context, and streams an answer.
112
+
113
+ ```bash
114
+ repoask ask "Where is the database connection initialized?"
115
+ repoask ask "What does the UserService class do?"
116
+ repoask ask "How are API errors handled?" --top-k 15
117
+ repoask ask "Show me all route handlers" --lang typescript
118
+ ```
119
+
120
+ Options:
121
+ - `--top-k N` — number of chunks to retrieve (default: 10)
122
+ - `--lang LANG` — filter results to a specific language (`python`, `javascript`, `typescript`)
123
+
124
+ ### `repoask chat`
125
+ Interactive multi-turn session. Maintains conversation context within the session and persists history to disk between sessions.
126
+
127
+ ```
128
+ Commands inside chat:
129
+ /clear clear the current session context
130
+ /quit exit
131
+ ```
132
+
133
+ Options:
134
+ - `--top-k N` — number of chunks to retrieve per turn (default: 10)
135
+ - `--no-history` — skip persisting this session to disk
136
+
137
+ ### `repoask status`
138
+ Shows index statistics for the current repository.
139
+
140
+ ```
141
+ Repository /path/to/your/repo
142
+ Index path /path/to/your/repo/.repoask
143
+ Embedding huggingface / sentence-transformers/all-MiniLM-L6-v2
144
+ LLM groq / llama3-8b-8192
145
+ Chunks 1,842
146
+ Files indexed 74
147
+ Last indexed 2026-06-22 14:30:01
148
+ ```
149
+
150
+ ### `repoask config`
151
+ Displays the current global configuration with API keys masked.
152
+
153
+ ---
154
+
155
+ ## Supported Providers
156
+
157
+ ### Embedding
158
+ | Provider | Models | Notes |
159
+ |---|---|---|
160
+ | `huggingface` | `sentence-transformers/all-MiniLM-L6-v2` (default) | Runs locally, no API key needed |
161
+ | `openai` | `text-embedding-3-small`, `text-embedding-3-large` | Requires OpenAI API key |
162
+
163
+ ### LLM
164
+ | Provider | Example Models | Notes |
165
+ |---|---|---|
166
+ | `groq` | `llama3-8b-8192`, `mixtral-8x7b-32768` | Fast, free tier available |
167
+ | `openai` | `gpt-4o-mini`, `gpt-4o` | Requires OpenAI API key |
168
+ | `anthropic` | `claude-haiku-4-5-20251001`, `claude-sonnet-4-6` | Requires Anthropic API key |
169
+
170
+ ---
171
+
172
+ ## Configuration
173
+
174
+ Config is stored in TOML format. RepoAsk merges two config files in order:
175
+
176
+ 1. **Global** `~/.repoask/config.toml` — API keys and provider defaults, shared across all repos
177
+ 2. **Per-repo** `.repoask/config.toml` — overrides for a specific project (optional)
178
+
179
+ ### Full config reference
180
+
181
+ ```toml
182
+ [embedding]
183
+ provider = "huggingface"
184
+ model = "sentence-transformers/all-MiniLM-L6-v2"
185
+ api_key = ""
186
+
187
+ [llm]
188
+ provider = "groq"
189
+ model = "llama3-8b-8192"
190
+ api_key = "gsk_..."
191
+ temperature = 0.2
192
+ max_tokens = 2048
193
+
194
+ [indexing]
195
+ ignore_patterns = [
196
+ ".git", "node_modules", "__pycache__", "dist", "build",
197
+ ".venv", "venv", "*.lock", "*.min.js", "*.min.css",
198
+ "*.pyc", ".DS_Store", "coverage", ".next", ".nuxt",
199
+ ]
200
+ max_file_size_kb = 500
201
+ languages = ["python", "javascript", "typescript"]
202
+
203
+ [store]
204
+ path = ".repoask"
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Local Index Files
210
+
211
+ When you run `repoask index`, a `.repoask/` directory is created inside your repository:
212
+
213
+ ```
214
+ .repoask/
215
+ ├── chroma/ # vector store (ChromaDB, file-based)
216
+ ├── tracker.db # file hash registry for incremental indexing (SQLite)
217
+ └── history.db # chat session history (SQLite)
218
+ ```
219
+
220
+ No external database or server is required. Add `.repoask/` to your `.gitignore` to avoid committing the index.
221
+
222
+ ```bash
223
+ echo ".repoask/" >> .gitignore
224
+ ```
225
+
226
+ ---
227
+
228
+ ## How It Works
229
+
230
+ 1. **Scan** — traverses the repository, respecting `.gitignore` and configured ignore patterns
231
+ 2. **Chunk** — parses each file with tree-sitter and extracts symbols (functions, classes, interfaces) as individual chunks instead of fixed token windows
232
+ 3. **Embed** — converts each chunk to a vector embedding using your configured provider
233
+ 4. **Store** — saves chunks + embeddings + metadata (file path, symbol name, line numbers) to a local ChromaDB collection
234
+ 5. **Retrieve** — when you ask a question, it embeds the question and finds the most similar chunks via cosine similarity
235
+ 6. **Build context** — enriches the top-K hits with import-referenced definitions and assembles a structured context block
236
+ 7. **Answer** — sends the context + question to your LLM with a prompt that requires citations and disallows hallucination
237
+
238
+ ---
239
+
240
+ ## Tips
241
+
242
+ - Run `repoask index` after pulling new changes — it only re-embeds what changed.
243
+ - Use `--lang` to narrow answers when you know which part of the stack is relevant.
244
+ - Increase `--top-k` for broad architectural questions; lower it for specific function lookups.
245
+ - HuggingFace embedding (`all-MiniLM-L6-v2`) is free and fast enough for most repos. Switch to `text-embedding-3-small` if you want higher retrieval quality on large codebases.
246
+ - Groq is the recommended LLM provider for speed and cost — the free tier is sufficient for most usage.
@@ -0,0 +1,223 @@
1
+ # RepoAsk
2
+
3
+ Ask questions about any codebase using AI. RepoAsk indexes your repository with a RAG pipeline — chunking code by AST symbols, embedding them locally, and answering questions with citations to exact file paths and line numbers.
4
+
5
+ No server, no database setup. Everything is stored as local files inside your project.
6
+
7
+ ---
8
+
9
+ ## Requirements
10
+
11
+ - Python 3.10+
12
+ - An API key for at least one LLM provider (Groq, OpenAI, or Anthropic)
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ RepoAsk is a CLI tool — install it with `pipx` so it gets its own isolated environment and is available system-wide.
19
+
20
+ ```bash
21
+ # Install pipx if you don't have it
22
+ brew install pipx
23
+ pipx ensurepath
24
+
25
+ # Install repoask
26
+ pipx install repoask
27
+ ```
28
+
29
+ > **macOS / Homebrew users:** Do not use `pip install` directly — Homebrew manages its Python environment and will block it. `pipx` is the correct tool for installing Python CLI applications.
30
+
31
+ **Alternative — manual venv (for development or local builds):**
32
+
33
+ ```bash
34
+ python3 -m venv ~/.venvs/repoask
35
+ source ~/.venvs/repoask/bin/activate
36
+ pip install repoask
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Quick Start
42
+
43
+ **1. Configure your providers and API keys (once, globally)**
44
+
45
+ ```bash
46
+ repoask init
47
+ ```
48
+
49
+ This walks you through choosing an embedding provider and an LLM provider, then saves your config to `~/.repoask/config.toml`.
50
+
51
+ **2. Index a repository**
52
+
53
+ ```bash
54
+ cd /path/to/your/repo
55
+ repoask index
56
+ ```
57
+
58
+ RepoAsk scans all Python, JavaScript, and TypeScript files, extracts symbols (functions, classes, interfaces) using tree-sitter, generates embeddings, and stores everything locally under `.repoask/`.
59
+
60
+ **3. Ask a question**
61
+
62
+ ```bash
63
+ repoask ask "How does authentication work?"
64
+ ```
65
+
66
+ Or start an interactive session:
67
+
68
+ ```bash
69
+ repoask chat
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Commands
75
+
76
+ ### `repoask init`
77
+ Interactive setup wizard. Sets your embedding provider, LLM provider, models, and API keys. Saves to `~/.repoask/config.toml`.
78
+
79
+ ### `repoask index`
80
+ Scans and indexes the current repository. Only re-indexes files that have changed since the last run (incremental by default).
81
+
82
+ ```bash
83
+ repoask index # incremental (default)
84
+ repoask index --full # force a complete re-index
85
+ ```
86
+
87
+ ### `repoask ask "<question>"`
88
+ One-shot question. Retrieves relevant code chunks, builds context, and streams an answer.
89
+
90
+ ```bash
91
+ repoask ask "Where is the database connection initialized?"
92
+ repoask ask "What does the UserService class do?"
93
+ repoask ask "How are API errors handled?" --top-k 15
94
+ repoask ask "Show me all route handlers" --lang typescript
95
+ ```
96
+
97
+ Options:
98
+ - `--top-k N` — number of chunks to retrieve (default: 10)
99
+ - `--lang LANG` — filter results to a specific language (`python`, `javascript`, `typescript`)
100
+
101
+ ### `repoask chat`
102
+ Interactive multi-turn session. Maintains conversation context within the session and persists history to disk between sessions.
103
+
104
+ ```
105
+ Commands inside chat:
106
+ /clear clear the current session context
107
+ /quit exit
108
+ ```
109
+
110
+ Options:
111
+ - `--top-k N` — number of chunks to retrieve per turn (default: 10)
112
+ - `--no-history` — skip persisting this session to disk
113
+
114
+ ### `repoask status`
115
+ Shows index statistics for the current repository.
116
+
117
+ ```
118
+ Repository /path/to/your/repo
119
+ Index path /path/to/your/repo/.repoask
120
+ Embedding huggingface / sentence-transformers/all-MiniLM-L6-v2
121
+ LLM groq / llama3-8b-8192
122
+ Chunks 1,842
123
+ Files indexed 74
124
+ Last indexed 2026-06-22 14:30:01
125
+ ```
126
+
127
+ ### `repoask config`
128
+ Displays the current global configuration with API keys masked.
129
+
130
+ ---
131
+
132
+ ## Supported Providers
133
+
134
+ ### Embedding
135
+ | Provider | Models | Notes |
136
+ |---|---|---|
137
+ | `huggingface` | `sentence-transformers/all-MiniLM-L6-v2` (default) | Runs locally, no API key needed |
138
+ | `openai` | `text-embedding-3-small`, `text-embedding-3-large` | Requires OpenAI API key |
139
+
140
+ ### LLM
141
+ | Provider | Example Models | Notes |
142
+ |---|---|---|
143
+ | `groq` | `llama3-8b-8192`, `mixtral-8x7b-32768` | Fast, free tier available |
144
+ | `openai` | `gpt-4o-mini`, `gpt-4o` | Requires OpenAI API key |
145
+ | `anthropic` | `claude-haiku-4-5-20251001`, `claude-sonnet-4-6` | Requires Anthropic API key |
146
+
147
+ ---
148
+
149
+ ## Configuration
150
+
151
+ Config is stored in TOML format. RepoAsk merges two config files in order:
152
+
153
+ 1. **Global** `~/.repoask/config.toml` — API keys and provider defaults, shared across all repos
154
+ 2. **Per-repo** `.repoask/config.toml` — overrides for a specific project (optional)
155
+
156
+ ### Full config reference
157
+
158
+ ```toml
159
+ [embedding]
160
+ provider = "huggingface"
161
+ model = "sentence-transformers/all-MiniLM-L6-v2"
162
+ api_key = ""
163
+
164
+ [llm]
165
+ provider = "groq"
166
+ model = "llama3-8b-8192"
167
+ api_key = "gsk_..."
168
+ temperature = 0.2
169
+ max_tokens = 2048
170
+
171
+ [indexing]
172
+ ignore_patterns = [
173
+ ".git", "node_modules", "__pycache__", "dist", "build",
174
+ ".venv", "venv", "*.lock", "*.min.js", "*.min.css",
175
+ "*.pyc", ".DS_Store", "coverage", ".next", ".nuxt",
176
+ ]
177
+ max_file_size_kb = 500
178
+ languages = ["python", "javascript", "typescript"]
179
+
180
+ [store]
181
+ path = ".repoask"
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Local Index Files
187
+
188
+ When you run `repoask index`, a `.repoask/` directory is created inside your repository:
189
+
190
+ ```
191
+ .repoask/
192
+ ├── chroma/ # vector store (ChromaDB, file-based)
193
+ ├── tracker.db # file hash registry for incremental indexing (SQLite)
194
+ └── history.db # chat session history (SQLite)
195
+ ```
196
+
197
+ No external database or server is required. Add `.repoask/` to your `.gitignore` to avoid committing the index.
198
+
199
+ ```bash
200
+ echo ".repoask/" >> .gitignore
201
+ ```
202
+
203
+ ---
204
+
205
+ ## How It Works
206
+
207
+ 1. **Scan** — traverses the repository, respecting `.gitignore` and configured ignore patterns
208
+ 2. **Chunk** — parses each file with tree-sitter and extracts symbols (functions, classes, interfaces) as individual chunks instead of fixed token windows
209
+ 3. **Embed** — converts each chunk to a vector embedding using your configured provider
210
+ 4. **Store** — saves chunks + embeddings + metadata (file path, symbol name, line numbers) to a local ChromaDB collection
211
+ 5. **Retrieve** — when you ask a question, it embeds the question and finds the most similar chunks via cosine similarity
212
+ 6. **Build context** — enriches the top-K hits with import-referenced definitions and assembles a structured context block
213
+ 7. **Answer** — sends the context + question to your LLM with a prompt that requires citations and disallows hallucination
214
+
215
+ ---
216
+
217
+ ## Tips
218
+
219
+ - Run `repoask index` after pulling new changes — it only re-embeds what changed.
220
+ - Use `--lang` to narrow answers when you know which part of the stack is relevant.
221
+ - Increase `--top-k` for broad architectural questions; lower it for specific function lookups.
222
+ - HuggingFace embedding (`all-MiniLM-L6-v2`) is free and fast enough for most repos. Switch to `text-embedding-3-small` if you want higher retrieval quality on large codebases.
223
+ - Groq is the recommended LLM provider for speed and cost — the free tier is sufficient for most usage.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "repoask"
7
+ version = "0.1.0"
8
+ description = "AI-powered repository assistant — ask questions about any codebase"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ dependencies = [
13
+ # CLI
14
+ "typer>=0.12",
15
+ "rich>=13",
16
+ # Config
17
+ "tomli>=2.0; python_version < '3.11'",
18
+ "tomli-w>=1.0",
19
+ "pydantic>=2",
20
+ # Parsing
21
+ "tree-sitter>=0.23",
22
+ "tree-sitter-python>=0.23",
23
+ "tree-sitter-javascript>=0.23",
24
+ "tree-sitter-typescript>=0.23",
25
+ # Vector store
26
+ "chromadb>=0.5",
27
+ # Embeddings
28
+ "sentence-transformers>=3",
29
+ "openai>=1",
30
+ "anthropic>=0.28",
31
+ "groq>=0.9",
32
+ # Gitignore handling
33
+ "pathspec>=0.12",
34
+ ]
35
+
36
+ [project.scripts]
37
+ repoask = "repoask.cli:app"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from ..providers.base import LLMProvider, Message
9
+ from ..retrieval.context_builder import build_context
10
+ from ..retrieval.retriever import Retriever
11
+ from ..store.chroma import VectorStore
12
+
13
+ SYSTEM_PROMPT = """\
14
+ You are RepoAsk, an expert repository assistant.
15
+ You answer questions about the codebase using ONLY the context provided below.
16
+ When answering:
17
+ - Cite the exact file path and line numbers from the context (e.g. `src/foo.py:42`).
18
+ - If multiple functions are relevant, reference all of them.
19
+ - If the answer cannot be found in the provided context, say: "I don't have enough context to answer that — try re-indexing or narrowing your question to a specific file."
20
+ - Do NOT invent code or behaviour that is not present in the context.
21
+ - Keep answers concise but complete. Use markdown code blocks for code snippets.
22
+ """
23
+
24
+
25
+ class ChatSession:
26
+ """Holds the in-memory message history for a single interactive session."""
27
+
28
+ def __init__(
29
+ self,
30
+ llm: LLMProvider,
31
+ retriever: Retriever,
32
+ store: VectorStore,
33
+ history_db: Path | None = None,
34
+ top_k: int = 10,
35
+ ):
36
+ self._llm = llm
37
+ self._retriever = retriever
38
+ self._store = store
39
+ self._top_k = top_k
40
+ self._messages: list[Message] = []
41
+ self._history_db = history_db
42
+ self._session_id = str(int(time.time()))
43
+
44
+ if history_db:
45
+ self._init_history_db()
46
+
47
+ # ------------------------------------------------------------------ #
48
+ # Persistent history
49
+ # ------------------------------------------------------------------ #
50
+
51
+ def _init_history_db(self) -> None:
52
+ assert self._history_db
53
+ self._history_db.parent.mkdir(parents=True, exist_ok=True)
54
+ self._conn = sqlite3.connect(str(self._history_db))
55
+ self._conn.execute(
56
+ "CREATE TABLE IF NOT EXISTS messages "
57
+ "(id INTEGER PRIMARY KEY AUTOINCREMENT, "
58
+ "session_id TEXT, role TEXT, content TEXT, created_at REAL)"
59
+ )
60
+ self._conn.commit()
61
+
62
+ def _persist_message(self, message: Message) -> None:
63
+ if not self._history_db:
64
+ return
65
+ self._conn.execute(
66
+ "INSERT INTO messages (session_id, role, content, created_at) VALUES (?, ?, ?, ?)",
67
+ (self._session_id, message.role, message.content, time.time()),
68
+ )
69
+ self._conn.commit()
70
+
71
+ def load_last_session(self, n_messages: int = 10) -> None:
72
+ """Optionally seed RAM context with the tail of the previous session."""
73
+ if not self._history_db or not self._history_db.exists():
74
+ return
75
+ rows = self._conn.execute(
76
+ "SELECT role, content FROM messages "
77
+ "WHERE session_id = (SELECT session_id FROM messages ORDER BY created_at DESC LIMIT 1) "
78
+ "ORDER BY created_at DESC LIMIT ?",
79
+ (n_messages,),
80
+ ).fetchall()
81
+ for role, content in reversed(rows):
82
+ self._messages.append(Message(role=role, content=content))
83
+
84
+ # ------------------------------------------------------------------ #
85
+ # Core ask
86
+ # ------------------------------------------------------------------ #
87
+
88
+ def ask(self, question: str, stream: bool = True):
89
+ # 1. Retrieve relevant chunks
90
+ hits = self._retriever.retrieve(question, top_k=self._top_k)
91
+
92
+ # 2. Build context
93
+ context = build_context(hits, self._store, question)
94
+
95
+ # 3. Compose messages
96
+ user_content = f"## Retrieved context\n\n{context}\n\n## Question\n\n{question}"
97
+ user_msg = Message(role="user", content=user_content)
98
+ self._messages.append(user_msg)
99
+ self._persist_message(user_msg)
100
+
101
+ full_messages = [Message(role="system", content=SYSTEM_PROMPT)] + self._messages
102
+
103
+ # 4. Call LLM
104
+ if stream:
105
+ return self._stream_response(full_messages)
106
+ else:
107
+ response_text = self._llm.chat(full_messages, stream=False)
108
+ assistant_msg = Message(role="assistant", content=response_text)
109
+ self._messages.append(assistant_msg)
110
+ self._persist_message(assistant_msg)
111
+ return response_text
112
+
113
+ def _stream_response(self, messages: list[Message]):
114
+ chunks = []
115
+ for chunk in self._llm.chat(messages, stream=True):
116
+ chunks.append(chunk)
117
+ yield chunk
118
+ full_text = "".join(chunks)
119
+ assistant_msg = Message(role="assistant", content=full_text)
120
+ self._messages.append(assistant_msg)
121
+ self._persist_message(assistant_msg)
122
+
123
+ def clear(self) -> None:
124
+ self._messages.clear()