ai-context-injector 1.0.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.
- ai_context_injector-1.0.0/LICENSE +21 -0
- ai_context_injector-1.0.0/PKG-INFO +680 -0
- ai_context_injector-1.0.0/README.md +642 -0
- ai_context_injector-1.0.0/pyproject.toml +143 -0
- ai_context_injector-1.0.0/setup.cfg +4 -0
- ai_context_injector-1.0.0/src/ai_context_injector/__init__.py +110 -0
- ai_context_injector-1.0.0/src/ai_context_injector/core/__init__.py +19 -0
- ai_context_injector-1.0.0/src/ai_context_injector/core/formatter.py +347 -0
- ai_context_injector-1.0.0/src/ai_context_injector/core/injector.py +397 -0
- ai_context_injector-1.0.0/src/ai_context_injector/core/parser.py +295 -0
- ai_context_injector-1.0.0/src/ai_context_injector/core/types.py +241 -0
- ai_context_injector-1.0.0/src/ai_context_injector/providers/__init__.py +5 -0
- ai_context_injector-1.0.0/src/ai_context_injector/providers/base.py +191 -0
- ai_context_injector-1.0.0/src/ai_context_injector/py.typed +2 -0
- ai_context_injector-1.0.0/src/ai_context_injector.egg-info/PKG-INFO +680 -0
- ai_context_injector-1.0.0/src/ai_context_injector.egg-info/SOURCES.txt +19 -0
- ai_context_injector-1.0.0/src/ai_context_injector.egg-info/dependency_links.txt +1 -0
- ai_context_injector-1.0.0/src/ai_context_injector.egg-info/requires.txt +10 -0
- ai_context_injector-1.0.0/src/ai_context_injector.egg-info/top_level.txt +1 -0
- ai_context_injector-1.0.0/tests/test_edge_cases.py +582 -0
- ai_context_injector-1.0.0/tests/test_integration.py +543 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dark
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-context-injector
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Lightweight Python library for tag-based context injection with anti-hallucination safeguards. Zero dependencies, 100% type-safe, <25ms performance.
|
|
5
|
+
Author-email: Dark <jdis03@proton.me>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JDis03/ai-context-injector
|
|
8
|
+
Project-URL: Documentation, https://github.com/JDis03/ai-context-injector#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/JDis03/ai-context-injector
|
|
10
|
+
Project-URL: Issues, https://github.com/JDis03/ai-context-injector/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/JDis03/ai-context-injector/releases
|
|
12
|
+
Keywords: ai,llm,context,injection,rag,memory,hallucination,openai,anthropic,claude
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
34
|
+
Provides-Extra: examples
|
|
35
|
+
Requires-Dist: openai>=1.0; extra == "examples"
|
|
36
|
+
Requires-Dist: anthropic>=0.18; extra == "examples"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# AI Context Injector
|
|
40
|
+
|
|
41
|
+
A lightweight Python library for tag-based context injection into LLM prompts with built-in anti-hallucination safeguards.
|
|
42
|
+
|
|
43
|
+
Extract relevant context from your codebase, memory systems, or custom data sources using simple tags like `@memory`, `@code`, or `@session` in user queries.
|
|
44
|
+
|
|
45
|
+
## Key Features
|
|
46
|
+
|
|
47
|
+
- **Tag-based retrieval**: `@memory dark keyboard`, `@code UserService`, `@session last decision`
|
|
48
|
+
- **Anti-hallucination safeguards**: 5 critical rules + automatic citations to prevent LLM fabrication
|
|
49
|
+
- **Hard project isolation**: Never mix contexts from different projects without explicit opt-in
|
|
50
|
+
- **Plugin architecture**: Write custom providers for any data source
|
|
51
|
+
- **Blazing fast**: <25ms total pipeline (proven at 17.7ms in production)
|
|
52
|
+
- **Zero dependencies**: Core library uses only Python stdlib
|
|
53
|
+
- **100% type-safe**: Full type hints with py.typed marker
|
|
54
|
+
- **Battle-tested**: 219 tests including real-world integration scenarios
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install ai-context-injector
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start (5 lines)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from ai_context_injector import ContextInjector
|
|
66
|
+
|
|
67
|
+
injector = ContextInjector(current_project="my-app")
|
|
68
|
+
injector.register_provider("@memory", my_memory_provider)
|
|
69
|
+
|
|
70
|
+
context = injector.inject("@memory user authentication flow")
|
|
71
|
+
# Ready to inject into LLM prompt!
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Why Context Injection?
|
|
75
|
+
|
|
76
|
+
LLMs need relevant context to provide accurate answers. Instead of manually copying code snippets or documentation, use tags to automatically retrieve and format context:
|
|
77
|
+
|
|
78
|
+
**Without context injection:**
|
|
79
|
+
```
|
|
80
|
+
User: "How does our authentication work?"
|
|
81
|
+
LLM: *hallucinates based on general knowledge*
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**With context injection:**
|
|
85
|
+
```
|
|
86
|
+
User: "@memory authentication @code AuthService"
|
|
87
|
+
System: *retrieves actual decisions + code*
|
|
88
|
+
LLM: *answers based on YOUR actual implementation*
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Complete Example
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from ai_context_injector import ContextInjector, ContextItem, IContextProvider
|
|
95
|
+
from datetime import datetime
|
|
96
|
+
|
|
97
|
+
# 1. Create a custom provider
|
|
98
|
+
class MyMemoryProvider(IContextProvider):
|
|
99
|
+
@property
|
|
100
|
+
def name(self) -> str:
|
|
101
|
+
return "MyMemory"
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def source_type(self) -> str:
|
|
105
|
+
return "memory"
|
|
106
|
+
|
|
107
|
+
def is_available(self) -> bool:
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
def retrieve(self, request):
|
|
111
|
+
# Your retrieval logic here (database, files, API, etc.)
|
|
112
|
+
results = search_my_database(request.query, request.project)
|
|
113
|
+
|
|
114
|
+
return [
|
|
115
|
+
ContextItem(
|
|
116
|
+
content=result.text,
|
|
117
|
+
source="memory",
|
|
118
|
+
project=request.project,
|
|
119
|
+
metadata={"id": result.id},
|
|
120
|
+
relevance_score=result.score,
|
|
121
|
+
timestamp=result.created_at
|
|
122
|
+
)
|
|
123
|
+
for result in results
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
# 2. Initialize injector
|
|
127
|
+
injector = ContextInjector(current_project="my-app")
|
|
128
|
+
injector.register_provider("@memory", MyMemoryProvider())
|
|
129
|
+
|
|
130
|
+
# 3. Inject context from user queries
|
|
131
|
+
user_query = "Tell me @memory what we decided about the database"
|
|
132
|
+
|
|
133
|
+
if injector.has_tags(user_query):
|
|
134
|
+
context = injector.inject(user_query)
|
|
135
|
+
|
|
136
|
+
# Inject into LLM prompt
|
|
137
|
+
llm_prompt = f"""
|
|
138
|
+
{context}
|
|
139
|
+
|
|
140
|
+
User Question: {injector.extract_query_only(user_query)}
|
|
141
|
+
|
|
142
|
+
Answer based ONLY on the context above.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
response = llm.generate(llm_prompt)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Anti-Hallucination Safeguards
|
|
149
|
+
|
|
150
|
+
Every injected context includes 5 critical rules to prevent LLM fabrication:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
=== BEGIN CONTEXT ===
|
|
154
|
+
|
|
155
|
+
CRITICAL RULES FOR USING THIS CONTEXT:
|
|
156
|
+
1. ONLY cite information that appears in the context sections below
|
|
157
|
+
2. If context is from a different project, CLEARLY state which project
|
|
158
|
+
3. Include source citations [memory:project:date] or [code:file:line]
|
|
159
|
+
4. If unsure or context missing, say "I don't have information about this"
|
|
160
|
+
5. NEVER mix information from different projects without explicit warning
|
|
161
|
+
|
|
162
|
+
Retrieved Context for: my-app
|
|
163
|
+
Found 2 relevant item(s)
|
|
164
|
+
|
|
165
|
+
--- Context Item 1/2 ---
|
|
166
|
+
Source: memory | Project: my-app | Relevance: 0.95 | Date: 2026-05-30
|
|
167
|
+
Citation: [memory:my-app:2026-05-30]
|
|
168
|
+
|
|
169
|
+
Decision: Use PostgreSQL with WAL mode for better performance
|
|
170
|
+
|
|
171
|
+
--- Context Item 2/2 ---
|
|
172
|
+
Source: code | Project: my-app | Relevance: 0.88 | Date: 2026-05-30
|
|
173
|
+
Citation: [code:src/db/connection.py:15-23]
|
|
174
|
+
|
|
175
|
+
def create_connection():
|
|
176
|
+
return psycopg2.connect(
|
|
177
|
+
host="localhost",
|
|
178
|
+
database="myapp",
|
|
179
|
+
options="-c wal_level=replica"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
=== END CONTEXT ===
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Tag Syntax
|
|
186
|
+
|
|
187
|
+
### Basic Tags
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
"@memory user authentication" # Search memory for "user authentication"
|
|
191
|
+
"@code AuthService class" # Search code for "AuthService class"
|
|
192
|
+
"@session last decision" # Search current session
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Modifiers
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
"@memory:all database design" # Search across ALL projects (with warning)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Multiple Tags
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
"@memory architecture and @code UserService" # Aggregates from both sources
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Project Isolation (Key Differentiator)
|
|
208
|
+
|
|
209
|
+
By default, context is **strictly isolated** to the current project:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
injector = ContextInjector(current_project="frontend")
|
|
213
|
+
|
|
214
|
+
# Only retrieves from "frontend" project
|
|
215
|
+
context = injector.inject("@memory React components")
|
|
216
|
+
|
|
217
|
+
# To search across projects, use :all modifier (generates warning)
|
|
218
|
+
context = injector.inject("@memory:all authentication patterns")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Why this matters:** Prevents LLMs from mixing context across different projects, which is a major source of hallucinations and incorrect answers.
|
|
222
|
+
|
|
223
|
+
## Advanced Usage
|
|
224
|
+
|
|
225
|
+
### With Performance Metrics
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
response = injector.inject_with_metrics("@memory query")
|
|
229
|
+
|
|
230
|
+
print(f"Found: {response.total_found} items")
|
|
231
|
+
print(f"Filtered: {response.filtered_count} items")
|
|
232
|
+
print(f"Performance: {response.performance_ms:.2f}ms")
|
|
233
|
+
print(f"Filter ratio: {response.filter_ratio:.1%}")
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Custom Parser and Formatter
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from ai_context_injector import TagParser, ContextFormatter
|
|
240
|
+
|
|
241
|
+
# Custom parser with additional tags
|
|
242
|
+
parser = TagParser(custom_tags={"@docs", "@tickets"})
|
|
243
|
+
|
|
244
|
+
# Compact formatter for tight token budgets
|
|
245
|
+
formatter = ContextFormatter(
|
|
246
|
+
include_metadata=False,
|
|
247
|
+
include_anti_hallucination_rules=False
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
injector = ContextInjector(
|
|
251
|
+
current_project="my-app",
|
|
252
|
+
parser=parser,
|
|
253
|
+
formatter=formatter
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Relevance Filtering
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
# Only return items with relevance >= 0.80
|
|
261
|
+
context = injector.inject(
|
|
262
|
+
"@memory database optimization",
|
|
263
|
+
min_relevance=0.80,
|
|
264
|
+
max_items=5
|
|
265
|
+
)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Custom Provider Examples
|
|
269
|
+
|
|
270
|
+
### Simple In-Memory Provider
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from ai_context_injector import IContextProvider, ContextItem
|
|
274
|
+
from datetime import datetime
|
|
275
|
+
|
|
276
|
+
class SimpleProvider(IContextProvider):
|
|
277
|
+
def __init__(self, data):
|
|
278
|
+
self.data = data # Dict[str, List[tuple]]
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def name(self) -> str:
|
|
282
|
+
return "Simple"
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def source_type(self) -> str:
|
|
286
|
+
return "memory"
|
|
287
|
+
|
|
288
|
+
def is_available(self) -> bool:
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
def retrieve(self, request):
|
|
292
|
+
items = self.data.get(request.project, [])
|
|
293
|
+
|
|
294
|
+
return [
|
|
295
|
+
ContextItem(
|
|
296
|
+
content=content,
|
|
297
|
+
source="memory",
|
|
298
|
+
project=request.project,
|
|
299
|
+
metadata={},
|
|
300
|
+
relevance_score=score,
|
|
301
|
+
timestamp=datetime.now()
|
|
302
|
+
)
|
|
303
|
+
for content, score in items
|
|
304
|
+
if score >= request.min_relevance
|
|
305
|
+
][:request.max_items]
|
|
306
|
+
|
|
307
|
+
# Usage
|
|
308
|
+
data = {
|
|
309
|
+
"my-app": [
|
|
310
|
+
("Decision: Use Redis for caching", 0.95),
|
|
311
|
+
("Learning: Connection pooling improved latency", 0.90),
|
|
312
|
+
]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
provider = SimpleProvider(data)
|
|
316
|
+
injector.register_provider("@memory", provider)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### SQLite Provider
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
import sqlite3
|
|
323
|
+
from ai_context_injector import IContextProvider, ContextItem
|
|
324
|
+
from datetime import datetime
|
|
325
|
+
|
|
326
|
+
class SQLiteProvider(IContextProvider):
|
|
327
|
+
def __init__(self, db_path):
|
|
328
|
+
self.db_path = db_path
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def name(self) -> str:
|
|
332
|
+
return "SQLite"
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def source_type(self) -> str:
|
|
336
|
+
return "memory"
|
|
337
|
+
|
|
338
|
+
def is_available(self) -> bool:
|
|
339
|
+
try:
|
|
340
|
+
conn = sqlite3.connect(self.db_path)
|
|
341
|
+
conn.close()
|
|
342
|
+
return True
|
|
343
|
+
except:
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
def retrieve(self, request):
|
|
347
|
+
conn = sqlite3.connect(self.db_path)
|
|
348
|
+
cursor = conn.cursor()
|
|
349
|
+
|
|
350
|
+
# Simple full-text search
|
|
351
|
+
cursor.execute("""
|
|
352
|
+
SELECT content, relevance, timestamp
|
|
353
|
+
FROM memories
|
|
354
|
+
WHERE project = ?
|
|
355
|
+
AND content LIKE ?
|
|
356
|
+
AND relevance >= ?
|
|
357
|
+
ORDER BY relevance DESC
|
|
358
|
+
LIMIT ?
|
|
359
|
+
""", (
|
|
360
|
+
request.project,
|
|
361
|
+
f"%{request.query}%",
|
|
362
|
+
request.min_relevance,
|
|
363
|
+
request.max_items
|
|
364
|
+
))
|
|
365
|
+
|
|
366
|
+
results = [
|
|
367
|
+
ContextItem(
|
|
368
|
+
content=row[0],
|
|
369
|
+
source="memory",
|
|
370
|
+
project=request.project,
|
|
371
|
+
metadata={},
|
|
372
|
+
relevance_score=row[1],
|
|
373
|
+
timestamp=datetime.fromisoformat(row[2])
|
|
374
|
+
)
|
|
375
|
+
for row in cursor.fetchall()
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
conn.close()
|
|
379
|
+
return results
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Filesystem Code Provider
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
import subprocess
|
|
386
|
+
from pathlib import Path
|
|
387
|
+
from ai_context_injector import IContextProvider, ContextItem
|
|
388
|
+
from datetime import datetime
|
|
389
|
+
|
|
390
|
+
class FilesystemCodeProvider(IContextProvider):
|
|
391
|
+
def __init__(self, repo_path):
|
|
392
|
+
self.repo_path = Path(repo_path)
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def name(self) -> str:
|
|
396
|
+
return "Filesystem"
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def source_type(self) -> str:
|
|
400
|
+
return "code"
|
|
401
|
+
|
|
402
|
+
def is_available(self) -> bool:
|
|
403
|
+
return self.repo_path.exists()
|
|
404
|
+
|
|
405
|
+
def retrieve(self, request):
|
|
406
|
+
# Use ripgrep for fast search
|
|
407
|
+
result = subprocess.run(
|
|
408
|
+
["rg", "--json", request.query, str(self.repo_path)],
|
|
409
|
+
capture_output=True,
|
|
410
|
+
text=True
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Parse ripgrep JSON output and convert to ContextItem
|
|
414
|
+
# (implementation details omitted for brevity)
|
|
415
|
+
|
|
416
|
+
return items[:request.max_items]
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## API Reference
|
|
420
|
+
|
|
421
|
+
### Core Classes
|
|
422
|
+
|
|
423
|
+
#### `ContextInjector`
|
|
424
|
+
|
|
425
|
+
Main orchestrator for the entire pipeline.
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
injector = ContextInjector(
|
|
429
|
+
current_project="my-app", # Project name (auto-detected from cwd if None)
|
|
430
|
+
parser=None, # Custom TagParser instance
|
|
431
|
+
formatter=None # Custom ContextFormatter instance
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Register providers
|
|
435
|
+
injector.register_provider(tag, provider)
|
|
436
|
+
|
|
437
|
+
# Inject context
|
|
438
|
+
context = injector.inject(
|
|
439
|
+
user_input, # String with tags
|
|
440
|
+
max_items=10, # Max total items
|
|
441
|
+
min_relevance=0.70 # Min relevance score (0.0-1.0)
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# With metrics
|
|
445
|
+
response = injector.inject_with_metrics(user_input, max_items=10, min_relevance=0.70)
|
|
446
|
+
|
|
447
|
+
# Utility methods
|
|
448
|
+
injector.has_tags(user_input) # bool
|
|
449
|
+
injector.extract_query_only(user_input) # str (tags removed)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
#### `IContextProvider`
|
|
453
|
+
|
|
454
|
+
Abstract base class for custom providers.
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
class MyProvider(IContextProvider):
|
|
458
|
+
@property
|
|
459
|
+
def name(self) -> str:
|
|
460
|
+
"""Provider name for logging/debugging."""
|
|
461
|
+
return "MyProvider"
|
|
462
|
+
|
|
463
|
+
@property
|
|
464
|
+
def source_type(self) -> str:
|
|
465
|
+
"""Source type: 'memory', 'code', 'session', 'custom', etc."""
|
|
466
|
+
return "memory"
|
|
467
|
+
|
|
468
|
+
def is_available(self) -> bool:
|
|
469
|
+
"""Check if provider is available (DB connected, files exist, etc.)."""
|
|
470
|
+
return True
|
|
471
|
+
|
|
472
|
+
def retrieve(self, request: ContextRequest) -> List[ContextItem]:
|
|
473
|
+
"""Retrieve context items matching the request."""
|
|
474
|
+
# Your logic here
|
|
475
|
+
return items
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
#### `ContextItem`
|
|
479
|
+
|
|
480
|
+
Represents a single piece of context.
|
|
481
|
+
|
|
482
|
+
```python
|
|
483
|
+
item = ContextItem(
|
|
484
|
+
content="Decision: Use PostgreSQL", # Required
|
|
485
|
+
source="memory", # Required
|
|
486
|
+
project="my-app", # Required
|
|
487
|
+
metadata={}, # Required (can be empty)
|
|
488
|
+
relevance_score=0.95, # Required (0.0-1.0)
|
|
489
|
+
timestamp=datetime.now(), # Required
|
|
490
|
+
file_path=None, # Optional (for code)
|
|
491
|
+
line_range=None # Optional (for code, tuple)
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Generate citation
|
|
495
|
+
citation = item.citation() # "[memory:my-app:2026-05-30]"
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
#### `TagParser`
|
|
499
|
+
|
|
500
|
+
Parses tags from user input.
|
|
501
|
+
|
|
502
|
+
```python
|
|
503
|
+
parser = TagParser(custom_tags={"@docs", "@tickets"})
|
|
504
|
+
|
|
505
|
+
tags = parser.parse("@memory query") # List[ParsedTag]
|
|
506
|
+
has_tags = parser.has_tags("@memory query") # bool
|
|
507
|
+
clean = parser.remove_tags("@memory query") # str
|
|
508
|
+
parser.register_tag("@custom") # None
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
#### `ContextFormatter`
|
|
512
|
+
|
|
513
|
+
Formats context items for LLM injection.
|
|
514
|
+
|
|
515
|
+
```python
|
|
516
|
+
formatter = ContextFormatter(
|
|
517
|
+
include_metadata=True,
|
|
518
|
+
include_citations=True,
|
|
519
|
+
include_anti_hallucination_rules=True
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
response = formatter.format(items, current_project)
|
|
523
|
+
compact = formatter.format_compact(items)
|
|
524
|
+
single = formatter.format_single(item, include_delimiters=True)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Convenience Functions
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
from ai_context_injector import inject_context, parse_tags, format_context
|
|
531
|
+
|
|
532
|
+
# Quick injection
|
|
533
|
+
context = inject_context(
|
|
534
|
+
user_input="@memory query",
|
|
535
|
+
providers={"@memory": my_provider},
|
|
536
|
+
project="my-app",
|
|
537
|
+
max_items=10,
|
|
538
|
+
min_relevance=0.70
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Quick parsing
|
|
542
|
+
tags = parse_tags("@memory query", custom_tags={"@docs"})
|
|
543
|
+
|
|
544
|
+
# Quick formatting
|
|
545
|
+
response = format_context(items, current_project="my-app")
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## Architecture
|
|
549
|
+
|
|
550
|
+
```
|
|
551
|
+
User Input: "@memory dark keyboard @code KeyboardView"
|
|
552
|
+
│
|
|
553
|
+
├─> TagParser
|
|
554
|
+
│ ├─> ParsedTag(tag="@memory", query="dark keyboard")
|
|
555
|
+
│ └─> ParsedTag(tag="@code", query="KeyboardView")
|
|
556
|
+
│
|
|
557
|
+
├─> ContextInjector
|
|
558
|
+
│ ├─> Route to providers
|
|
559
|
+
│ │ ├─> MemoryProvider.retrieve() → [ContextItem, ...]
|
|
560
|
+
│ │ └─> CodeProvider.retrieve() → [ContextItem, ...]
|
|
561
|
+
│ │
|
|
562
|
+
│ ├─> Aggregate results
|
|
563
|
+
│ ├─> Deduplicate (first 100 chars hash)
|
|
564
|
+
│ ├─> Sort by relevance
|
|
565
|
+
│ └─> Limit to max_items
|
|
566
|
+
│
|
|
567
|
+
└─> ContextFormatter
|
|
568
|
+
├─> Add delimiters
|
|
569
|
+
├─> Add anti-hallucination rules
|
|
570
|
+
├─> Add metadata + citations
|
|
571
|
+
├─> Check cross-project warnings
|
|
572
|
+
└─> Format items
|
|
573
|
+
│
|
|
574
|
+
└─> Formatted context string (ready for LLM)
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
## Performance
|
|
578
|
+
|
|
579
|
+
Target: <25ms total pipeline
|
|
580
|
+
Proven: 17.7ms in production (memory-system)
|
|
581
|
+
|
|
582
|
+
Breakdown:
|
|
583
|
+
- Parsing: <1ms
|
|
584
|
+
- Provider retrieval: ~10ms (depends on your provider)
|
|
585
|
+
- Aggregation/dedup: ~2ms
|
|
586
|
+
- Sorting: ~2ms
|
|
587
|
+
- Formatting: ~2ms
|
|
588
|
+
|
|
589
|
+
**Tip:** Provider performance is usually the bottleneck. Use indexes, caching, and efficient queries.
|
|
590
|
+
|
|
591
|
+
## Testing
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
# Run all tests
|
|
595
|
+
pytest
|
|
596
|
+
|
|
597
|
+
# Run with coverage
|
|
598
|
+
pytest --cov=ai_context_injector
|
|
599
|
+
|
|
600
|
+
# Run only unit tests
|
|
601
|
+
pytest tests/core/
|
|
602
|
+
|
|
603
|
+
# Run only integration tests
|
|
604
|
+
pytest tests/test_integration.py
|
|
605
|
+
|
|
606
|
+
# Run specific test
|
|
607
|
+
pytest tests/core/test_injector.py::TestBasicInjection::test_inject_with_single_tag
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
## Design Principles
|
|
611
|
+
|
|
612
|
+
1. **Explicit over implicit**: Empty providers dict by default, users must register
|
|
613
|
+
2. **Project isolation by default**: Never auto-mix contexts across projects
|
|
614
|
+
3. **Zero magic**: No auto-discovery, no global state, no hidden dependencies
|
|
615
|
+
4. **Library not framework**: Integrate into your app, don't build around it
|
|
616
|
+
5. **Performance matters**: <25ms target, every millisecond counts
|
|
617
|
+
6. **Type safety**: Full type hints, no `Any` types in public API
|
|
618
|
+
7. **Fail gracefully**: Missing/unavailable providers return None, not exceptions
|
|
619
|
+
|
|
620
|
+
## FAQ
|
|
621
|
+
|
|
622
|
+
### Why not just use RAG?
|
|
623
|
+
|
|
624
|
+
RAG is great for semantic search, but context injection solves a different problem:
|
|
625
|
+
- **Structured retrieval**: Tag-based routing to different data sources
|
|
626
|
+
- **Multi-source aggregation**: Combine memory + code + session in one query
|
|
627
|
+
- **Project isolation**: Hard boundaries prevent context mixing
|
|
628
|
+
- **Anti-hallucination**: Built-in safeguards with citations
|
|
629
|
+
|
|
630
|
+
You can use RAG *as a provider* in this system!
|
|
631
|
+
|
|
632
|
+
### Why project isolation by default?
|
|
633
|
+
|
|
634
|
+
In multi-project environments, mixing contexts causes LLMs to:
|
|
635
|
+
- Suggest code patterns from wrong project
|
|
636
|
+
- Reference APIs that don't exist in current project
|
|
637
|
+
- Mix architectural decisions across boundaries
|
|
638
|
+
|
|
639
|
+
Hard isolation by default prevents these errors. Use `:all` modifier when you actually want cross-project search.
|
|
640
|
+
|
|
641
|
+
### Can I use this with any LLM?
|
|
642
|
+
|
|
643
|
+
Yes! This library just generates formatted context strings. Use them with:
|
|
644
|
+
- OpenAI (GPT-4, GPT-3.5)
|
|
645
|
+
- Anthropic (Claude)
|
|
646
|
+
- Local models (Ollama, LM Studio)
|
|
647
|
+
- Any LLM API that accepts text prompts
|
|
648
|
+
|
|
649
|
+
### How do I handle large codebases?
|
|
650
|
+
|
|
651
|
+
1. **Smart providers**: Use search indexes (ripgrep, SQLite FTS, Elasticsearch)
|
|
652
|
+
2. **Relevance filtering**: Set `min_relevance=0.80` to reduce noise
|
|
653
|
+
3. **Limit results**: Use `max_items=5` for tight token budgets
|
|
654
|
+
4. **Compact format**: Disable metadata with `ContextFormatter(include_metadata=False)`
|
|
655
|
+
|
|
656
|
+
### Can I use this in production?
|
|
657
|
+
|
|
658
|
+
Yes! The core library has:
|
|
659
|
+
- 219 tests (100% passing)
|
|
660
|
+
- No external dependencies
|
|
661
|
+
- Battle-tested in memory-system project
|
|
662
|
+
- Proven <20ms performance
|
|
663
|
+
|
|
664
|
+
Just write solid providers and you're good to go.
|
|
665
|
+
|
|
666
|
+
## Contributing
|
|
667
|
+
|
|
668
|
+
Contributions welcome! Please:
|
|
669
|
+
1. Add tests for new features
|
|
670
|
+
2. Follow existing code style
|
|
671
|
+
3. Update documentation
|
|
672
|
+
4. Run `pytest` before submitting
|
|
673
|
+
|
|
674
|
+
## License
|
|
675
|
+
|
|
676
|
+
MIT License - see LICENSE file for details
|
|
677
|
+
|
|
678
|
+
## Acknowledgments
|
|
679
|
+
|
|
680
|
+
Ported from the context injection system in memory-system project, which has been battle-tested in production for multi-project development workflows.
|