contexara 0.2.1__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.
- contexara-0.2.1/PKG-INFO +68 -0
- contexara-0.2.1/README.md +50 -0
- contexara-0.2.1/contexara/__init__.py +11 -0
- contexara-0.2.1/contexara/__main__.py +7 -0
- contexara-0.2.1/contexara/cli.py +236 -0
- contexara-0.2.1/contexara/embedder.py +39 -0
- contexara-0.2.1/contexara/enhance.py +30 -0
- contexara-0.2.1/contexara/extract.py +262 -0
- contexara-0.2.1/contexara/ingest.py +43 -0
- contexara-0.2.1/contexara/llm.py +143 -0
- contexara-0.2.1/contexara/main.py +40 -0
- contexara-0.2.1/contexara/retrieve.py +155 -0
- contexara-0.2.1/contexara/store.py +674 -0
- contexara-0.2.1/contexara.egg-info/PKG-INFO +68 -0
- contexara-0.2.1/contexara.egg-info/SOURCES.txt +27 -0
- contexara-0.2.1/contexara.egg-info/dependency_links.txt +1 -0
- contexara-0.2.1/contexara.egg-info/entry_points.txt +2 -0
- contexara-0.2.1/contexara.egg-info/requires.txt +8 -0
- contexara-0.2.1/contexara.egg-info/top_level.txt +1 -0
- contexara-0.2.1/pyproject.toml +35 -0
- contexara-0.2.1/setup.cfg +4 -0
- contexara-0.2.1/tests/test_cli.py +30 -0
- contexara-0.2.1/tests/test_enhance.py +25 -0
- contexara-0.2.1/tests/test_extract.py +83 -0
- contexara-0.2.1/tests/test_ingest.py +56 -0
- contexara-0.2.1/tests/test_llm.py +86 -0
- contexara-0.2.1/tests/test_main.py +57 -0
- contexara-0.2.1/tests/test_retrieve.py +61 -0
- contexara-0.2.1/tests/test_store.py +136 -0
contexara-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contexara
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: CLI-first memory engine for LLM applications.
|
|
5
|
+
Author: Contexara Contributors
|
|
6
|
+
Project-URL: Homepage, https://github.com/Prajwal-Narayan/Contexara
|
|
7
|
+
Project-URL: Repository, https://github.com/Prajwal-Narayan/Contexara
|
|
8
|
+
Keywords: llm,memory,agentic-ai,retrieval,context
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: boto3>=1.34.0
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
13
|
+
Requires-Dist: sentence-transformers>=3.0.0
|
|
14
|
+
Requires-Dist: numpy>=1.26.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
18
|
+
|
|
19
|
+
# Contexara
|
|
20
|
+
|
|
21
|
+
A CLI-first memory engine for LLM applications. Drop it into any AI system and it remembers what matters — across sessions, without a vector database, without infra.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install contexara
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
Every conversation turn is analysed by an LLM (AWS Bedrock Haiku) and distilled into atomic facts. Facts are embedded with `all-MiniLM-L6-v2` and stored as BLOBs in a local SQLite file — no external services required.
|
|
32
|
+
|
|
33
|
+
Retrieval uses **Reciprocal Rank Fusion**: semantic similarity and keyword overlap are ranked independently then fused, so queries work whether you ask loosely or precisely.
|
|
34
|
+
|
|
35
|
+
Memories are never deleted. When the store exceeds a budget threshold, old memories are compressed into sharp facts by the LLM and the originals move to an archive table — permanently recoverable. Every compressed memory carries a `compression_level` and is penalised in retrieval accordingly. Raw memories always beat compressed ones when scores are close.
|
|
36
|
+
|
|
37
|
+
## CLI
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
contexara ask "what was I working on?"
|
|
41
|
+
contexara store "prefers Python for backend work"
|
|
42
|
+
contexara list
|
|
43
|
+
contexara search "tech stack"
|
|
44
|
+
contexara stats
|
|
45
|
+
contexara consolidate
|
|
46
|
+
contexara delete <id>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Use in code
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from contexara import ask, store, retrieve, ingest_turn
|
|
53
|
+
|
|
54
|
+
store("User prefers concise answers")
|
|
55
|
+
ingest_turn(user_text, assistant_text) # auto-extracts memories
|
|
56
|
+
answer = ask("what do I prefer?")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Stack
|
|
60
|
+
|
|
61
|
+
- Storage: SQLite (embeddings as BLOBs, archive table included)
|
|
62
|
+
- Embeddings: `all-MiniLM-L6-v2` via sentence-transformers
|
|
63
|
+
- LLM: AWS Bedrock (Haiku) — extraction, compression, Q&A
|
|
64
|
+
- Retrieval: RRF hybrid search + source-aware ranking
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
Built by [Prajwal Narayan](https://github.com/Prajwal-Narayan)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Contexara
|
|
2
|
+
|
|
3
|
+
A CLI-first memory engine for LLM applications. Drop it into any AI system and it remembers what matters — across sessions, without a vector database, without infra.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install contexara
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
Every conversation turn is analysed by an LLM (AWS Bedrock Haiku) and distilled into atomic facts. Facts are embedded with `all-MiniLM-L6-v2` and stored as BLOBs in a local SQLite file — no external services required.
|
|
14
|
+
|
|
15
|
+
Retrieval uses **Reciprocal Rank Fusion**: semantic similarity and keyword overlap are ranked independently then fused, so queries work whether you ask loosely or precisely.
|
|
16
|
+
|
|
17
|
+
Memories are never deleted. When the store exceeds a budget threshold, old memories are compressed into sharp facts by the LLM and the originals move to an archive table — permanently recoverable. Every compressed memory carries a `compression_level` and is penalised in retrieval accordingly. Raw memories always beat compressed ones when scores are close.
|
|
18
|
+
|
|
19
|
+
## CLI
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
contexara ask "what was I working on?"
|
|
23
|
+
contexara store "prefers Python for backend work"
|
|
24
|
+
contexara list
|
|
25
|
+
contexara search "tech stack"
|
|
26
|
+
contexara stats
|
|
27
|
+
contexara consolidate
|
|
28
|
+
contexara delete <id>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Use in code
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from contexara import ask, store, retrieve, ingest_turn
|
|
35
|
+
|
|
36
|
+
store("User prefers concise answers")
|
|
37
|
+
ingest_turn(user_text, assistant_text) # auto-extracts memories
|
|
38
|
+
answer = ask("what do I prefer?")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Stack
|
|
42
|
+
|
|
43
|
+
- Storage: SQLite (embeddings as BLOBs, archive table included)
|
|
44
|
+
- Embeddings: `all-MiniLM-L6-v2` via sentence-transformers
|
|
45
|
+
- LLM: AWS Bedrock (Haiku) — extraction, compression, Q&A
|
|
46
|
+
- Retrieval: RRF hybrid search + source-aware ranking
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
Built by [Prajwal Narayan](https://github.com/Prajwal-Narayan)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Contexara public package API."""
|
|
2
|
+
|
|
3
|
+
from .enhance import enhance
|
|
4
|
+
from .ingest import ingest_turn
|
|
5
|
+
from .main import ask
|
|
6
|
+
from .retrieve import retrieve
|
|
7
|
+
from .store import clear_all, compress_old_memories, consolidate, delete_memory, get_stats, list_all, search_memories, store
|
|
8
|
+
|
|
9
|
+
__all__ = ["ask", "store", "list_all", "clear_all", "delete_memory", "search_memories", "get_stats", "consolidate", "compress_old_memories", "retrieve", "enhance", "ingest_turn"]
|
|
10
|
+
|
|
11
|
+
__version__ = "0.2.1"
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Command-line interface for Contexara."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .main import ask
|
|
10
|
+
from .store import clear_all, consolidate, delete_memory, get_stats, list_all, search_memories, store
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="contexara",
|
|
16
|
+
description="CLI-first memory engine for LLM context management.",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
19
|
+
|
|
20
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
21
|
+
|
|
22
|
+
subparsers.add_parser("chat", help="Start interactive chat mode.").set_defaults(func=_cmd_chat)
|
|
23
|
+
|
|
24
|
+
ask_parser = subparsers.add_parser("ask", help="Ask a memory-aware question.")
|
|
25
|
+
ask_parser.add_argument("query", nargs="+")
|
|
26
|
+
ask_parser.set_defaults(func=_cmd_ask)
|
|
27
|
+
|
|
28
|
+
store_parser = subparsers.add_parser("store", help="Store a memory string.")
|
|
29
|
+
store_parser.add_argument("content", nargs="+")
|
|
30
|
+
store_parser.set_defaults(func=_cmd_store)
|
|
31
|
+
|
|
32
|
+
subparsers.add_parser("list", help="List all stored memories.").set_defaults(func=_cmd_list)
|
|
33
|
+
|
|
34
|
+
delete_parser = subparsers.add_parser("delete", help="Delete a memory by ID.")
|
|
35
|
+
delete_parser.add_argument("id", type=int, help="Memory ID (from contexara list).")
|
|
36
|
+
delete_parser.set_defaults(func=_cmd_delete)
|
|
37
|
+
|
|
38
|
+
search_parser = subparsers.add_parser("search", help="Semantic search over memories.")
|
|
39
|
+
search_parser.add_argument("query", nargs="+")
|
|
40
|
+
search_parser.add_argument("--top", type=int, default=5, help="Number of results (default 5).")
|
|
41
|
+
search_parser.set_defaults(func=_cmd_search)
|
|
42
|
+
|
|
43
|
+
subparsers.add_parser("stats", help="Show memory statistics.").set_defaults(func=_cmd_stats)
|
|
44
|
+
|
|
45
|
+
subparsers.add_parser("consolidate", help="LLM-merge overlapping memories.").set_defaults(func=_cmd_consolidate)
|
|
46
|
+
|
|
47
|
+
clear_parser = subparsers.add_parser("clear", help="Delete all stored memories.")
|
|
48
|
+
clear_parser.add_argument("--yes", action="store_true", help="Skip confirmation.")
|
|
49
|
+
clear_parser.set_defaults(func=_cmd_clear)
|
|
50
|
+
|
|
51
|
+
return parser
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _join_parts(parts: Iterable[str]) -> str:
|
|
55
|
+
return " ".join(parts).strip()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _print_memories_table(memories: list[dict]) -> None:
|
|
59
|
+
if not memories:
|
|
60
|
+
print("(no memories)")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
id_w = max(len("ID"), max(len(str(m["id"])) for m in memories))
|
|
64
|
+
kind_w = max(len("KIND"), max(len(str(m.get("kind", ""))) for m in memories))
|
|
65
|
+
imp_w = max(len("IMP"), max(len(str(m.get("importance", ""))) for m in memories))
|
|
66
|
+
src_w = max(len("SRC"), max(len(str(m.get("source", "raw"))) for m in memories))
|
|
67
|
+
content_w = 60
|
|
68
|
+
|
|
69
|
+
header = f"{'ID':<{id_w}} {'KIND':<{kind_w}} {'IMP':<{imp_w}} {'SRC':<{src_w}} {'CONTENT':<{content_w}}"
|
|
70
|
+
print(header)
|
|
71
|
+
print("-" * len(header))
|
|
72
|
+
for m in memories:
|
|
73
|
+
content = m["content"]
|
|
74
|
+
if len(content) > content_w:
|
|
75
|
+
content = content[: content_w - 3] + "..."
|
|
76
|
+
print(f"{str(m['id']):<{id_w}} {str(m.get('kind','')):<{kind_w}} {str(m.get('importance','')):<{imp_w}} {str(m.get('source','raw')):<{src_w}} {content}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _cmd_chat(_args: argparse.Namespace) -> int:
|
|
80
|
+
print("\nContexara Chat (type 'exit' to quit)")
|
|
81
|
+
print("Commands: /store <text>, /list, /show <id>, /delete <id>, /search <query>, /stats, /consolidate, /clear\n")
|
|
82
|
+
|
|
83
|
+
while True:
|
|
84
|
+
user_input = input("You: ").strip()
|
|
85
|
+
if not user_input:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if user_input.lower() == "exit":
|
|
89
|
+
print("Goodbye.")
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
if user_input.startswith("/store "):
|
|
93
|
+
store(user_input[7:].strip())
|
|
94
|
+
print("Memory stored.")
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if user_input == "/list":
|
|
98
|
+
_print_memories_table(list_all())
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
if user_input.startswith("/delete "):
|
|
102
|
+
try:
|
|
103
|
+
mid = int(user_input[8:].strip())
|
|
104
|
+
if delete_memory(mid):
|
|
105
|
+
print(f"Memory {mid} deleted.")
|
|
106
|
+
else:
|
|
107
|
+
print(f"No memory with ID {mid}.")
|
|
108
|
+
except ValueError:
|
|
109
|
+
print("Usage: /delete <id>")
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
if user_input.startswith("/search "):
|
|
113
|
+
query = user_input[8:].strip()
|
|
114
|
+
results = search_memories(query)
|
|
115
|
+
_print_memories_table(results)
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if user_input.startswith("/show "):
|
|
119
|
+
try:
|
|
120
|
+
mid = int(user_input[6:].strip())
|
|
121
|
+
match = next((m for m in list_all() if m["id"] == mid), None)
|
|
122
|
+
if match:
|
|
123
|
+
print(f"\nID : {match['id']}")
|
|
124
|
+
print(f"Kind : {match['kind']}")
|
|
125
|
+
print(f"Importance: {match['importance']}")
|
|
126
|
+
print(f"Source : {match.get('source', 'raw')}")
|
|
127
|
+
print(f"Level : {match.get('compression_level', 0)}")
|
|
128
|
+
print(f"Created : {match['created_at']}")
|
|
129
|
+
print(f"Content : {match['content']}\n")
|
|
130
|
+
else:
|
|
131
|
+
print(f"No memory with ID {mid}.")
|
|
132
|
+
except ValueError:
|
|
133
|
+
print("Usage: /show <id>")
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if user_input == "/consolidate":
|
|
137
|
+
print("Consolidating memories...")
|
|
138
|
+
result = consolidate()
|
|
139
|
+
print(f"Done. {result['before']} memories → {result['after']} memories.")
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
if user_input == "/stats":
|
|
143
|
+
_print_stats(get_stats())
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
if user_input == "/clear":
|
|
147
|
+
clear_all()
|
|
148
|
+
print("All memories cleared.")
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
response = ask(user_input)
|
|
152
|
+
print(f"\nBot: {response}\n")
|
|
153
|
+
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _print_stats(stats: dict) -> None:
|
|
158
|
+
print(f"\nTotal memories : {stats['total']}")
|
|
159
|
+
print(f"Oldest : {stats['oldest'] or 'N/A'}")
|
|
160
|
+
print(f"Newest : {stats['newest'] or 'N/A'}")
|
|
161
|
+
print("\nBy kind:")
|
|
162
|
+
for kind, count in stats["by_kind"].items():
|
|
163
|
+
print(f" {kind:<12} {count}")
|
|
164
|
+
print("\nBy source:")
|
|
165
|
+
for source, count in stats.get("by_source", {}).items():
|
|
166
|
+
ss = stats.get("source_stats", {}).get(source, {})
|
|
167
|
+
avg_imp = ss.get("avg_importance", "")
|
|
168
|
+
avg_age = ss.get("avg_age_days", "")
|
|
169
|
+
print(f" {source:<12} {count:<6} avg_importance={avg_imp} avg_age={avg_age}d")
|
|
170
|
+
print()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _cmd_ask(args: argparse.Namespace) -> int:
|
|
174
|
+
print(ask(_join_parts(args.query)))
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _cmd_store(args: argparse.Namespace) -> int:
|
|
179
|
+
store(_join_parts(args.content))
|
|
180
|
+
print("Memory stored.")
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _cmd_list(_args: argparse.Namespace) -> int:
|
|
185
|
+
_print_memories_table(list_all())
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _cmd_delete(args: argparse.Namespace) -> int:
|
|
190
|
+
if delete_memory(args.id):
|
|
191
|
+
print(f"Memory {args.id} deleted.")
|
|
192
|
+
else:
|
|
193
|
+
print(f"No memory with ID {args.id}.")
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _cmd_search(args: argparse.Namespace) -> int:
|
|
198
|
+
results = search_memories(_join_parts(args.query), top_k=args.top)
|
|
199
|
+
_print_memories_table(results)
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _cmd_stats(_args: argparse.Namespace) -> int:
|
|
204
|
+
_print_stats(get_stats())
|
|
205
|
+
return 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _cmd_consolidate(_args: argparse.Namespace) -> int:
|
|
209
|
+
print("Consolidating memories...")
|
|
210
|
+
result = consolidate()
|
|
211
|
+
print(f"Done. {result['before']} memories → {result['after']} memories.")
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _cmd_clear(args: argparse.Namespace) -> int:
|
|
216
|
+
if not args.yes:
|
|
217
|
+
confirm = input("Delete all memories? [y/N]: ").strip().lower()
|
|
218
|
+
if confirm not in {"y", "yes"}:
|
|
219
|
+
print("Cancelled.")
|
|
220
|
+
return 0
|
|
221
|
+
clear_all()
|
|
222
|
+
print("All memories cleared.")
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main() -> int:
|
|
227
|
+
parser = _build_parser()
|
|
228
|
+
args = parser.parse_args()
|
|
229
|
+
if not args.command:
|
|
230
|
+
parser.print_help()
|
|
231
|
+
return 0
|
|
232
|
+
return args.func(args)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
_MODEL_NAME = "all-MiniLM-L6-v2"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@lru_cache(maxsize=1)
|
|
12
|
+
def _get_model():
|
|
13
|
+
import logging
|
|
14
|
+
from sentence_transformers import SentenceTransformer
|
|
15
|
+
logging.getLogger("transformers.modeling_utils").setLevel(logging.ERROR)
|
|
16
|
+
return SentenceTransformer(_MODEL_NAME)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def embed(text: str) -> np.ndarray:
|
|
20
|
+
"""Return a normalized float32 embedding vector for text."""
|
|
21
|
+
model = _get_model()
|
|
22
|
+
vector = model.encode(text, normalize_embeddings=True)
|
|
23
|
+
return vector.astype(np.float32)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def serialize(vector: np.ndarray) -> bytes:
|
|
27
|
+
buf = io.BytesIO()
|
|
28
|
+
np.save(buf, vector)
|
|
29
|
+
return buf.getvalue()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def deserialize(blob: bytes) -> np.ndarray:
|
|
33
|
+
buf = io.BytesIO(blob)
|
|
34
|
+
return np.load(buf)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
|
|
38
|
+
"""Cosine similarity for pre-normalized vectors (dot product suffices)."""
|
|
39
|
+
return float(np.dot(a, b))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
def enhance(query: str, memories: list[str]) -> str:
|
|
2
|
+
"""
|
|
3
|
+
Build an enhanced prompt by injecting relevant memories.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
if not query or not query.strip():
|
|
7
|
+
raise ValueError("Query cannot be empty")
|
|
8
|
+
|
|
9
|
+
query = query.strip()
|
|
10
|
+
|
|
11
|
+
# Format memory block
|
|
12
|
+
if memories:
|
|
13
|
+
memory_lines = "\n".join(f"- {m}" for m in memories)
|
|
14
|
+
|
|
15
|
+
context_block = f"""You are an AI assistant with memory.
|
|
16
|
+
|
|
17
|
+
Relevant context:
|
|
18
|
+
{memory_lines}
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
else:
|
|
22
|
+
context_block = "You are an AI assistant.\n\n"
|
|
23
|
+
|
|
24
|
+
# Final prompt
|
|
25
|
+
prompt = f"""{context_block}User:
|
|
26
|
+
{query}
|
|
27
|
+
|
|
28
|
+
Assistant:"""
|
|
29
|
+
|
|
30
|
+
return prompt
|