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.
@@ -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 {}