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