trace-memory 1.0.0__py3-none-any.whl
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.
- trace_memory/__init__.py +43 -0
- trace_memory/_llm_utils.py +139 -0
- trace_memory/ctree.py +1187 -0
- trace_memory/prompt_synthesizer.py +256 -0
- trace_memory/vector_db.py +191 -0
- trace_memory-1.0.0.dist-info/METADATA +715 -0
- trace_memory-1.0.0.dist-info/RECORD +11 -0
- trace_memory-1.0.0.dist-info/WHEEL +5 -0
- trace_memory-1.0.0.dist-info/licenses/LICENSE +202 -0
- trace_memory-1.0.0.dist-info/licenses/NOTICE +8 -0
- trace_memory-1.0.0.dist-info/top_level.txt +1 -0
trace_memory/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TRACE — Temporal Retrieval And Context Engine
|
|
3
|
+
==============================================
|
|
4
|
+
A self-healing, hierarchical memory engine for long-running LLM agents.
|
|
5
|
+
|
|
6
|
+
Public surface:
|
|
7
|
+
CTree — the hierarchical B+Tree conversation store
|
|
8
|
+
TopicNode — a topic branch node in the tree
|
|
9
|
+
MessageNode — a leaf node holding a single user/assistant exchange
|
|
10
|
+
Node — base node class
|
|
11
|
+
VectorDatabase — local SQLite vector store for semantic RAG
|
|
12
|
+
ConversationVector — a conversation message with its embedding
|
|
13
|
+
PromptSynthesizer — assembles the full RAG-enriched system prompt
|
|
14
|
+
|
|
15
|
+
Quick start:
|
|
16
|
+
from trace_memory import CTree, VectorDatabase, PromptSynthesizer
|
|
17
|
+
|
|
18
|
+
tree = CTree(api_key="...", model="gpt-4o-mini")
|
|
19
|
+
vdb = VectorDatabase("memory.db")
|
|
20
|
+
tree.vdb = vdb
|
|
21
|
+
tree.synthesizer = PromptSynthesizer(tree, vdb)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .ctree import CTree, Node, TopicNode, MessageNode
|
|
25
|
+
from .vector_db import VectorDatabase, ConversationVector
|
|
26
|
+
from .prompt_synthesizer import PromptSynthesizer
|
|
27
|
+
|
|
28
|
+
__version__ = "1.0.0"
|
|
29
|
+
__author__ = "Husain"
|
|
30
|
+
__license__ = "Apache-2.0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Tree
|
|
34
|
+
"CTree",
|
|
35
|
+
"Node",
|
|
36
|
+
"TopicNode",
|
|
37
|
+
"MessageNode",
|
|
38
|
+
# Vector store
|
|
39
|
+
"VectorDatabase",
|
|
40
|
+
"ConversationVector",
|
|
41
|
+
# Prompt synthesis
|
|
42
|
+
"PromptSynthesizer",
|
|
43
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal LLM helper utilities for TRACE.
|
|
3
|
+
|
|
4
|
+
Users should never import from this module directly.
|
|
5
|
+
Configure the LLM endpoint via environment variables:
|
|
6
|
+
|
|
7
|
+
OPENAI_BASE_URL — defaults to http://127.0.0.1:1234/v1 (LM Studio)
|
|
8
|
+
OPENAI_API_KEY — defaults to "lm-studio"
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import openai
|
|
12
|
+
import time
|
|
13
|
+
import os
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── LLM Call ──────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
def ChatGPT_API(
|
|
20
|
+
model: str,
|
|
21
|
+
prompt: str,
|
|
22
|
+
api_key: str = None,
|
|
23
|
+
chat_history: list = None,
|
|
24
|
+
temperature: float = 0,
|
|
25
|
+
max_tokens: int = None,
|
|
26
|
+
) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Send a prompt to any OpenAI-compatible chat endpoint and return the
|
|
29
|
+
response text. Retries up to 10 times on transient errors.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
model : Model ID string (e.g. "gpt-4o-mini", "meta-llama-3.1-8b").
|
|
34
|
+
prompt : The user message to send.
|
|
35
|
+
api_key : API key; falls back to OPENAI_API_KEY env var.
|
|
36
|
+
chat_history : Optional list of prior messages (dicts with role/content).
|
|
37
|
+
temperature : Sampling temperature (0 = deterministic).
|
|
38
|
+
max_tokens : Hard cap on response tokens; None = model default.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
str — Response text, or "Error" after all retries are exhausted.
|
|
43
|
+
"""
|
|
44
|
+
max_retries = 10
|
|
45
|
+
base_url = os.getenv("OPENAI_BASE_URL", "http://127.0.0.1:1234/v1")
|
|
46
|
+
api_key_to_use = api_key or os.getenv("OPENAI_API_KEY") or "lm-studio"
|
|
47
|
+
|
|
48
|
+
client = openai.OpenAI(api_key=api_key_to_use, base_url=base_url)
|
|
49
|
+
|
|
50
|
+
for i in range(max_retries):
|
|
51
|
+
try:
|
|
52
|
+
if chat_history is not None:
|
|
53
|
+
messages = list(chat_history)
|
|
54
|
+
messages.append({"role": "user", "content": prompt})
|
|
55
|
+
else:
|
|
56
|
+
messages = [{"role": "user", "content": prompt}]
|
|
57
|
+
|
|
58
|
+
api_kwargs = {"model": model, "messages": messages, "temperature": temperature}
|
|
59
|
+
if max_tokens is not None:
|
|
60
|
+
api_kwargs["max_tokens"] = max_tokens
|
|
61
|
+
|
|
62
|
+
response = client.chat.completions.create(**api_kwargs)
|
|
63
|
+
content = response.choices[0].message.content
|
|
64
|
+
return content if content is not None else ""
|
|
65
|
+
except Exception:
|
|
66
|
+
if i < max_retries - 1:
|
|
67
|
+
time.sleep(1)
|
|
68
|
+
else:
|
|
69
|
+
return "Error"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── JSON Extraction ────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def _normalize_for_json(text: str) -> str:
|
|
75
|
+
"""Normalise common LLM JSON formatting quirks before parsing."""
|
|
76
|
+
text = text.replace(": N/A", ": null")
|
|
77
|
+
text = text.replace(": True", ": true").replace(": False", ": false").replace(": None", ": null")
|
|
78
|
+
text = text.replace(": True,", ": true,").replace(": False,", ": false,").replace(": None,", ": null,")
|
|
79
|
+
text = text.replace(",}", "}").replace(",]", "]")
|
|
80
|
+
text = text.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
|
|
81
|
+
text = " ".join(text.split())
|
|
82
|
+
return text
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _try_parse(text: str):
|
|
86
|
+
return json.loads(_normalize_for_json(text))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def extract_json(content: str):
|
|
90
|
+
"""
|
|
91
|
+
Robustly extract a JSON object or array from an LLM response string.
|
|
92
|
+
|
|
93
|
+
Tries, in order:
|
|
94
|
+
1. Content inside ```json … ``` fences.
|
|
95
|
+
2. Content inside generic ``` … ``` fences.
|
|
96
|
+
3. First { … } block.
|
|
97
|
+
4. First [ … ] block.
|
|
98
|
+
5. The raw string as-is.
|
|
99
|
+
|
|
100
|
+
Returns the parsed Python object, or {} if nothing parses.
|
|
101
|
+
"""
|
|
102
|
+
if content is None:
|
|
103
|
+
return {}
|
|
104
|
+
|
|
105
|
+
candidates = []
|
|
106
|
+
|
|
107
|
+
json_start = content.find("```json")
|
|
108
|
+
if json_start != -1:
|
|
109
|
+
json_end = content.find("```", json_start + 7)
|
|
110
|
+
if json_end != -1:
|
|
111
|
+
candidates.append(content[json_start + 7 : json_end].strip())
|
|
112
|
+
|
|
113
|
+
generic_start = content.find("```")
|
|
114
|
+
if generic_start != -1:
|
|
115
|
+
generic_end = content.find("```", generic_start + 3)
|
|
116
|
+
if generic_end != -1:
|
|
117
|
+
candidates.append(content[generic_start + 3 : generic_end].strip())
|
|
118
|
+
|
|
119
|
+
brace_start = content.find("{")
|
|
120
|
+
brace_end = content.rfind("}")
|
|
121
|
+
if brace_start != -1 and brace_end > brace_start:
|
|
122
|
+
candidates.append(content[brace_start : brace_end + 1])
|
|
123
|
+
|
|
124
|
+
bracket_start = content.find("[")
|
|
125
|
+
bracket_end = content.rfind("]")
|
|
126
|
+
if bracket_start != -1 and bracket_end > bracket_start:
|
|
127
|
+
candidates.append(content[bracket_start : bracket_end + 1])
|
|
128
|
+
|
|
129
|
+
candidates.append(content.strip())
|
|
130
|
+
|
|
131
|
+
for candidate in candidates:
|
|
132
|
+
if not candidate:
|
|
133
|
+
continue
|
|
134
|
+
try:
|
|
135
|
+
return _try_parse(candidate)
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
return {}
|