deepcq 0.1.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.
- deepcq/__init__.py +37 -0
- deepcq/core.py +126 -0
- deepcq/integrations/__init__.py +0 -0
- deepcq/integrations/ternary/__init__.py +26 -0
- deepcq/integrations/ternary/adapters.py +143 -0
- deepcq/integrations/ternary/prompt_library.py +170 -0
- deepcq/intent.py +51 -0
- deepcq/memory/__init__.py +28 -0
- deepcq/memory/session.py +62 -0
- deepcq/memory/state.py +37 -0
- deepcq/memory/store.py +82 -0
- deepcq/regex_gen.py +22 -0
- deepcq/result.py +19 -0
- deepcq/schema.py +62 -0
- deepcq/tokens.py +85 -0
- deepcq-0.1.0.dist-info/METADATA +220 -0
- deepcq-0.1.0.dist-info/RECORD +20 -0
- deepcq-0.1.0.dist-info/WHEEL +5 -0
- deepcq-0.1.0.dist-info/licenses/LICENSE +21 -0
- deepcq-0.1.0.dist-info/top_level.txt +1 -0
deepcq/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
_EXPORTS = {
|
|
8
|
+
"DeepCQ": ("deepcq.core", "DeepCQ"),
|
|
9
|
+
"MemorySchema": ("deepcq.schema", "MemorySchema"),
|
|
10
|
+
"QueryResult": ("deepcq.result", "QueryResult"),
|
|
11
|
+
"OptimizationMetrics": ("deepcq.result", "OptimizationMetrics"),
|
|
12
|
+
"AppState": ("deepcq.memory.state", "AppState"),
|
|
13
|
+
"SessionCache": ("deepcq.memory.session", "SessionCache"),
|
|
14
|
+
"History": ("deepcq.memory.session", "History"),
|
|
15
|
+
"LongTermStore": ("deepcq.memory.store", "LongTermStore"),
|
|
16
|
+
"ReferenceData": ("deepcq.memory.store", "ReferenceData"),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DeepCQ", "MemorySchema", "QueryResult", "OptimizationMetrics",
|
|
21
|
+
"AppState", "SessionCache", "History", "LongTermStore", "ReferenceData",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def __getattr__(name: str):
|
|
26
|
+
try:
|
|
27
|
+
module_name, attribute_name = _EXPORTS[name]
|
|
28
|
+
except KeyError as exc:
|
|
29
|
+
raise AttributeError(name) from None
|
|
30
|
+
module = import_module(module_name)
|
|
31
|
+
value = getattr(module, attribute_name)
|
|
32
|
+
globals()[name] = value
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def __dir__() -> list[str]:
|
|
37
|
+
return sorted(set(globals()) | set(__all__))
|
deepcq/core.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""DeepCQ — main orchestrator."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .schema import MemorySchema
|
|
5
|
+
from .intent import score_sections
|
|
6
|
+
from .regex_gen import build_content_pattern, content_pattern_string
|
|
7
|
+
from .result import QueryResult, OptimizationMetrics
|
|
8
|
+
from .memory.state import AppState
|
|
9
|
+
from .memory.session import SessionCache, History
|
|
10
|
+
from .memory.store import LongTermStore, ReferenceData
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DeepCQ:
|
|
14
|
+
"""
|
|
15
|
+
Deep Context Query.
|
|
16
|
+
|
|
17
|
+
Manages five memory tiers and queries them dynamically using regex
|
|
18
|
+
patterns generated from the user's query. No domain knowledge is
|
|
19
|
+
baked in — the schema declares section names only.
|
|
20
|
+
|
|
21
|
+
Example::
|
|
22
|
+
|
|
23
|
+
from deepcq import DeepCQ, MemorySchema
|
|
24
|
+
|
|
25
|
+
schema = MemorySchema(sections=["status", "errors", "metrics"])
|
|
26
|
+
cq = DeepCQ(schema=schema)
|
|
27
|
+
|
|
28
|
+
cq.state.set("errors", "[errors]\\n- OOM at 02:15\\n")
|
|
29
|
+
cq.state.set("metrics", "[metrics]\\nCPU: 78%\\nMemory: 4.2GB\\n")
|
|
30
|
+
cq.state.set("status", "[status]\\nAll services running\\n")
|
|
31
|
+
|
|
32
|
+
result = cq.query("any memory or cpu spikes?")
|
|
33
|
+
print(result.content)
|
|
34
|
+
print(f"Reduction: {result.metrics.reduction_pct:.1f}%")
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
schema: MemorySchema,
|
|
40
|
+
store_path: str | None = None,
|
|
41
|
+
reference_path: str | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.schema = schema
|
|
44
|
+
self.state = AppState()
|
|
45
|
+
self.cache = SessionCache()
|
|
46
|
+
self.history = History()
|
|
47
|
+
self.notes = LongTermStore(store_path)
|
|
48
|
+
self.reference = ReferenceData(reference_path)
|
|
49
|
+
|
|
50
|
+
def query(self, query: str) -> QueryResult:
|
|
51
|
+
"""
|
|
52
|
+
Query all memory tiers and return only content relevant to the query.
|
|
53
|
+
"""
|
|
54
|
+
tier_texts: dict[str, str] = {
|
|
55
|
+
"state": self.state.as_text(),
|
|
56
|
+
"cache": self.cache.as_text(),
|
|
57
|
+
"history": self.history.as_text(),
|
|
58
|
+
"notes": self.notes.as_text(),
|
|
59
|
+
"reference": self.reference.as_text(),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
full_text = "\n".join(v for v in tier_texts.values() if v)
|
|
63
|
+
original_chars = len(full_text)
|
|
64
|
+
|
|
65
|
+
intent = score_sections(query, self.schema, tier_texts)
|
|
66
|
+
|
|
67
|
+
included: list[str] = []
|
|
68
|
+
excluded: list[str] = []
|
|
69
|
+
for section in self.schema.sections:
|
|
70
|
+
if intent.get(section, 0.0) >= self.schema.confidence_threshold:
|
|
71
|
+
included.append(section)
|
|
72
|
+
else:
|
|
73
|
+
excluded.append(section)
|
|
74
|
+
|
|
75
|
+
if not included and self.schema.fallback_include_all:
|
|
76
|
+
return QueryResult(
|
|
77
|
+
content=full_text,
|
|
78
|
+
intent=intent,
|
|
79
|
+
sections_included=list(self.schema.sections),
|
|
80
|
+
sections_excluded=[],
|
|
81
|
+
content_pattern=content_pattern_string(query),
|
|
82
|
+
metrics=OptimizationMetrics(
|
|
83
|
+
original_chars=original_chars,
|
|
84
|
+
filtered_chars=original_chars,
|
|
85
|
+
reduction_pct=0.0,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
parts: list[str] = []
|
|
90
|
+
|
|
91
|
+
for section in included:
|
|
92
|
+
pat = self.schema.extraction_pattern(section)
|
|
93
|
+
for tier_name in ("state", "notes", "reference"):
|
|
94
|
+
text = tier_texts[tier_name]
|
|
95
|
+
if not text:
|
|
96
|
+
continue
|
|
97
|
+
m = pat.search(text)
|
|
98
|
+
if m:
|
|
99
|
+
parts.append(m.group(0).strip())
|
|
100
|
+
|
|
101
|
+
content_pat = build_content_pattern(query)
|
|
102
|
+
for tier_name in ("cache", "history"):
|
|
103
|
+
text = tier_texts[tier_name]
|
|
104
|
+
if text and content_pat.search(text):
|
|
105
|
+
parts.append(text.strip())
|
|
106
|
+
|
|
107
|
+
content = "\n\n".join(p for p in parts if p)
|
|
108
|
+
filtered_chars = len(content)
|
|
109
|
+
reduction_pct = (
|
|
110
|
+
round((1.0 - filtered_chars / original_chars) * 100, 1)
|
|
111
|
+
if original_chars > 0
|
|
112
|
+
else 0.0
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return QueryResult(
|
|
116
|
+
content=content,
|
|
117
|
+
intent=intent,
|
|
118
|
+
sections_included=included,
|
|
119
|
+
sections_excluded=excluded,
|
|
120
|
+
content_pattern=content_pattern_string(query),
|
|
121
|
+
metrics=OptimizationMetrics(
|
|
122
|
+
original_chars=original_chars,
|
|
123
|
+
filtered_chars=filtered_chars,
|
|
124
|
+
reduction_pct=reduction_pct,
|
|
125
|
+
),
|
|
126
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
|
|
5
|
+
_EXPORTS = {
|
|
6
|
+
"PromptLibrary": ("deepcq.integrations.ternary.prompt_library", "PromptLibrary"),
|
|
7
|
+
"ToolContextProvider": ("deepcq.integrations.ternary.adapters", "ToolContextProvider"),
|
|
8
|
+
"LangChainMemoryAdapter": ("deepcq.integrations.ternary.adapters", "LangChainMemoryAdapter"),
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
__all__ = ["PromptLibrary", "ToolContextProvider", "LangChainMemoryAdapter"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __getattr__(name: str):
|
|
15
|
+
try:
|
|
16
|
+
module_name, attribute_name = _EXPORTS[name]
|
|
17
|
+
except KeyError:
|
|
18
|
+
raise AttributeError(name) from None
|
|
19
|
+
module = import_module(module_name)
|
|
20
|
+
value = getattr(module, attribute_name)
|
|
21
|
+
globals()[name] = value
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def __dir__() -> list[str]:
|
|
26
|
+
return sorted(set(globals()) | set(__all__))
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapters making DeepCQ usable as a memory/context backend for agent frameworks.
|
|
3
|
+
|
|
4
|
+
ToolContextProvider — formats QueryResult as a prompt-injectable context string.
|
|
5
|
+
LangChainMemoryAdapter — duck-types LangChain's BaseChatMemory interface.
|
|
6
|
+
|
|
7
|
+
Neither adapter imports LangChain or any other framework. They implement the
|
|
8
|
+
interfaces by duck-typing so they work without installing any extras.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from deepcq import DeepCQ
|
|
13
|
+
from .prompt_library import PromptLibrary
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolContextProvider:
|
|
17
|
+
"""
|
|
18
|
+
Formats DeepCQ query results as context strings for LLM prompts.
|
|
19
|
+
|
|
20
|
+
Matches the pattern used in ternary-ai/agents OpenAI Financial
|
|
21
|
+
Researcher Agent notebook: retrieve relevant context, inject into
|
|
22
|
+
the system prompt or tool schema before calling the LLM.
|
|
23
|
+
|
|
24
|
+
Example::
|
|
25
|
+
|
|
26
|
+
cq = DeepCQ(schema=schema)
|
|
27
|
+
provider = ToolContextProvider(cq)
|
|
28
|
+
|
|
29
|
+
# Before each LLM call:
|
|
30
|
+
context = provider.get_context("What is portfolio concentration risk?")
|
|
31
|
+
prompt = f"Use this context:\\n{context}\\n\\nQuestion: ..."
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, cq: DeepCQ) -> None:
|
|
35
|
+
self._cq = cq
|
|
36
|
+
|
|
37
|
+
def get_context(self, query: str) -> str:
|
|
38
|
+
"""Return filtered memory content relevant to the query."""
|
|
39
|
+
return self._cq.query(query).content
|
|
40
|
+
|
|
41
|
+
def get_prompt_template(self, query: str, library: PromptLibrary) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Retrieve the best matching prompt template(s) from a PromptLibrary
|
|
44
|
+
for the given query. Returns the raw template text with {variable}
|
|
45
|
+
placeholders intact — the caller fills them in.
|
|
46
|
+
"""
|
|
47
|
+
return library.query(query).content
|
|
48
|
+
|
|
49
|
+
def get_context_with_template(
|
|
50
|
+
self, query: str, library: PromptLibrary
|
|
51
|
+
) -> tuple[str, str]:
|
|
52
|
+
"""
|
|
53
|
+
Convenience: return (memory_context, prompt_template) together.
|
|
54
|
+
|
|
55
|
+
Useful when you want to inject both retrieved state and a prompt
|
|
56
|
+
template into an LLM call in one step.
|
|
57
|
+
"""
|
|
58
|
+
return self.get_context(query), self.get_prompt_template(query, library)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LangChainMemoryAdapter:
|
|
62
|
+
"""
|
|
63
|
+
Makes DeepCQ usable as a LangChain chat memory component.
|
|
64
|
+
|
|
65
|
+
Duck-types the LangChain BaseChatMemory interface (save_context /
|
|
66
|
+
load_memory_variables / clear) without importing LangChain itself.
|
|
67
|
+
Drop it into any LangChain chain that accepts a memory= argument.
|
|
68
|
+
|
|
69
|
+
The adapter stores conversation turns in DeepCQ's History tier (T3)
|
|
70
|
+
and caches tool results in the SessionCache tier (T2). On each call to
|
|
71
|
+
load_memory_variables, it runs a deepcq query against the incoming
|
|
72
|
+
message to return only the most relevant prior context.
|
|
73
|
+
|
|
74
|
+
Example (LangChain)::
|
|
75
|
+
|
|
76
|
+
cq = DeepCQ(schema=schema)
|
|
77
|
+
memory = LangChainMemoryAdapter(cq)
|
|
78
|
+
|
|
79
|
+
chain = ConversationalRetrievalChain(
|
|
80
|
+
llm=llm,
|
|
81
|
+
retriever=retriever,
|
|
82
|
+
memory=memory,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
Example (manual / OpenAI Agents SDK)::
|
|
86
|
+
|
|
87
|
+
memory.save_context({"input": user_msg}, {"output": agent_reply})
|
|
88
|
+
ctx = memory.load_memory_variables({"input": next_user_msg})
|
|
89
|
+
print(ctx["history"]) # filtered relevant context
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
memory_key: str = "history"
|
|
93
|
+
|
|
94
|
+
def __init__(self, cq: DeepCQ) -> None:
|
|
95
|
+
self._cq = cq
|
|
96
|
+
|
|
97
|
+
def save_context(self, inputs: dict, outputs: dict) -> None:
|
|
98
|
+
"""Store one conversation turn (human + AI) in DeepCQ History."""
|
|
99
|
+
human = inputs.get("input") or inputs.get("human_input") or ""
|
|
100
|
+
ai = outputs.get("output") or outputs.get("response") or ""
|
|
101
|
+
if human:
|
|
102
|
+
self._cq.history.add("human", human)
|
|
103
|
+
if ai:
|
|
104
|
+
self._cq.history.add("ai", ai)
|
|
105
|
+
|
|
106
|
+
def load_memory_variables(self, inputs: dict) -> dict[str, str]:
|
|
107
|
+
"""
|
|
108
|
+
Return filtered context relevant to the incoming message.
|
|
109
|
+
|
|
110
|
+
LangChain calls this before each LLM invocation. The returned dict
|
|
111
|
+
must have the key matching memory_key (default "history").
|
|
112
|
+
"""
|
|
113
|
+
query = inputs.get("input") or inputs.get("human_input") or ""
|
|
114
|
+
result = self._cq.query(query) if query else None
|
|
115
|
+
content = result.content if result else self._cq.history.as_text()
|
|
116
|
+
return {self.memory_key: content}
|
|
117
|
+
|
|
118
|
+
def clear(self) -> None:
|
|
119
|
+
"""Reset History and SessionCache (analogous to LangChain memory.clear())."""
|
|
120
|
+
self._cq.history.clear()
|
|
121
|
+
self._cq.cache.clear()
|
|
122
|
+
|
|
123
|
+
# LangChain also accesses these attributes on memory objects
|
|
124
|
+
@property
|
|
125
|
+
def chat_memory(self):
|
|
126
|
+
"""Compatibility shim — returns an object with a messages property."""
|
|
127
|
+
turns = self._cq.history.turns()
|
|
128
|
+
|
|
129
|
+
class _Messages:
|
|
130
|
+
def __init__(self, turns):
|
|
131
|
+
self.messages = [
|
|
132
|
+
{"role": t.role, "content": t.text} for t in turns
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
return _Messages(turns)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def output_key(self) -> str:
|
|
139
|
+
return "output"
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def input_key(self) -> str:
|
|
143
|
+
return "input"
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PromptLibrary — loads ternary-ai/prompts CSVs into queryable deepcq memory.
|
|
3
|
+
|
|
4
|
+
Each CSV file becomes one schema section. Each non-empty line in the CSV
|
|
5
|
+
becomes one prompt entry in that section. Prompt templates may contain
|
|
6
|
+
{variable} placeholders — these are stored and returned verbatim.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from deepcq.integrations.ternary import PromptLibrary
|
|
11
|
+
|
|
12
|
+
# From a local clone of ternary-ai/prompts:
|
|
13
|
+
lib = PromptLibrary.from_local("/path/to/ternary-ai/prompts/")
|
|
14
|
+
|
|
15
|
+
# From raw GitHub URLs (uses urllib, no extra deps):
|
|
16
|
+
lib = PromptLibrary.from_urls(PromptLibrary.DEFAULT_URLS)
|
|
17
|
+
|
|
18
|
+
result = lib.query("portfolio concentration risk")
|
|
19
|
+
print(result.sections_included) # e.g. ["risk-management", "portfolio"]
|
|
20
|
+
print(result.content) # matching prompt templates
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import csv
|
|
25
|
+
import io
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Iterable
|
|
28
|
+
|
|
29
|
+
from deepcq import DeepCQ, MemorySchema, QueryResult
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_BASE_URL = "https://raw.githubusercontent.com/ternary-ai/prompts/main/"
|
|
33
|
+
|
|
34
|
+
SECTIONS = [
|
|
35
|
+
"agents",
|
|
36
|
+
"analysis",
|
|
37
|
+
"concepts",
|
|
38
|
+
"content",
|
|
39
|
+
"journal",
|
|
40
|
+
"knowledgegraphs",
|
|
41
|
+
"named-entities",
|
|
42
|
+
"portfolio",
|
|
43
|
+
"research",
|
|
44
|
+
"risk-management",
|
|
45
|
+
"scenario-analysis",
|
|
46
|
+
"summarization",
|
|
47
|
+
"trading",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
DEFAULT_URLS: dict[str, str] = {s: f"{_BASE_URL}{s}.csv" for s in SECTIONS}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PromptLibrary:
|
|
54
|
+
"""
|
|
55
|
+
Queryable library of ternary-ai financial prompt templates.
|
|
56
|
+
|
|
57
|
+
Wraps DeepCQ with a schema whose sections map 1-to-1 to the CSV files
|
|
58
|
+
in the ternary-ai/prompts repository. Querying returns only the
|
|
59
|
+
sections whose stored prompts are most relevant to the user's query.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
SECTIONS = SECTIONS
|
|
63
|
+
DEFAULT_URLS = DEFAULT_URLS
|
|
64
|
+
|
|
65
|
+
def __init__(self) -> None:
|
|
66
|
+
schema = MemorySchema(
|
|
67
|
+
sections=self.SECTIONS,
|
|
68
|
+
section_header_format="[{name}]",
|
|
69
|
+
confidence_threshold=0.25, # broader recall for prompt retrieval
|
|
70
|
+
fallback_include_all=False, # don't dump everything on vague queries
|
|
71
|
+
)
|
|
72
|
+
self._cq = DeepCQ(schema=schema)
|
|
73
|
+
self._loaded: set[str] = set()
|
|
74
|
+
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
# Loading helpers
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def load_section(self, section: str, rows: Iterable[str]) -> None:
|
|
80
|
+
"""Store a list of prompt strings into the named section."""
|
|
81
|
+
lines = [r.strip() for r in rows if r.strip()]
|
|
82
|
+
if not lines:
|
|
83
|
+
return
|
|
84
|
+
body = "\n".join(lines)
|
|
85
|
+
self._cq.state.set(section, f"[{section}]\n{body}\n")
|
|
86
|
+
self._loaded.add(section)
|
|
87
|
+
|
|
88
|
+
def load_text(self, section: str, raw: str) -> None:
|
|
89
|
+
"""Parse raw CSV text (one prompt per line) and load into section."""
|
|
90
|
+
reader = csv.reader(io.StringIO(raw))
|
|
91
|
+
prompts: list[str] = []
|
|
92
|
+
for row in reader:
|
|
93
|
+
# Each row may be a single cell or bare line
|
|
94
|
+
text = " ".join(row).strip()
|
|
95
|
+
if text:
|
|
96
|
+
prompts.append(text)
|
|
97
|
+
self.load_section(section, prompts)
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# Factory methods
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_local(cls, directory: str | Path) -> "PromptLibrary":
|
|
105
|
+
"""
|
|
106
|
+
Load all CSV files from a local clone of ternary-ai/prompts.
|
|
107
|
+
|
|
108
|
+
Only files whose stem matches a known section name are loaded.
|
|
109
|
+
"""
|
|
110
|
+
lib = cls()
|
|
111
|
+
base = Path(directory)
|
|
112
|
+
for section in cls.SECTIONS:
|
|
113
|
+
path = base / f"{section}.csv"
|
|
114
|
+
if path.exists():
|
|
115
|
+
lib.load_text(section, path.read_text(encoding="utf-8"))
|
|
116
|
+
return lib
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_urls(
|
|
120
|
+
cls,
|
|
121
|
+
url_map: dict[str, str] | None = None,
|
|
122
|
+
timeout: int = 10,
|
|
123
|
+
) -> "PromptLibrary":
|
|
124
|
+
"""
|
|
125
|
+
Fetch CSV files from raw GitHub URLs using urllib (stdlib only).
|
|
126
|
+
|
|
127
|
+
Defaults to ternary-ai/prompts on GitHub. Pass a custom url_map to
|
|
128
|
+
override individual sections or point to a fork.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
url_map: section_name → raw URL. Defaults to DEFAULT_URLS.
|
|
132
|
+
timeout: HTTP request timeout in seconds.
|
|
133
|
+
"""
|
|
134
|
+
import urllib.request
|
|
135
|
+
from urllib.parse import urlparse
|
|
136
|
+
|
|
137
|
+
lib = cls()
|
|
138
|
+
urls = url_map if url_map is not None else cls.DEFAULT_URLS
|
|
139
|
+
|
|
140
|
+
for section, url in urls.items():
|
|
141
|
+
if urlparse(url).scheme not in ("http", "https"):
|
|
142
|
+
print(
|
|
143
|
+
f"[deepcq] Warning: refusing to load '{section}' from {url}: "
|
|
144
|
+
"only http/https URLs are allowed"
|
|
145
|
+
)
|
|
146
|
+
continue
|
|
147
|
+
try:
|
|
148
|
+
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
149
|
+
raw = resp.read().decode("utf-8")
|
|
150
|
+
lib.load_text(section, raw)
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
# Non-fatal: skip sections that fail to fetch
|
|
153
|
+
print(f"[deepcq] Warning: could not load '{section}' from {url}: {exc}")
|
|
154
|
+
|
|
155
|
+
return lib
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
# Query interface
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def query(self, text: str) -> QueryResult:
|
|
162
|
+
"""Return the prompt templates most relevant to the query."""
|
|
163
|
+
return self._cq.query(text)
|
|
164
|
+
|
|
165
|
+
def loaded_sections(self) -> list[str]:
|
|
166
|
+
"""Sections that have been successfully loaded."""
|
|
167
|
+
return sorted(self._loaded)
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
return f"PromptLibrary(loaded={self.loaded_sections()})"
|
deepcq/intent.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Two-phase section relevance scorer."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from .schema import MemorySchema
|
|
4
|
+
from .tokens import tokenize
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def score_sections(
|
|
8
|
+
query: str,
|
|
9
|
+
schema: MemorySchema,
|
|
10
|
+
tier_texts: dict[str, str],
|
|
11
|
+
) -> dict[str, float]:
|
|
12
|
+
"""
|
|
13
|
+
Score each schema section for relevance to the query.
|
|
14
|
+
|
|
15
|
+
For each section we build a vocabulary from three sources:
|
|
16
|
+
1. The section name tokens (and optional schema keywords).
|
|
17
|
+
2. The tokenized content of that section's stored text.
|
|
18
|
+
|
|
19
|
+
Score = proportion of unique query tokens found in the section vocabulary.
|
|
20
|
+
|
|
21
|
+
This is query-term-first: the patterns come from the query itself —
|
|
22
|
+
no keyword lists are required. Optional schema keywords extend the
|
|
23
|
+
vocabulary for sections whose names are abbreviations or jargon.
|
|
24
|
+
"""
|
|
25
|
+
query_tokens = set(tokenize(query, include_stems=True))
|
|
26
|
+
if not query_tokens:
|
|
27
|
+
return {s: 0.0 for s in schema.sections}
|
|
28
|
+
|
|
29
|
+
scores: dict[str, float] = {}
|
|
30
|
+
|
|
31
|
+
for section in schema.sections:
|
|
32
|
+
vocab: set[str] = set()
|
|
33
|
+
|
|
34
|
+
vocab.update(tokenize(section, include_stems=True))
|
|
35
|
+
|
|
36
|
+
for kw in schema.keywords.get(section, []):
|
|
37
|
+
vocab.update(tokenize(kw, include_stems=True))
|
|
38
|
+
|
|
39
|
+
pat = schema.extraction_pattern(section)
|
|
40
|
+
for text in tier_texts.values():
|
|
41
|
+
if not text:
|
|
42
|
+
continue
|
|
43
|
+
m = pat.search(text)
|
|
44
|
+
if m:
|
|
45
|
+
vocab.update(tokenize(m.group(0), include_stems=True))
|
|
46
|
+
|
|
47
|
+
matched = query_tokens & vocab
|
|
48
|
+
score = round(len(matched) / len(query_tokens), 4)
|
|
49
|
+
scores[section] = score
|
|
50
|
+
|
|
51
|
+
return scores
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
|
|
5
|
+
_EXPORTS = {
|
|
6
|
+
"AppState": ("deepcq.memory.state", "AppState"),
|
|
7
|
+
"SessionCache": ("deepcq.memory.session", "SessionCache"),
|
|
8
|
+
"History": ("deepcq.memory.session", "History"),
|
|
9
|
+
"LongTermStore": ("deepcq.memory.store", "LongTermStore"),
|
|
10
|
+
"ReferenceData": ("deepcq.memory.store", "ReferenceData"),
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
__all__ = ["AppState", "SessionCache", "History", "LongTermStore", "ReferenceData"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def __getattr__(name: str):
|
|
17
|
+
try:
|
|
18
|
+
module_name, attribute_name = _EXPORTS[name]
|
|
19
|
+
except KeyError:
|
|
20
|
+
raise AttributeError(name) from None
|
|
21
|
+
module = import_module(module_name)
|
|
22
|
+
value = getattr(module, attribute_name)
|
|
23
|
+
globals()[name] = value
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __dir__() -> list[str]:
|
|
28
|
+
return sorted(set(globals()) | set(__all__))
|
deepcq/memory/session.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""T2 — SessionCache and T3 — History."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from collections import deque
|
|
4
|
+
from typing import NamedTuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SessionCache:
|
|
8
|
+
"""T2 — Ephemeral per-session cache. Deduplicates repeated (key, value) puts."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, max_entries: int = 30) -> None:
|
|
11
|
+
self._max = max_entries
|
|
12
|
+
self._entries: dict[str, str] = {}
|
|
13
|
+
self._order: list[str] = []
|
|
14
|
+
|
|
15
|
+
def put(self, key: str, value: str) -> None:
|
|
16
|
+
if self._entries.get(key) == value:
|
|
17
|
+
return
|
|
18
|
+
if key in self._order:
|
|
19
|
+
self._order.remove(key)
|
|
20
|
+
self._entries[key] = value
|
|
21
|
+
self._order.append(key)
|
|
22
|
+
while len(self._order) > self._max:
|
|
23
|
+
evicted = self._order.pop(0)
|
|
24
|
+
self._entries.pop(evicted, None)
|
|
25
|
+
|
|
26
|
+
def get(self, key: str) -> str | None:
|
|
27
|
+
return self._entries.get(key)
|
|
28
|
+
|
|
29
|
+
def as_text(self) -> str:
|
|
30
|
+
if not self._entries:
|
|
31
|
+
return ""
|
|
32
|
+
return "\n".join(f"[cache:{k}]\n{v}" for k, v in self._entries.items())
|
|
33
|
+
|
|
34
|
+
def clear(self) -> None:
|
|
35
|
+
self._entries.clear()
|
|
36
|
+
self._order.clear()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Turn(NamedTuple):
|
|
40
|
+
role: str
|
|
41
|
+
text: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class History:
|
|
45
|
+
"""T3 — Sliding conversation window. Default: 6 turns."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, max_turns: int = 6) -> None:
|
|
48
|
+
self._turns: deque[Turn] = deque(maxlen=max_turns)
|
|
49
|
+
|
|
50
|
+
def add(self, role: str, text: str) -> None:
|
|
51
|
+
self._turns.append(Turn(role=role, text=text))
|
|
52
|
+
|
|
53
|
+
def turns(self) -> list[Turn]:
|
|
54
|
+
return list(self._turns)
|
|
55
|
+
|
|
56
|
+
def as_text(self) -> str:
|
|
57
|
+
if not self._turns:
|
|
58
|
+
return ""
|
|
59
|
+
return "\n\n".join(f"{t.role.upper()}: {t.text}" for t in self._turns)
|
|
60
|
+
|
|
61
|
+
def clear(self) -> None:
|
|
62
|
+
self._turns.clear()
|
deepcq/memory/state.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""T1 — Live application state."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AppState:
|
|
6
|
+
"""
|
|
7
|
+
Primary memory tier: the current state of the running application.
|
|
8
|
+
|
|
9
|
+
Each section is stored as raw text that includes its own header, e.g.::
|
|
10
|
+
|
|
11
|
+
state.set("errors", "[errors]\\n- OOM at 02:15\\n")
|
|
12
|
+
state.set("metrics", "[metrics]\\nCPU: 78%\\n")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
self._sections: dict[str, str] = {}
|
|
17
|
+
|
|
18
|
+
def set(self, section: str, text: str) -> None:
|
|
19
|
+
self._sections[section] = text
|
|
20
|
+
|
|
21
|
+
def get(self, section: str) -> str:
|
|
22
|
+
return self._sections.get(section, "")
|
|
23
|
+
|
|
24
|
+
def remove(self, section: str) -> None:
|
|
25
|
+
self._sections.pop(section, None)
|
|
26
|
+
|
|
27
|
+
def sections(self) -> list[str]:
|
|
28
|
+
return list(self._sections.keys())
|
|
29
|
+
|
|
30
|
+
def as_text(self) -> str:
|
|
31
|
+
return "\n".join(self._sections.values())
|
|
32
|
+
|
|
33
|
+
def as_dict(self) -> dict[str, str]:
|
|
34
|
+
return dict(self._sections)
|
|
35
|
+
|
|
36
|
+
def clear(self) -> None:
|
|
37
|
+
self._sections.clear()
|
deepcq/memory/store.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""T4 — LongTermStore and T5 — ReferenceData."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LongTermStore:
|
|
8
|
+
"""T4 — Persistent, append-only notes store. File-backed when path given."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, path: str | Path | None = None) -> None:
|
|
11
|
+
self._path = Path(path) if path else None
|
|
12
|
+
self._data: dict[str, list[str]] = {}
|
|
13
|
+
if self._path and self._path.exists():
|
|
14
|
+
self._load()
|
|
15
|
+
|
|
16
|
+
def append(self, key: str, text: str) -> None:
|
|
17
|
+
self._data.setdefault(key, []).append(text)
|
|
18
|
+
if self._path:
|
|
19
|
+
self._save()
|
|
20
|
+
|
|
21
|
+
def get(self, key: str) -> list[str]:
|
|
22
|
+
return list(self._data.get(key, []))
|
|
23
|
+
|
|
24
|
+
def keys(self) -> list[str]:
|
|
25
|
+
return list(self._data.keys())
|
|
26
|
+
|
|
27
|
+
def as_text(self) -> str:
|
|
28
|
+
if not self._data:
|
|
29
|
+
return ""
|
|
30
|
+
parts: list[str] = []
|
|
31
|
+
for key, entries in self._data.items():
|
|
32
|
+
parts.append(f"[notes:{key}]")
|
|
33
|
+
parts.extend(entries)
|
|
34
|
+
return "\n".join(parts)
|
|
35
|
+
|
|
36
|
+
def _load(self) -> None:
|
|
37
|
+
try:
|
|
38
|
+
self._data = json.loads(self._path.read_text())
|
|
39
|
+
except (json.JSONDecodeError, OSError):
|
|
40
|
+
self._data = {}
|
|
41
|
+
|
|
42
|
+
def _save(self) -> None:
|
|
43
|
+
if self._path:
|
|
44
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
self._path.write_text(json.dumps(self._data, indent=2))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ReferenceData:
|
|
49
|
+
"""T5 — Lazy-loaded reference / configuration data. File-backed when path given."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, path: str | Path | None = None) -> None:
|
|
52
|
+
self._path = Path(path) if path else None
|
|
53
|
+
self._data: dict | None = None
|
|
54
|
+
|
|
55
|
+
def _ensure_loaded(self) -> None:
|
|
56
|
+
if self._data is None:
|
|
57
|
+
if self._path and self._path.exists():
|
|
58
|
+
try:
|
|
59
|
+
self._data = json.loads(self._path.read_text())
|
|
60
|
+
except (json.JSONDecodeError, OSError):
|
|
61
|
+
self._data = {}
|
|
62
|
+
else:
|
|
63
|
+
self._data = {}
|
|
64
|
+
|
|
65
|
+
def get(self, key: str, default=None):
|
|
66
|
+
self._ensure_loaded()
|
|
67
|
+
return self._data.get(key, default)
|
|
68
|
+
|
|
69
|
+
def set(self, key: str, value) -> None:
|
|
70
|
+
self._ensure_loaded()
|
|
71
|
+
self._data[key] = value
|
|
72
|
+
if self._path:
|
|
73
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
self._path.write_text(json.dumps(self._data, indent=2))
|
|
75
|
+
|
|
76
|
+
def as_text(self) -> str:
|
|
77
|
+
self._ensure_loaded()
|
|
78
|
+
if not self._data:
|
|
79
|
+
return ""
|
|
80
|
+
lines = ["[reference]"]
|
|
81
|
+
lines.extend(f"{k}: {v}" for k, v in self._data.items())
|
|
82
|
+
return "\n".join(lines)
|
deepcq/regex_gen.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Dynamic regex pattern builder from query tokens."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
import functools
|
|
5
|
+
from .tokens import tokenize
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@functools.lru_cache(maxsize=512)
|
|
9
|
+
def _compile_content_pattern(token_tuple: tuple[str, ...]) -> re.Pattern[str]:
|
|
10
|
+
if not token_tuple:
|
|
11
|
+
return re.compile(r"(?!)")
|
|
12
|
+
escaped = [re.escape(t) for t in sorted(token_tuple, key=len, reverse=True)]
|
|
13
|
+
return re.compile(r"\b(" + "|".join(escaped) + r")\b", re.IGNORECASE)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_content_pattern(query: str) -> re.Pattern[str]:
|
|
17
|
+
tokens = tuple(sorted(set(tokenize(query, include_stems=True))))
|
|
18
|
+
return _compile_content_pattern(tokens)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def content_pattern_string(query: str) -> str:
|
|
22
|
+
return build_content_pattern(query).pattern
|
deepcq/result.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class OptimizationMetrics:
|
|
7
|
+
original_chars: int
|
|
8
|
+
filtered_chars: int
|
|
9
|
+
reduction_pct: float
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class QueryResult:
|
|
14
|
+
content: str
|
|
15
|
+
intent: dict[str, float]
|
|
16
|
+
sections_included: list[str]
|
|
17
|
+
sections_excluded: list[str]
|
|
18
|
+
content_pattern: str
|
|
19
|
+
metrics: OptimizationMetrics
|
deepcq/schema.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""MemorySchema — defines section names and optional keyword overrides."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Sequence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class MemorySchema:
|
|
10
|
+
"""
|
|
11
|
+
Declares the logical sections that structure stored memory text.
|
|
12
|
+
|
|
13
|
+
sections:
|
|
14
|
+
List of section names, e.g. ["errors", "metrics", "status"].
|
|
15
|
+
No keyword lists required — deepcq derives patterns from the
|
|
16
|
+
query itself (query-term-first approach).
|
|
17
|
+
|
|
18
|
+
keywords:
|
|
19
|
+
Optional per-section extra terms that boost relevance scoring.
|
|
20
|
+
Useful when section names are abbreviations or jargon.
|
|
21
|
+
|
|
22
|
+
section_header_format:
|
|
23
|
+
How sections are delimited in stored text.
|
|
24
|
+
Default "[{name}]" means sections look like "[errors]", "[metrics]".
|
|
25
|
+
|
|
26
|
+
confidence_threshold:
|
|
27
|
+
Minimum score (0.0–1.0) for a section to be included.
|
|
28
|
+
|
|
29
|
+
fallback_include_all:
|
|
30
|
+
When True (default), return all content unfiltered if no section
|
|
31
|
+
scores above the threshold. Prevents silent data loss.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
sections: Sequence[str]
|
|
35
|
+
keywords: dict[str, list[str]] = field(default_factory=dict)
|
|
36
|
+
section_header_format: str = "[{name}]"
|
|
37
|
+
confidence_threshold: float = 0.3
|
|
38
|
+
fallback_include_all: bool = True
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
name_idx = self.section_header_format.index("{name}")
|
|
42
|
+
self._prefix = self.section_header_format[:name_idx]
|
|
43
|
+
|
|
44
|
+
escaped_prefix = re.escape(self._prefix)
|
|
45
|
+
lookahead = rf"(?=\n{escaped_prefix}|\Z)"
|
|
46
|
+
|
|
47
|
+
self._extraction_patterns: dict[str, re.Pattern[str]] = {}
|
|
48
|
+
for section in self.sections:
|
|
49
|
+
header = self.section_header_format.format(name=section)
|
|
50
|
+
pat = re.compile(
|
|
51
|
+
rf"({re.escape(header)}.*?){lookahead}",
|
|
52
|
+
re.DOTALL | re.IGNORECASE,
|
|
53
|
+
)
|
|
54
|
+
self._extraction_patterns[section] = pat
|
|
55
|
+
|
|
56
|
+
self._id = hash((tuple(self.sections), self.section_header_format))
|
|
57
|
+
|
|
58
|
+
def extraction_pattern(self, section: str) -> re.Pattern[str]:
|
|
59
|
+
return self._extraction_patterns[section]
|
|
60
|
+
|
|
61
|
+
def schema_id(self) -> int:
|
|
62
|
+
return self._id
|
deepcq/tokens.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Query tokenizer and suffix-strip stemmer. No external dependencies."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
_STOPWORDS = frozenset({
|
|
6
|
+
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
|
|
7
|
+
"of", "with", "by", "from", "is", "are", "was", "were", "be", "been",
|
|
8
|
+
"being", "have", "has", "had", "do", "does", "did", "will", "would",
|
|
9
|
+
"could", "should", "may", "might", "shall", "can", "it", "its",
|
|
10
|
+
"this", "that", "these", "those", "i", "me", "my", "we", "our",
|
|
11
|
+
"you", "your", "he", "she", "him", "her", "his", "hers", "they",
|
|
12
|
+
"them", "their", "what", "which", "who", "whom", "how", "when",
|
|
13
|
+
"where", "why", "all", "any", "both", "each", "few", "more", "most",
|
|
14
|
+
"other", "some", "such", "no", "not", "only", "same", "so", "than",
|
|
15
|
+
"too", "very", "just", "about", "up", "down", "out", "if", "then",
|
|
16
|
+
"as", "also", "into", "through", "during", "before", "after",
|
|
17
|
+
"above", "below", "between", "while", "although", "because", "since",
|
|
18
|
+
"until", "unless", "however", "therefore", "there", "here", "now",
|
|
19
|
+
"show", "tell", "get", "give", "let", "make", "see", "know", "go",
|
|
20
|
+
"come", "think", "say", "want", "look", "use", "find", "take",
|
|
21
|
+
"much", "many", "lot", "like", "new", "good", "right", "old",
|
|
22
|
+
"long", "little", "own", "first", "last", "next", "back", "over",
|
|
23
|
+
"again", "once", "am", "s", "re", "ve", "ll", "d", "please",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
# Ordered longest-first so greedier matches win.
|
|
27
|
+
# "es" is intentionally omitted: "spikes"→"spike" via "s" rule is correct;
|
|
28
|
+
# "es" would give "spik" which is worse for content matching.
|
|
29
|
+
_SUFFIX_RULES: list[tuple[str, str]] = [
|
|
30
|
+
("ational", "ate"), ("tional", "tion"), ("izing", "ize"),
|
|
31
|
+
("ising", "ise"), ("ization", "ize"), ("isation", "ise"),
|
|
32
|
+
("nesses", "ness"), ("iness", "y"), ("ations", "ate"),
|
|
33
|
+
("tion", ""), ("ions", ""), ("ing", ""), ("ous", ""),
|
|
34
|
+
("ive", ""), ("ful", ""), ("ness", ""), ("less", ""),
|
|
35
|
+
("able", ""), ("ible", ""), ("ment", ""), ("ance", ""),
|
|
36
|
+
("ence", ""), ("ion", ""), ("ies", "y"), ("ves", "f"),
|
|
37
|
+
("ed", ""), ("er", ""), ("ly", ""), ("s", ""),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
_MIN_ROOT = 3
|
|
41
|
+
_VOWELS = frozenset("aeiou")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _dedouble(root: str) -> str:
|
|
45
|
+
"""Remove a doubled terminal consonant produced by suffix stripping.
|
|
46
|
+
|
|
47
|
+
e.g. "runn" (from running) → "run", "bugg" (from bugged) → "bug".
|
|
48
|
+
"""
|
|
49
|
+
if len(root) >= 2 and root[-1] == root[-2] and root[-1] not in _VOWELS:
|
|
50
|
+
return root[:-1]
|
|
51
|
+
return root
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def stem(token: str) -> str:
|
|
55
|
+
"""Return a suffix-stripped root form with doubled-consonant cleanup."""
|
|
56
|
+
for suffix, replacement in _SUFFIX_RULES:
|
|
57
|
+
if token.endswith(suffix) and len(token) - len(suffix) >= _MIN_ROOT:
|
|
58
|
+
root = token[: len(token) - len(suffix)] + replacement
|
|
59
|
+
if replacement == "":
|
|
60
|
+
root = _dedouble(root)
|
|
61
|
+
return root
|
|
62
|
+
return token
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def tokenize(text: str, include_stems: bool = True) -> list[str]:
|
|
66
|
+
"""
|
|
67
|
+
Lowercase, strip punctuation, drop stopwords.
|
|
68
|
+
With include_stems=True each unique token is followed by its stemmed
|
|
69
|
+
form (if different), giving the caller both surface and root forms.
|
|
70
|
+
"""
|
|
71
|
+
words = re.findall(r"[a-zA-Z][a-zA-Z0-9]*", text.lower())
|
|
72
|
+
seen: set[str] = set()
|
|
73
|
+
result: list[str] = []
|
|
74
|
+
for w in words:
|
|
75
|
+
if w in _STOPWORDS or len(w) < 2:
|
|
76
|
+
continue
|
|
77
|
+
if w not in seen:
|
|
78
|
+
seen.add(w)
|
|
79
|
+
result.append(w)
|
|
80
|
+
if include_stems:
|
|
81
|
+
s = stem(w)
|
|
82
|
+
if s and s != w and s not in seen and len(s) >= _MIN_ROOT:
|
|
83
|
+
seen.add(s)
|
|
84
|
+
result.append(s)
|
|
85
|
+
return result
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepcq
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deep Context Query — intent-aware memory querying for LLM agents
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/ternary-ai/deepcq
|
|
7
|
+
Project-URL: Repository, https://github.com/ternary-ai/deepcq
|
|
8
|
+
Project-URL: Issues, https://github.com/ternary-ai/deepcq/issues
|
|
9
|
+
Keywords: llm,agents,memory,context,rag,prompt-engineering
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# deepcq
|
|
21
|
+
|
|
22
|
+
**Deep Context Query** — intent-aware memory querying for LLM agents.
|
|
23
|
+
|
|
24
|
+
deepcq sits between an agent's memory and its prompt. Instead of stuffing an
|
|
25
|
+
entire memory blob into every LLM call, it scores which sections of memory
|
|
26
|
+
are actually relevant to the current query and returns only those — cutting
|
|
27
|
+
prompt size while keeping recall.
|
|
28
|
+
|
|
29
|
+
No embeddings, no vector store, no external dependencies. Relevance is
|
|
30
|
+
computed from the query's own tokens against text already in memory, using
|
|
31
|
+
plain regex and a lightweight stemmer.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install deepcq
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For local development from a clone:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd deepcq
|
|
43
|
+
pip install -e ".[dev]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Core idea
|
|
47
|
+
|
|
48
|
+
You declare a `MemorySchema`: a list of section names such as
|
|
49
|
+
`["status", "errors", "metrics"]`. deepcq doesn't need to know what those
|
|
50
|
+
sections mean — it derives what's relevant from the words in each query
|
|
51
|
+
itself ("query-term-first"), rather than from a hand-built keyword table.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from deepcq import DeepCQ, MemorySchema
|
|
55
|
+
|
|
56
|
+
schema = MemorySchema(sections=["status", "errors", "metrics"])
|
|
57
|
+
cq = DeepCQ(schema=schema)
|
|
58
|
+
|
|
59
|
+
cq.state.set("errors", "[errors]\n- OOM at 02:15\n")
|
|
60
|
+
cq.state.set("metrics", "[metrics]\nCPU: 78%\nMemory: 4.2GB\n")
|
|
61
|
+
cq.state.set("status", "[status]\nAll services running\n")
|
|
62
|
+
|
|
63
|
+
result = cq.query("any memory or cpu spikes?")
|
|
64
|
+
print(result.content) # only the metrics section
|
|
65
|
+
print(result.sections_included) # ["metrics"]
|
|
66
|
+
print(f"{result.metrics.reduction_pct:.1f}% smaller")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## How it works
|
|
70
|
+
|
|
71
|
+
### 1. Five memory tiers
|
|
72
|
+
|
|
73
|
+
`DeepCQ` manages five tiers, each suited to a different memory lifetime:
|
|
74
|
+
|
|
75
|
+
| Tier | Class | Lifetime | Purpose |
|
|
76
|
+
|------|-------|----------|---------|
|
|
77
|
+
| T1 | `AppState` | live | Current state of the running application (sections keyed by name) |
|
|
78
|
+
| T2 | `SessionCache` | per-session, bounded | Ephemeral key/value scratch space, deduped, LRU-evicted |
|
|
79
|
+
| T3 | `History` | sliding window | Last N conversation turns |
|
|
80
|
+
| T4 | `LongTermStore` | persistent | Append-only notes, optionally file-backed (JSON) |
|
|
81
|
+
| T5 | `ReferenceData` | persistent, lazy | Static config/reference data, optionally file-backed |
|
|
82
|
+
|
|
83
|
+
Each tier renders itself to text via `as_text()`. `DeepCQ.query()` pulls text
|
|
84
|
+
from all five tiers before scoring.
|
|
85
|
+
|
|
86
|
+
### 2. Tokenizing the query (`tokens.py`)
|
|
87
|
+
|
|
88
|
+
The query is lowercased, split into words, stripped of stopwords, and each
|
|
89
|
+
surviving token is paired with a suffix-stripped stem (`spikes` → `spike`,
|
|
90
|
+
`running` → `run`). No external NLP dependency — it's a small suffix-rule
|
|
91
|
+
table plus a doubled-consonant cleanup (`running` → `runn` → `run`).
|
|
92
|
+
|
|
93
|
+
### 3. Scoring sections (`intent.py`)
|
|
94
|
+
|
|
95
|
+
For each section in the schema, deepcq builds a vocabulary from:
|
|
96
|
+
1. The section name itself (tokenized/stemmed).
|
|
97
|
+
2. Optional `keywords` you declare for that section in the schema (useful
|
|
98
|
+
when a section name is jargon, e.g. `"db"` → `["database", "postgres"]`).
|
|
99
|
+
3. Whatever text is currently stored under that section's header across all
|
|
100
|
+
tiers.
|
|
101
|
+
|
|
102
|
+
The section's score is the fraction of unique query tokens found in that
|
|
103
|
+
vocabulary. Sections scoring at or above `schema.confidence_threshold`
|
|
104
|
+
(default `0.3`) are "included".
|
|
105
|
+
|
|
106
|
+
### 4. Extracting content (`schema.py`, `regex_gen.py`)
|
|
107
|
+
|
|
108
|
+
Sections are delimited in stored text by a header, `"[{name}]"` by default
|
|
109
|
+
(configurable via `section_header_format`, e.g. `"## {name}"`). Each section
|
|
110
|
+
compiles to a regex that captures from its header up to the next header or
|
|
111
|
+
end of string — this is how `MemorySchema.extraction_pattern(section)` pulls
|
|
112
|
+
a section's block out of raw tier text.
|
|
113
|
+
|
|
114
|
+
For the two free-text tiers (`cache`, `history`, which aren't
|
|
115
|
+
section-delimited), deepcq instead builds a single regex out of all query
|
|
116
|
+
tokens and includes that tier's text only if any token matches
|
|
117
|
+
(`build_content_pattern` in `regex_gen.py`).
|
|
118
|
+
|
|
119
|
+
### 5. Result (`result.py`)
|
|
120
|
+
|
|
121
|
+
`query()` returns a `QueryResult`:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
@dataclass
|
|
125
|
+
class QueryResult:
|
|
126
|
+
content: str # the filtered, concatenated text
|
|
127
|
+
intent: dict[str, float] # per-section relevance scores
|
|
128
|
+
sections_included: list[str]
|
|
129
|
+
sections_excluded: list[str]
|
|
130
|
+
content_pattern: str # the regex used against cache/history
|
|
131
|
+
metrics: OptimizationMetrics # original_chars, filtered_chars, reduction_pct
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Fallback safety
|
|
135
|
+
|
|
136
|
+
If no section clears the confidence threshold and
|
|
137
|
+
`schema.fallback_include_all` is `True` (the default), `query()` returns the
|
|
138
|
+
full unfiltered text instead of nothing — this prevents a vague query from
|
|
139
|
+
silently discarding memory the agent might actually need.
|
|
140
|
+
|
|
141
|
+
## Package layout
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
deepcq/
|
|
145
|
+
├── pyproject.toml
|
|
146
|
+
├── README.md
|
|
147
|
+
├── deepcq/ # the installable package
|
|
148
|
+
│ ├── __init__.py # public API: DeepCQ, MemorySchema, QueryResult, ...
|
|
149
|
+
│ ├── core.py # DeepCQ orchestrator (query())
|
|
150
|
+
│ ├── schema.py # MemorySchema + extraction regex compilation
|
|
151
|
+
│ ├── intent.py # score_sections()
|
|
152
|
+
│ ├── tokens.py # tokenize() / stem()
|
|
153
|
+
│ ├── regex_gen.py # query-token regex builder for free-text tiers
|
|
154
|
+
│ ├── result.py # QueryResult / OptimizationMetrics dataclasses
|
|
155
|
+
│ ├── memory/
|
|
156
|
+
│ │ ├── state.py # T1 AppState
|
|
157
|
+
│ │ ├── session.py # T2 SessionCache, T3 History
|
|
158
|
+
│ │ └── store.py # T4 LongTermStore, T5 ReferenceData
|
|
159
|
+
│ └── integrations/
|
|
160
|
+
│ └── ternary/ # optional adapter for ternary-ai agents/prompts
|
|
161
|
+
│ ├── adapters.py # ToolContextProvider, LangChainMemoryAdapter
|
|
162
|
+
│ └── prompt_library.py # PromptLibrary (loads ternary-ai/prompts CSVs)
|
|
163
|
+
├── examples/
|
|
164
|
+
│ ├── openai_agents_sdk.py
|
|
165
|
+
│ ├── claude_agents_sdk.py
|
|
166
|
+
│ ├── qwen_agents_sdk.py
|
|
167
|
+
│ └── google_a2a.py
|
|
168
|
+
└── tests/
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Optional integrations
|
|
172
|
+
|
|
173
|
+
`deepcq.integrations.ternary` is an optional adapter layer for using deepcq
|
|
174
|
+
as a memory/context backend in agent frameworks. It has no hard dependency
|
|
175
|
+
on any framework — both adapters duck-type the interface they mimic:
|
|
176
|
+
|
|
177
|
+
- **`ToolContextProvider`** — formats a `QueryResult` as a plain string to
|
|
178
|
+
inject into a system prompt or tool call.
|
|
179
|
+
- **`LangChainMemoryAdapter`** — duck-types LangChain's `BaseChatMemory`
|
|
180
|
+
(`save_context` / `load_memory_variables` / `clear`), so it can be passed
|
|
181
|
+
directly as a chain's `memory=` argument without installing LangChain.
|
|
182
|
+
- **`PromptLibrary`** — loads the CSV prompt templates from the
|
|
183
|
+
[ternary-ai/prompts](https://github.com/ternary-ai/prompts) repo (locally
|
|
184
|
+
or via `urllib` from raw GitHub URLs) into a deepcq schema, so prompt
|
|
185
|
+
templates themselves can be retrieved by intent.
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from deepcq import DeepCQ, MemorySchema
|
|
189
|
+
from deepcq.integrations.ternary import ToolContextProvider, PromptLibrary
|
|
190
|
+
|
|
191
|
+
cq = DeepCQ(schema=MemorySchema(sections=["research", "risk-management"]))
|
|
192
|
+
provider = ToolContextProvider(cq)
|
|
193
|
+
context = provider.get_context("what is my portfolio concentration risk?")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
See `examples/openai_agents_sdk.py`, `examples/claude_agents_sdk.py`,
|
|
197
|
+
`examples/qwen_agents_sdk.py`, and `examples/google_a2a.py` for focused
|
|
198
|
+
integration examples. They are designed as SDK wiring references, so you
|
|
199
|
+
may need to adjust the exact import path to match the SDK version you have
|
|
200
|
+
installed.
|
|
201
|
+
|
|
202
|
+
## Testing
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pip install -e ".[dev]"
|
|
206
|
+
pytest
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Moving this into its own repository
|
|
210
|
+
|
|
211
|
+
This directory is self-contained — nothing outside `deepcq/` is required.
|
|
212
|
+
To split it out:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
cp -r deepcq/ /path/to/new/deepcq-repo
|
|
216
|
+
cd /path/to/new/deepcq-repo
|
|
217
|
+
git init
|
|
218
|
+
pip install -e ".[dev]"
|
|
219
|
+
pytest
|
|
220
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
deepcq/__init__.py,sha256=28J347FSAfusuTH-8MDRfTJlicHulRmQJEQOGITqDzE,1157
|
|
2
|
+
deepcq/core.py,sha256=vs5B7ILnTnKXy46Relt2xvdRR08zMbVv1ZjvcgFBm8E,4340
|
|
3
|
+
deepcq/intent.py,sha256=9ly-C_gVrHx9gU_Ctzb9_R74C0hWkM8RSIEfHvdvLo4,1642
|
|
4
|
+
deepcq/regex_gen.py,sha256=ulWhnQr6YISS-Lyl6njqGYlgSKTc-8Mjl6-YRKVzGwY,745
|
|
5
|
+
deepcq/result.py,sha256=aSVDwCCSBAQF0Q4fBjKnNA_QRESmS2PNSxg9l8Pav-Y,384
|
|
6
|
+
deepcq/schema.py,sha256=nj8CEMst4zq9fMgKl-vaqxA6QEXAKbnmr7VFSgmM1O4,2172
|
|
7
|
+
deepcq/tokens.py,sha256=XPYelX-nbSlQAO26moZ0HMijIQzk9ah9NSe4X-Tc6_o,3629
|
|
8
|
+
deepcq/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
deepcq/integrations/ternary/__init__.py,sha256=QZKZafC5QxBhw1S-8CATAEDc7IjaKXtkpLKTiQ40pAs,778
|
|
10
|
+
deepcq/integrations/ternary/adapters.py,sha256=AQLKpm5NKxBD9s_xYVZhS1E3NuMKYZmF88K6GciCRzo,4999
|
|
11
|
+
deepcq/integrations/ternary/prompt_library.py,sha256=jZYYChy9AfUnAMNF5X4qsy3ebQU1HD4J9ib8eyNysHM,5742
|
|
12
|
+
deepcq/memory/__init__.py,sha256=h2as2ZhLCORFpcrgu6GJpNC3LnneVlrNtsnhhmyE3yA,804
|
|
13
|
+
deepcq/memory/session.py,sha256=09amEaF9Dsb4ztP-AKhtq8zEy8cNkm1O0WLbes7wdNM,1766
|
|
14
|
+
deepcq/memory/state.py,sha256=mpqtwZ4jwLkxUWRItwSpXmrKxQgYUF6zHTsyMszT_E8,1016
|
|
15
|
+
deepcq/memory/store.py,sha256=1ILyTXEWHsWzGBCueon-6XpmYhl6n089WwAO8LBA6hI,2648
|
|
16
|
+
deepcq-0.1.0.dist-info/licenses/LICENSE,sha256=XoHDFJshSHmLHwGhVEXsU8sI3oIqCLvZnfu0-h8C4m4,1067
|
|
17
|
+
deepcq-0.1.0.dist-info/METADATA,sha256=0fWYLPk_95Ab0eZ2Ac5WA_trHtQUrGFriOZkryOnhaM,8507
|
|
18
|
+
deepcq-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
19
|
+
deepcq-0.1.0.dist-info/top_level.txt,sha256=FfVfEANDQkJ__PFSyHZq5r8oKAwaDzMtLS9MXzdiEwI,7
|
|
20
|
+
deepcq-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ternary-ai
|
|
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 @@
|
|
|
1
|
+
deepcq
|