concierge-graph 3.8.2__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.
- agents/__init__.py +14 -0
- agents/revisor_critico.py +610 -0
- concierge_graph-3.8.2.dist-info/METADATA +327 -0
- concierge_graph-3.8.2.dist-info/RECORD +37 -0
- concierge_graph-3.8.2.dist-info/WHEEL +5 -0
- concierge_graph-3.8.2.dist-info/entry_points.txt +3 -0
- concierge_graph-3.8.2.dist-info/licenses/LICENSE +21 -0
- concierge_graph-3.8.2.dist-info/top_level.txt +8 -0
- core/__init__.py +23 -0
- core/config.py +192 -0
- core/hybrid_search.py +288 -0
- core/memory_extractor.py +245 -0
- core/middleware.py +723 -0
- core/probabilistic_retriever.py +99 -0
- core/project_index.py +316 -0
- core/vector_backend.py +471 -0
- ingestion/__init__.py +25 -0
- ingestion/crawler.py +722 -0
- ingestion/orchestrator.py +920 -0
- ingestion/parser.py +984 -0
- ingestion/summarizer.py +948 -0
- interface/__init__.py +16 -0
- interface/action_hooks.py +261 -0
- interface/cli.py +391 -0
- interface/mcp_server.py +1737 -0
- main.py +281 -0
- scripts/bootstrap_core_memory.py +93 -0
- services/__init__.py +13 -0
- services/janitor.py +762 -0
- storage/__init__.py +31 -0
- storage/base_backend.py +241 -0
- storage/connection.py +310 -0
- storage/logic.py +703 -0
- storage/schema.py +390 -0
- storage/semantic_logic.py +125 -0
- storage/store.py +802 -0
- storage/vector_store.py +759 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core/probabilistic_retriever.py - Grafo Concierge v3.8.0 (Absolute Solidity)
|
|
3
|
+
|
|
4
|
+
Probabilistic Retrieval Engine (SA-CTS - U-Mem Pattern) with Thompson Sampling.
|
|
5
|
+
Balances exploration and exploitation based on utility metrics (utility_alpha, utility_beta).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("grafo-concierge.probabilistic-retriever")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ThompsonRetriever:
|
|
18
|
+
"""Implements probabilistic retrieval based on Thompson Sampling (SA-CTS).
|
|
19
|
+
|
|
20
|
+
Combines vector similarity with historical utility (Beta parameters)
|
|
21
|
+
stored in the vector metadata in Qdrant to balance exploration/exploitation.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, vector_search_fn: Callable[[str, int], list[Any]]) -> None:
|
|
25
|
+
"""Initializes the ThompsonRetriever.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
vector_search_fn: Vector search function that returns a list of candidates
|
|
29
|
+
(VectorSearchResult or equivalent dictionaries), each with
|
|
30
|
+
metadata containing utility_alpha and utility_beta.
|
|
31
|
+
"""
|
|
32
|
+
self.vector_search_fn = vector_search_fn
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def sample_multiplier(alpha: float, beta: float) -> float:
|
|
36
|
+
"""Samples a probabilistic multiplier using numpy's Beta Distribution."""
|
|
37
|
+
return float(np.random.beta(alpha, beta))
|
|
38
|
+
|
|
39
|
+
def retrieve(
|
|
40
|
+
self,
|
|
41
|
+
query: str,
|
|
42
|
+
limit: int = 5,
|
|
43
|
+
top_k: int = 3
|
|
44
|
+
) -> list[dict[str, Any]]:
|
|
45
|
+
"""Retrieves semantic facts applying Thompson Sampling.
|
|
46
|
+
|
|
47
|
+
Steps:
|
|
48
|
+
1. Runs default vector search (semantic similarity).
|
|
49
|
+
2. For each returned candidate, extracts utility_alpha and utility_beta from metadata.
|
|
50
|
+
3. Samples probabilistic multiplier using Beta Distribution:
|
|
51
|
+
thompson_multiplier = ThompsonRetriever.sample_multiplier(alpha, beta)
|
|
52
|
+
4. The candidate's final score will be: vector_similarity * thompson_multiplier.
|
|
53
|
+
5. Returns top_k memories based on final score.
|
|
54
|
+
"""
|
|
55
|
+
candidates = self.vector_search_fn(query, limit)
|
|
56
|
+
if not candidates:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
results = []
|
|
60
|
+
for cand in candidates:
|
|
61
|
+
# Candidate format identification (VectorSearchResult or dict)
|
|
62
|
+
if hasattr(cand, "metadata"):
|
|
63
|
+
metadata = cand.metadata or {}
|
|
64
|
+
similarity = cand.score
|
|
65
|
+
doc_id = cand.doc_id
|
|
66
|
+
node_id = cand.node_id
|
|
67
|
+
elif isinstance(cand, dict):
|
|
68
|
+
metadata = cand.get("metadata") or {}
|
|
69
|
+
similarity = cand.get("vector_score") or cand.get("score") or 0.0
|
|
70
|
+
doc_id = cand.get("doc_id") or cand.get("id")
|
|
71
|
+
node_id = cand.get("node_id")
|
|
72
|
+
else:
|
|
73
|
+
metadata = {}
|
|
74
|
+
similarity = 0.0
|
|
75
|
+
doc_id = None
|
|
76
|
+
node_id = None
|
|
77
|
+
|
|
78
|
+
# Get alpha and beta directly from metadata (default 1.0)
|
|
79
|
+
alpha = float(metadata.get("utility_alpha", 1.0))
|
|
80
|
+
beta = float(metadata.get("utility_beta", 1.0))
|
|
81
|
+
|
|
82
|
+
# Perform sampling with numpy Beta Distribution
|
|
83
|
+
thompson_multiplier = ThompsonRetriever.sample_multiplier(alpha, beta)
|
|
84
|
+
final_score = similarity * thompson_multiplier
|
|
85
|
+
|
|
86
|
+
results.append({
|
|
87
|
+
"doc_id": doc_id,
|
|
88
|
+
"node_id": node_id,
|
|
89
|
+
"score": final_score,
|
|
90
|
+
"raw_score": similarity,
|
|
91
|
+
"utility_alpha": alpha,
|
|
92
|
+
"utility_beta": beta,
|
|
93
|
+
"thompson_multiplier": thompson_multiplier,
|
|
94
|
+
"metadata": metadata
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
# Sort descending by final score and select top_k
|
|
98
|
+
results.sort(key=lambda x: x["score"], reverse=True)
|
|
99
|
+
return results[:top_k]
|
core/project_index.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core/project_index.py - Grafo Concierge v3.8.0 (Absolute Solidity)
|
|
3
|
+
|
|
4
|
+
Knowledge GPS — Automatic categorization of projects into Wings.
|
|
5
|
+
|
|
6
|
+
Responsibilities:
|
|
7
|
+
1. Automatic categorization: Infers a project's Primary Wing by
|
|
8
|
+
analyzing file names, tags, and descriptions against the
|
|
9
|
+
WING_KEYWORDS dictionary of ConciergeConfig.
|
|
10
|
+
2. Wing management: API to set/get Primary Wing and
|
|
11
|
+
add/remove Reference Wings.
|
|
12
|
+
3. List by Wing: Searches for projects in the same wing for
|
|
13
|
+
find_similar_projects().
|
|
14
|
+
|
|
15
|
+
Integration:
|
|
16
|
+
- Reads and writes to the `projects` table via SqliteStore.
|
|
17
|
+
- Reads and writes to the `reference_wings` table via SqliteStore.
|
|
18
|
+
- Queries ConciergeConfig.wing_keywords for classification.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from collections import Counter
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
from storage.store import SqliteStore
|
|
28
|
+
from core.config import ConciergeConfig, DEFAULT_CONFIG
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("grafo-concierge.project-index")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ProjectIndex:
|
|
34
|
+
"""Knowledge GPS — manages project categorization and discovery.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
sqlite_store: SqliteStore instance for persistence.
|
|
38
|
+
config: System configurations (default: DEFAULT_CONFIG).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
sqlite_store: SqliteStore,
|
|
44
|
+
config: ConciergeConfig = DEFAULT_CONFIG,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._store = sqlite_store
|
|
47
|
+
self._config = config
|
|
48
|
+
|
|
49
|
+
# ===================================================================
|
|
50
|
+
# AUTOMATIC CATEGORIZATION
|
|
51
|
+
# ===================================================================
|
|
52
|
+
|
|
53
|
+
def categorize_project(
|
|
54
|
+
self,
|
|
55
|
+
labels: list[str],
|
|
56
|
+
tags: Optional[list[str]] = None,
|
|
57
|
+
description: Optional[str] = None,
|
|
58
|
+
) -> str:
|
|
59
|
+
"""Infers the most suitable Primary Wing for a project.
|
|
60
|
+
|
|
61
|
+
Analyzes node labels (file names), detected tags, and
|
|
62
|
+
an optional description against the WING_KEYWORDS dictionary.
|
|
63
|
+
|
|
64
|
+
Algorithm:
|
|
65
|
+
1. Concatenates labels + tags + description into a corpus.
|
|
66
|
+
2. For each wing, counts how many keywords appear.
|
|
67
|
+
3. The wing with the most matches wins.
|
|
68
|
+
4. Tie: returns the first wing in alphabetical order.
|
|
69
|
+
5. Zero matches: returns config.default_wing ("geral").
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
labels: File / node names of the project.
|
|
73
|
+
tags: Tags detected during ingestion.
|
|
74
|
+
description: Textual description of the project (optional).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Wing name (e.g. "gestão/saas", "finanças/quant", "geral").
|
|
78
|
+
"""
|
|
79
|
+
# Build normalized corpus
|
|
80
|
+
corpus_parts: list[str] = []
|
|
81
|
+
corpus_parts.extend(labels)
|
|
82
|
+
if tags:
|
|
83
|
+
corpus_parts.extend(tags)
|
|
84
|
+
if description:
|
|
85
|
+
corpus_parts.append(description)
|
|
86
|
+
|
|
87
|
+
corpus = " ".join(corpus_parts).lower()
|
|
88
|
+
|
|
89
|
+
# Count matches per wing
|
|
90
|
+
scores: Counter[str] = Counter()
|
|
91
|
+
for wing, keywords in self._config.wing_keywords.items():
|
|
92
|
+
for kw in keywords:
|
|
93
|
+
if kw.lower() in corpus:
|
|
94
|
+
scores[wing] += 1
|
|
95
|
+
|
|
96
|
+
if not scores:
|
|
97
|
+
logger.debug("No keyword detected — using default wing '%s'.", self._config.default_wing)
|
|
98
|
+
return self._config.default_wing
|
|
99
|
+
|
|
100
|
+
# Wing with most matches (in case of tie, alphabetical order)
|
|
101
|
+
best_wing = max(sorted(scores.keys()), key=lambda w: scores[w])
|
|
102
|
+
logger.info(
|
|
103
|
+
"Automatic categorization: '%s' (score=%d, total_candidates=%d)",
|
|
104
|
+
best_wing, scores[best_wing], len(scores),
|
|
105
|
+
)
|
|
106
|
+
return best_wing
|
|
107
|
+
|
|
108
|
+
# ===================================================================
|
|
109
|
+
# PRIMARY WING — Management
|
|
110
|
+
# ===================================================================
|
|
111
|
+
|
|
112
|
+
def get_primary_wing(self, project_uuid: str) -> str:
|
|
113
|
+
"""Returns the Primary Wing of a project.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
project_uuid: Project UUID.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Name of the Primary Wing.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ProjectNotFoundError: If the project does not exist.
|
|
123
|
+
"""
|
|
124
|
+
project = self._store.get_project(project_uuid)
|
|
125
|
+
return project.get("primary_wing", self._config.default_wing)
|
|
126
|
+
|
|
127
|
+
def set_primary_wing(self, project_uuid: str, wing: str) -> None:
|
|
128
|
+
"""Sets the Primary Wing of a project (manual override).
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
project_uuid: Project UUID.
|
|
132
|
+
wing: Wing name to assign.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ProjectNotFoundError: If the project does not exist.
|
|
136
|
+
"""
|
|
137
|
+
# Validate that the project exists
|
|
138
|
+
self._store.get_project(project_uuid)
|
|
139
|
+
self._store.update_project(project_uuid, primary_wing=wing)
|
|
140
|
+
logger.info("Primary Wing definida: projeto=%s, wing='%s'", project_uuid, wing)
|
|
141
|
+
|
|
142
|
+
def auto_categorize_project(self, project_uuid: str) -> str:
|
|
143
|
+
"""Automatically categorizes a project based on existing nodes.
|
|
144
|
+
|
|
145
|
+
Flow:
|
|
146
|
+
1. Fetches all ACTIVE nodes of the project.
|
|
147
|
+
2. Extracts labels and tags.
|
|
148
|
+
3. Calls categorize_project().
|
|
149
|
+
4. Updates the Primary Wing in the database.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
project_uuid: Project UUID.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Name of the assigned wing.
|
|
156
|
+
"""
|
|
157
|
+
nodes = self._store.get_nodes_by_project(project_uuid, status="ACTIVE")
|
|
158
|
+
|
|
159
|
+
labels = [n["label"] for n in nodes]
|
|
160
|
+
all_tags: list[str] = []
|
|
161
|
+
for n in nodes:
|
|
162
|
+
if isinstance(n.get("tags"), list):
|
|
163
|
+
all_tags.extend(n["tags"])
|
|
164
|
+
|
|
165
|
+
# Use the project summary as description, if available
|
|
166
|
+
project = self._store.get_project(project_uuid)
|
|
167
|
+
description = project.get("summary")
|
|
168
|
+
|
|
169
|
+
wing = self.categorize_project(labels, all_tags, description)
|
|
170
|
+
self.set_primary_wing(project_uuid, wing)
|
|
171
|
+
|
|
172
|
+
return wing
|
|
173
|
+
|
|
174
|
+
# ===================================================================
|
|
175
|
+
# REFERENCE WINGS — Management
|
|
176
|
+
# ===================================================================
|
|
177
|
+
|
|
178
|
+
def get_reference_wings(self, project_uuid: str) -> list[str]:
|
|
179
|
+
"""Lists the Reference Wings of a project.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
project_uuid: Project UUID.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of referenced wing names.
|
|
186
|
+
"""
|
|
187
|
+
return self._store.get_reference_wings(project_uuid)
|
|
188
|
+
|
|
189
|
+
def add_reference_wing(self, project_uuid: str, wing: str) -> None:
|
|
190
|
+
"""Adds a Reference Wing to the project.
|
|
191
|
+
|
|
192
|
+
Reference Wings allow cross-disciplinary searches,
|
|
193
|
+
exposing only summaries (not raw data) of the referenced wing.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
project_uuid: Project UUID.
|
|
197
|
+
wing: Wing name to reference.
|
|
198
|
+
"""
|
|
199
|
+
primary = self.get_primary_wing(project_uuid)
|
|
200
|
+
if wing == primary:
|
|
201
|
+
logger.warning(
|
|
202
|
+
"Reference Wing '%s' é idêntica à Primary Wing — ignorando.", wing
|
|
203
|
+
)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
self._store.add_reference_wing(project_uuid, wing)
|
|
207
|
+
logger.info("Reference Wing adicionada: projeto=%s, wing='%s'", project_uuid, wing)
|
|
208
|
+
|
|
209
|
+
def remove_reference_wing(self, project_uuid: str, wing: str) -> None:
|
|
210
|
+
"""Removes a Reference Wing from the project.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
project_uuid: Project UUID.
|
|
214
|
+
wing: Wing name to remove.
|
|
215
|
+
"""
|
|
216
|
+
self._store.remove_reference_wing(project_uuid, wing)
|
|
217
|
+
logger.info("Reference Wing removida: projeto=%s, wing='%s'", project_uuid, wing)
|
|
218
|
+
|
|
219
|
+
# ===================================================================
|
|
220
|
+
# STRICT SCOPING — UUID Resolution for search
|
|
221
|
+
# ===================================================================
|
|
222
|
+
|
|
223
|
+
def resolve_scoped_uuids(
|
|
224
|
+
self,
|
|
225
|
+
project_uuid: str,
|
|
226
|
+
include_references: bool = False,
|
|
227
|
+
all_wings: bool = False,
|
|
228
|
+
) -> list[str]:
|
|
229
|
+
"""Resolves the list of project_uuids for Strict Scoping.
|
|
230
|
+
|
|
231
|
+
Modes:
|
|
232
|
+
- Default: Only projects of the same Primary Wing.
|
|
233
|
+
- include_references=True: Primary + Reference Wings.
|
|
234
|
+
- all_wings=True: All projects in the system.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
project_uuid: Anchor project UUID.
|
|
238
|
+
include_references: Include Reference Wings.
|
|
239
|
+
all_wings: Include all wings (ignores scoping).
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
List of project UUIDs in the scope of the search.
|
|
243
|
+
"""
|
|
244
|
+
all_projects = self._store.list_projects()
|
|
245
|
+
|
|
246
|
+
if all_wings:
|
|
247
|
+
return [p["uuid"] for p in all_projects]
|
|
248
|
+
|
|
249
|
+
# Assemble the set of wings in scope
|
|
250
|
+
primary = self.get_primary_wing(project_uuid)
|
|
251
|
+
target_wings = {primary}
|
|
252
|
+
|
|
253
|
+
if include_references:
|
|
254
|
+
ref_wings = self.get_reference_wings(project_uuid)
|
|
255
|
+
target_wings.update(ref_wings)
|
|
256
|
+
|
|
257
|
+
# Filter projects that belong to the wings in scope
|
|
258
|
+
scoped = [
|
|
259
|
+
p["uuid"] for p in all_projects
|
|
260
|
+
if p.get("primary_wing", self._config.default_wing) in target_wings
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
logger.debug(
|
|
264
|
+
"Strict Scoping: projeto=%s, wings=%s, projetos_no_escopo=%d",
|
|
265
|
+
project_uuid, target_wings, len(scoped),
|
|
266
|
+
)
|
|
267
|
+
return scoped
|
|
268
|
+
|
|
269
|
+
# ===================================================================
|
|
270
|
+
# DISCOVERY — Similar Projects
|
|
271
|
+
# ===================================================================
|
|
272
|
+
|
|
273
|
+
def find_similar_projects(
|
|
274
|
+
self,
|
|
275
|
+
project_uuid: str,
|
|
276
|
+
limit: int = 5,
|
|
277
|
+
include_references: bool = False,
|
|
278
|
+
all_wings: bool = False,
|
|
279
|
+
) -> list[dict]:
|
|
280
|
+
"""Searches projects of the same wing (or expanded wings).
|
|
281
|
+
|
|
282
|
+
Similarity is based on the wing: projects of the same Primary Wing
|
|
283
|
+
are considered similar by domain.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
project_uuid: Anchor project UUID.
|
|
287
|
+
limit: Maximum projects to return.
|
|
288
|
+
include_references: Include Reference Wings.
|
|
289
|
+
all_wings: Include all wings.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
List of dicts with project data (excluding itself).
|
|
293
|
+
"""
|
|
294
|
+
scoped_uuids = self.resolve_scoped_uuids(
|
|
295
|
+
project_uuid,
|
|
296
|
+
include_references=include_references,
|
|
297
|
+
all_wings=all_wings,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Remove the anchor project from the list
|
|
301
|
+
scoped_uuids = [u for u in scoped_uuids if u != project_uuid]
|
|
302
|
+
|
|
303
|
+
# Retrieve complete data of projects in scope
|
|
304
|
+
results: list[dict] = []
|
|
305
|
+
for uuid in scoped_uuids[:limit]:
|
|
306
|
+
try:
|
|
307
|
+
project = self._store.get_project(uuid)
|
|
308
|
+
results.append(project)
|
|
309
|
+
except Exception:
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
logger.info(
|
|
313
|
+
"find_similar_projects: projeto=%s, encontrados=%d (limit=%d)",
|
|
314
|
+
project_uuid, len(results), limit,
|
|
315
|
+
)
|
|
316
|
+
return results
|