guardianhub 0.1.88__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.
- guardianhub/__init__.py +29 -0
- guardianhub/_version.py +1 -0
- guardianhub/agents/runtime.py +12 -0
- guardianhub/auth/token_provider.py +22 -0
- guardianhub/clients/__init__.py +2 -0
- guardianhub/clients/classification_client.py +52 -0
- guardianhub/clients/graph_db_client.py +161 -0
- guardianhub/clients/langfuse/dataset_client.py +157 -0
- guardianhub/clients/langfuse/manager.py +118 -0
- guardianhub/clients/langfuse/prompt_client.py +68 -0
- guardianhub/clients/langfuse/score_evaluation_client.py +92 -0
- guardianhub/clients/langfuse/tracing_client.py +250 -0
- guardianhub/clients/langfuse_client.py +63 -0
- guardianhub/clients/llm_client.py +144 -0
- guardianhub/clients/llm_service.py +295 -0
- guardianhub/clients/metadata_extractor_client.py +53 -0
- guardianhub/clients/ocr_client.py +81 -0
- guardianhub/clients/paperless_client.py +515 -0
- guardianhub/clients/registry_client.py +18 -0
- guardianhub/clients/text_cleaner_client.py +58 -0
- guardianhub/clients/vector_client.py +344 -0
- guardianhub/config/__init__.py +0 -0
- guardianhub/config/config_development.json +84 -0
- guardianhub/config/config_prod.json +39 -0
- guardianhub/config/settings.py +221 -0
- guardianhub/http/http_client.py +26 -0
- guardianhub/logging/__init__.py +2 -0
- guardianhub/logging/logging.py +168 -0
- guardianhub/logging/logging_filters.py +35 -0
- guardianhub/models/__init__.py +0 -0
- guardianhub/models/agent_models.py +153 -0
- guardianhub/models/base.py +2 -0
- guardianhub/models/registry/client.py +16 -0
- guardianhub/models/registry/dynamic_loader.py +73 -0
- guardianhub/models/registry/loader.py +37 -0
- guardianhub/models/registry/registry.py +17 -0
- guardianhub/models/registry/signing.py +70 -0
- guardianhub/models/template/__init__.py +0 -0
- guardianhub/models/template/agent_plan.py +65 -0
- guardianhub/models/template/agent_response_evaluation.py +67 -0
- guardianhub/models/template/extraction.py +29 -0
- guardianhub/models/template/reflection_critique.py +206 -0
- guardianhub/models/template/suggestion.py +42 -0
- guardianhub/observability/__init__.py +1 -0
- guardianhub/observability/instrumentation.py +271 -0
- guardianhub/observability/otel_helper.py +43 -0
- guardianhub/observability/otel_middlewares.py +73 -0
- guardianhub/prompts/base.py +7 -0
- guardianhub/prompts/providers/langfuse_provider.py +13 -0
- guardianhub/prompts/providers/local_provider.py +22 -0
- guardianhub/prompts/registry.py +14 -0
- guardianhub/scripts/script.sh +31 -0
- guardianhub/services/base.py +15 -0
- guardianhub/template/__init__.py +0 -0
- guardianhub/tools/gh_registry_cli.py +171 -0
- guardianhub/utils/__init__.py +0 -0
- guardianhub/utils/app_state.py +74 -0
- guardianhub/utils/fastapi_utils.py +152 -0
- guardianhub/utils/json_utils.py +137 -0
- guardianhub/utils/metrics.py +60 -0
- guardianhub-0.1.88.dist-info/METADATA +240 -0
- guardianhub-0.1.88.dist-info/RECORD +64 -0
- guardianhub-0.1.88.dist-info/WHEEL +4 -0
- guardianhub-0.1.88.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vector Service Client for interacting with vector database.
|
|
3
|
+
|
|
4
|
+
This module provides functionality for:
|
|
5
|
+
- Managing vector collections
|
|
6
|
+
- Storing and retrieving document embeddings (via the service)
|
|
7
|
+
- Handling semantic and agentic tools
|
|
8
|
+
- Chunking large documents
|
|
9
|
+
- Sanitizing metadata
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from guardianhub import get_logger
|
|
18
|
+
from guardianhub.config.settings import settings
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
def _chunk_text(
|
|
23
|
+
text: str,
|
|
24
|
+
max_tokens: int = 500,
|
|
25
|
+
approx_chars_per_token: int = 4
|
|
26
|
+
) -> List[str]:
|
|
27
|
+
"""Split text into chunks of approximately max_tokens length."""
|
|
28
|
+
if not text:
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
max_chars = max_tokens * approx_chars_per_token
|
|
32
|
+
if len(text) <= max_chars:
|
|
33
|
+
return [text]
|
|
34
|
+
|
|
35
|
+
chunks = []
|
|
36
|
+
start = 0
|
|
37
|
+
|
|
38
|
+
while start < len(text):
|
|
39
|
+
end = min(len(text), start + max_chars)
|
|
40
|
+
slice_ = text[start:end]
|
|
41
|
+
|
|
42
|
+
# Look for the last newline or space to cut cleanly
|
|
43
|
+
if end < len(text):
|
|
44
|
+
last_nl = slice_.rfind("\n")
|
|
45
|
+
last_space = slice_.rfind(" ")
|
|
46
|
+
cut = max(last_nl, last_space)
|
|
47
|
+
if cut > 0:
|
|
48
|
+
end = start + cut
|
|
49
|
+
|
|
50
|
+
chunks.append(text[start:end].strip())
|
|
51
|
+
start = end
|
|
52
|
+
|
|
53
|
+
return [c for c in chunks if c]
|
|
54
|
+
|
|
55
|
+
def _sanitize_metadata_value(value: Any) -> Any:
|
|
56
|
+
"""Ensure metadata value is vector-DB safe."""
|
|
57
|
+
if value is None or value == []:
|
|
58
|
+
return None
|
|
59
|
+
if isinstance(value, (str, int, float, bool)):
|
|
60
|
+
return value
|
|
61
|
+
try:
|
|
62
|
+
# Complex types like dicts or lists should be JSON serialized
|
|
63
|
+
return json.dumps(value)
|
|
64
|
+
except Exception:
|
|
65
|
+
return str(value)
|
|
66
|
+
|
|
67
|
+
def sanitize_metadata(metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
68
|
+
"""Clean metadata dictionary by dropping None values and normalizing types."""
|
|
69
|
+
return {
|
|
70
|
+
k: _sanitize_metadata_value(v)
|
|
71
|
+
for k, v in metadata.items()
|
|
72
|
+
if _sanitize_metadata_value(v) is not None
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class VectorClient:
|
|
76
|
+
"""Client for interacting with the vector database service."""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
base_url: Optional[str] = None,
|
|
81
|
+
collection: str = settings.vector.default_collection,
|
|
82
|
+
http_timeout: float = 30.0,
|
|
83
|
+
**collection_kwargs
|
|
84
|
+
):
|
|
85
|
+
"""Initialize the VectorClient.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
base_url: Base URL for the vector service. If not provided, will try to get from settings.
|
|
89
|
+
collection_docs: Name of the document collection to use.
|
|
90
|
+
http_timeout: Timeout for HTTP requests in seconds.
|
|
91
|
+
**collection_kwargs: Additional collection configuration.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
self.base_url = base_url or settings.endpoints.get('VECTOR_SERVICE_URL')
|
|
95
|
+
# self.base_url = base_url.rstrip("/")
|
|
96
|
+
self.collection = collection
|
|
97
|
+
self.collection_config = collection_kwargs
|
|
98
|
+
self._client = httpx.AsyncClient(
|
|
99
|
+
base_url=self.base_url,
|
|
100
|
+
timeout=http_timeout
|
|
101
|
+
)
|
|
102
|
+
self.initialized = False
|
|
103
|
+
|
|
104
|
+
async def initialize(self):
|
|
105
|
+
"""Initialize the client and check connection asynchronously"""
|
|
106
|
+
try:
|
|
107
|
+
await self._check_connection()
|
|
108
|
+
self.initialized = True
|
|
109
|
+
return self.initialized
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(f"Vector client initialization failed: {str(e)}")
|
|
112
|
+
self.initialized = False
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
async def ensure_collection_exists(self, collection_name: str = None, **collection_kwargs) -> bool:
|
|
116
|
+
"""Ensure the specified collection exists, create it if it doesn't.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
collection_name: Name of the collection to check/create
|
|
120
|
+
**collection_kwargs: Additional arguments for collection creation
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
bool: True if collection exists or was created, False otherwise
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
# Try to create the collection - will succeed if it doesn't exist
|
|
127
|
+
await self.create_collection(collection_name, **collection_kwargs)
|
|
128
|
+
logger.info(f"Created collection: {collection_name}")
|
|
129
|
+
return True
|
|
130
|
+
except httpx.HTTPStatusError as e:
|
|
131
|
+
if e.response.status_code == 409: # Collection already exists
|
|
132
|
+
logger.debug(f"Collection {collection_name} already exists")
|
|
133
|
+
return True
|
|
134
|
+
logger.error(f"Error ensuring collection {collection_name} exists: {str(e)}")
|
|
135
|
+
return False
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"Unexpected error ensuring collection {collection_name} exists: {str(e)}")
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
async def _check_connection(self):
|
|
141
|
+
"""Check if the vector service is available."""
|
|
142
|
+
try:
|
|
143
|
+
response = await self._client.get("/health")
|
|
144
|
+
response.raise_for_status()
|
|
145
|
+
self.initialized = True
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Vector client health check failed: {str(e)}")
|
|
148
|
+
self.initialized = False
|
|
149
|
+
raise
|
|
150
|
+
|
|
151
|
+
async def close(self):
|
|
152
|
+
"""Close the HTTP client."""
|
|
153
|
+
await self._client.aclose()
|
|
154
|
+
|
|
155
|
+
async def upsert_documents(
|
|
156
|
+
self,
|
|
157
|
+
ids: List[str],
|
|
158
|
+
documents: List[str],
|
|
159
|
+
metadatas: Optional[List[Dict[str, Any]]],
|
|
160
|
+
collection: Optional[str] ,
|
|
161
|
+
embeddings: Optional[List[List[float]]] = None,
|
|
162
|
+
) -> Any:
|
|
163
|
+
"""Upsert documents into the vector database in a single batch call."""
|
|
164
|
+
if not ids or not documents or len(ids) != len(documents):
|
|
165
|
+
raise ValueError("ids and documents must be non-empty and same length")
|
|
166
|
+
|
|
167
|
+
if metadatas and len(metadatas) != len(ids):
|
|
168
|
+
raise ValueError("metadatas must be same length as ids if provided")
|
|
169
|
+
|
|
170
|
+
# Ensure metadata is safe for the vector store
|
|
171
|
+
sanitized_metas = [
|
|
172
|
+
sanitize_metadata(m) if m else {}
|
|
173
|
+
for m in (metadatas or [{}] * len(ids))
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
payload = {
|
|
177
|
+
# Note: We do not pass embeddings here; the service will generate them.
|
|
178
|
+
"documents": documents,
|
|
179
|
+
"ids": ids,
|
|
180
|
+
"metadatas": sanitized_metas,
|
|
181
|
+
"embeddings": embeddings
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
response = await self._client.post(
|
|
185
|
+
f"/collections/{collection}/add",
|
|
186
|
+
json=payload
|
|
187
|
+
)
|
|
188
|
+
response.raise_for_status()
|
|
189
|
+
return response.json()
|
|
190
|
+
|
|
191
|
+
async def upsert_document_from_text(
|
|
192
|
+
self,
|
|
193
|
+
document_content: str,
|
|
194
|
+
doc_id: str,
|
|
195
|
+
metadata: Dict[str, Any],
|
|
196
|
+
collection: Optional[str] = None
|
|
197
|
+
) -> Dict[str, Any]:
|
|
198
|
+
"""
|
|
199
|
+
Chunks a single document and upserts all chunks into the vector database.
|
|
200
|
+
|
|
201
|
+
The ingestion activity (store_in_vector_db_activity) should call this method.
|
|
202
|
+
It returns the ID of the first chunk, which acts as the 'vector_id' for the document.
|
|
203
|
+
"""
|
|
204
|
+
collection_name = collection
|
|
205
|
+
|
|
206
|
+
if not document_content:
|
|
207
|
+
logger.warning(f"Attempted to upsert empty content for document ID: {doc_id}")
|
|
208
|
+
return {"status": "skipped", "message": "Empty document content"}
|
|
209
|
+
|
|
210
|
+
# 1. Chunk the document
|
|
211
|
+
chunks = _chunk_text(document_content)
|
|
212
|
+
|
|
213
|
+
if not chunks:
|
|
214
|
+
logger.warning(f"Chunking failed for document ID: {doc_id}")
|
|
215
|
+
return {"status": "skipped", "message": "Chunking failed to produce content"}
|
|
216
|
+
|
|
217
|
+
# 2. Prepare IDs and Metadata for upsert
|
|
218
|
+
chunk_ids = [f"{doc_id}-{i}" for i in range(len(chunks))]
|
|
219
|
+
|
|
220
|
+
# All chunks share the same base metadata, plus chunk-specific indices
|
|
221
|
+
base_metadata = sanitize_metadata(metadata.copy())
|
|
222
|
+
|
|
223
|
+
chunk_metadatas = []
|
|
224
|
+
for i in range(len(chunks)):
|
|
225
|
+
meta = base_metadata.copy()
|
|
226
|
+
# Store the primary document ID in the metadata for easy filtering/retrieval
|
|
227
|
+
meta["original_doc_id"] = doc_id
|
|
228
|
+
meta["chunk_index"] = i
|
|
229
|
+
meta["chunk_total"] = len(chunks)
|
|
230
|
+
chunk_metadatas.append(meta)
|
|
231
|
+
|
|
232
|
+
# 3. Upsert the documents (chunks)
|
|
233
|
+
response = await self.upsert_documents(
|
|
234
|
+
ids=chunk_ids,
|
|
235
|
+
documents=chunks,
|
|
236
|
+
metadatas=chunk_metadatas,
|
|
237
|
+
collection=collection_name,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Return the ID of the first chunk
|
|
241
|
+
return {"id": chunk_ids[0], "status": "success", "chunk_count": len(chunks), "service_response": response}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def delete_document(self, doc_id: str, collection: str) -> None:
|
|
245
|
+
"""Delete a document (and all its chunks) from the vector database.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
doc_id: The ID of the document to delete
|
|
249
|
+
collection: The name of the collection containing the document
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
httpx.HTTPStatusError: If the deletion request fails
|
|
253
|
+
"""
|
|
254
|
+
# For a complete document deletion, we must delete based on the original_doc_id in metadata
|
|
255
|
+
response = await self._client.request(
|
|
256
|
+
method="DELETE",
|
|
257
|
+
url=f"/collections/{collection}/delete",
|
|
258
|
+
json={
|
|
259
|
+
"ids": [doc_id],
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
response.raise_for_status()
|
|
263
|
+
logger.info(f"Deleted document chunks for original ID {doc_id} from {collection}")
|
|
264
|
+
return response.json()
|
|
265
|
+
|
|
266
|
+
# Removed the previous embed_text method as embedding is handled by the service.
|
|
267
|
+
|
|
268
|
+
async def query(
|
|
269
|
+
self,
|
|
270
|
+
query: str,
|
|
271
|
+
collection: str,
|
|
272
|
+
n_results: int = 5,
|
|
273
|
+
where: Optional[Dict[str, Any]] = None
|
|
274
|
+
) -> List[Dict[str, Any]]:
|
|
275
|
+
"""Query the vector database using a text query."""
|
|
276
|
+
if not self.initialized:
|
|
277
|
+
await self.initialize()
|
|
278
|
+
|
|
279
|
+
payload = {
|
|
280
|
+
"query_texts": [query],
|
|
281
|
+
"n_results": n_results
|
|
282
|
+
}
|
|
283
|
+
# Where clause must be sanitized if needed, but we rely on the service to interpret Chroma syntax
|
|
284
|
+
if where:
|
|
285
|
+
payload["where"] = where
|
|
286
|
+
|
|
287
|
+
response = await self._client.post(
|
|
288
|
+
f"/collections/{collection}/query",
|
|
289
|
+
json=payload
|
|
290
|
+
)
|
|
291
|
+
response.raise_for_status()
|
|
292
|
+
return response.json()
|
|
293
|
+
|
|
294
|
+
async def create_collection(self, name: str, **kwargs) -> Dict[str, Any]:
|
|
295
|
+
"""Create a new collection."""
|
|
296
|
+
response = await self._client.post(
|
|
297
|
+
f"/collections",
|
|
298
|
+
json={
|
|
299
|
+
"name": name,
|
|
300
|
+
**kwargs
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
response.raise_for_status()
|
|
304
|
+
return response.json()
|
|
305
|
+
|
|
306
|
+
async def delete_collection(self, name: str) -> None:
|
|
307
|
+
"""Delete a collection."""
|
|
308
|
+
# response = await self._client.delete(f"/collections/{name}/delete")
|
|
309
|
+
# response.raise_for_status()
|
|
310
|
+
logger.info(f"Deleted collection {name}")
|
|
311
|
+
|
|
312
|
+
async def get_raw_embedding(self, document_text: str) -> Optional[List[float]]:
|
|
313
|
+
"""
|
|
314
|
+
Retrieves the raw embedding vector for a single document text
|
|
315
|
+
via the Vector DB Service's dedicated generation endpoint.
|
|
316
|
+
"""
|
|
317
|
+
endpoint = "/embed/text"
|
|
318
|
+
|
|
319
|
+
# NOTE: The Vector Service endpoint expects a list of texts
|
|
320
|
+
payload = {"texts": [document_text]}
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
response = await self._client.post(endpoint, json=payload)
|
|
324
|
+
response.raise_for_status()
|
|
325
|
+
|
|
326
|
+
data = response.json()
|
|
327
|
+
embeddings = data.get("embeddings")
|
|
328
|
+
|
|
329
|
+
if embeddings and len(embeddings) > 0:
|
|
330
|
+
# We requested one text, so we return the first (and only) embedding
|
|
331
|
+
return embeddings[0]
|
|
332
|
+
|
|
333
|
+
logger.error("Embedding service returned success but embeddings list was empty.")
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
except httpx.HTTPStatusError as e:
|
|
337
|
+
logger.error(
|
|
338
|
+
"HTTP error generating raw embedding: Status %d. Detail: %s",
|
|
339
|
+
e.response.status_code, e.response.text
|
|
340
|
+
)
|
|
341
|
+
return None
|
|
342
|
+
except httpx.RequestError as e:
|
|
343
|
+
logger.error("Network error connecting to Vector DB for embedding generation: %s", str(e))
|
|
344
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"service": {
|
|
3
|
+
"name": "sutram-orchestration-service",
|
|
4
|
+
"id": "sutram-orchestration-service-01",
|
|
5
|
+
"host": "0.0.0.0",
|
|
6
|
+
"port": 7001
|
|
7
|
+
},
|
|
8
|
+
"logging": {
|
|
9
|
+
"level": "INFO",
|
|
10
|
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
11
|
+
},
|
|
12
|
+
"vector": {
|
|
13
|
+
"default_collection": "test_collection"
|
|
14
|
+
},
|
|
15
|
+
"endpoints": {
|
|
16
|
+
"POSTGRES_URL": "postgresql+asyncpg://user:pass@postgres-host:5432/db_name",
|
|
17
|
+
"CONSUL_HTTP_ADDR": "http://guardian:8500",
|
|
18
|
+
"TOOL_REGISTRY_URL": "http://tools-registry.guardianhub.com",
|
|
19
|
+
"TOOL_EXECUTOR_URL": "http://agentic-orchestrator.guardianhub.com",
|
|
20
|
+
"GRAPH_DB_URL": "http://guardianhub:8009/",
|
|
21
|
+
"VECTOR_SERVICE_URL": "http://vectordb.guardianhub.com",
|
|
22
|
+
"LLM_URL": "http://aura-llm.guardianhub.com",
|
|
23
|
+
"LANGFUSE_HOST": "http://langfuse.guardianhub.com",
|
|
24
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://guardianhub:4318",
|
|
25
|
+
"LANGFUSE_OTLP_TRACES_ENDPOINT": "http://langfuse.guardianhub.com:4318",
|
|
26
|
+
"KG_INGESTION_URL": "http://graph-db-service:8009/ingest-yaml-schema",
|
|
27
|
+
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": "http/protobuf",
|
|
28
|
+
"OTEL_EXPORTER_OTLP_TRACES_PATH": "http/protobuf",
|
|
29
|
+
"LF_EXCLUDED_URL": "http://guardianhub:3000.*,/v1/metrics",
|
|
30
|
+
"LANGFUSE_PROJECT_ID": "cmgy74xzc0006xo07iwmvamue"
|
|
31
|
+
},
|
|
32
|
+
"llm": {
|
|
33
|
+
"model_configs": {
|
|
34
|
+
"judge": {
|
|
35
|
+
"model_name": "gemini-2.0-flash",
|
|
36
|
+
"base_url": "https://generativelanguage.googleapis.com/v1beta/models",
|
|
37
|
+
"api_key": "AIzaSyC7R2Ag1e-6VPf_c15boWPFr99FyvpHcT8",
|
|
38
|
+
"headers": {
|
|
39
|
+
},
|
|
40
|
+
"temperature": 0.1,
|
|
41
|
+
"max_tokens": 2048,
|
|
42
|
+
"model_kwargs": {},
|
|
43
|
+
"provider": "GEMINI",
|
|
44
|
+
"streaming": false
|
|
45
|
+
},
|
|
46
|
+
"tuned": {
|
|
47
|
+
"model_name": "phi3:3.8b-mini-4k-instruct-q4_K_M",
|
|
48
|
+
"provider": "OPENAI",
|
|
49
|
+
"base_url": "http://guardianhub:11434",
|
|
50
|
+
"temperature": 0.1,
|
|
51
|
+
"max_tokens": 2048,
|
|
52
|
+
"streaming": false,
|
|
53
|
+
"model_kwargs": {}
|
|
54
|
+
},
|
|
55
|
+
"coding": {
|
|
56
|
+
"model_name": "deepseek-coder:1.3b",
|
|
57
|
+
"temperature": 0.1,
|
|
58
|
+
"max_tokens": 2048,
|
|
59
|
+
"base_url": "http://guardianhub:11434",
|
|
60
|
+
"api_key": "your-api-key-here",
|
|
61
|
+
"streaming": false,
|
|
62
|
+
"model_kwargs": {}
|
|
63
|
+
},
|
|
64
|
+
"default": {
|
|
65
|
+
"model_name": "phi3:mini",
|
|
66
|
+
"provider": "OLLAMA",
|
|
67
|
+
"base_url": "http://guardianhub:11434",
|
|
68
|
+
"temperature": 0.1,
|
|
69
|
+
"max_tokens": 2048,
|
|
70
|
+
"streaming": false,
|
|
71
|
+
"model_kwargs": {}
|
|
72
|
+
},
|
|
73
|
+
"large": {
|
|
74
|
+
"model_name": "mistral-7b-instruct",
|
|
75
|
+
"temperature": 0.1,
|
|
76
|
+
"max_tokens": 2048,
|
|
77
|
+
"base_url": "http://guardianhub:11434",
|
|
78
|
+
"api_key": "your-api-key-here",
|
|
79
|
+
"streaming": false,
|
|
80
|
+
"model_kwargs": {}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"service": {
|
|
3
|
+
"name": "guardianhub-sdk",
|
|
4
|
+
"id": "guardianhub-sdk-01",
|
|
5
|
+
"host": "0.0.0.0",
|
|
6
|
+
"port": 8001
|
|
7
|
+
},
|
|
8
|
+
"logging": {
|
|
9
|
+
"level": "INFO",
|
|
10
|
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
11
|
+
},
|
|
12
|
+
"excluded_urls": "http://langfuse-web.langfuse.svc.cluster.local:3000.*,/v1/metrics",
|
|
13
|
+
"endpoints": {
|
|
14
|
+
"POSTGRES_URL": "postgresql+asyncpg://user:pass@postgres-host:5432/db_name",
|
|
15
|
+
"CONSUL_HTTP_ADDR": "http://consul-server.consul.svc.cluster.local:8500",
|
|
16
|
+
"TOOL_REGISTRY_URL": "http://tool-registry-service.default.svc.cluster.local:8002",
|
|
17
|
+
"TOOL_EXECUTOR_URL": "http://agentic-orchestrator-service.default.svc.cluster.local:8003",
|
|
18
|
+
"LLM_URL": "http://aura-llm-service.default.svc.cluster.local:8000/v1",
|
|
19
|
+
"LANGFUSE_HOST": "http://langfuse-web.langfuse.svc.cluster.local:3000",
|
|
20
|
+
"LANGFUSE_OTLP_TRACES_ENDPOINT": "http://langfuse-web.langfuse.svc.cluster.local:3000",
|
|
21
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://otel-collector-service.monitoring.svc.cluster.local:4318",
|
|
22
|
+
"KG_INGESTION_URL": "http://graph-db-service:8009/ingest-yaml-schema",
|
|
23
|
+
"ENVIRONMENT": "kubernetes-dev",
|
|
24
|
+
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": "http/protobuf",
|
|
25
|
+
"OTEL_EXPORTER_OTLP_TRACES_PATH": "http/protobuf",
|
|
26
|
+
"LF_EXCLUDED_URL": "http://langfuse-web.langfuse.svc.cluster.local:3000.*,/v1/metrics",
|
|
27
|
+
"GRAPH_DB_URL": "http://graph-db-service.default.svc.cluster.local:8009",
|
|
28
|
+
"VECTOR_SERVICE_URL": "http://vector-db-service.default.svc.cluster.local:8005"
|
|
29
|
+
},
|
|
30
|
+
"llm": {
|
|
31
|
+
"model_name": "phi-4",
|
|
32
|
+
"temperature": 0.1,
|
|
33
|
+
"max_tokens": 4096,
|
|
34
|
+
"base_url": "http://aura-llm-service.default.svc.cluster.local:8000/v1",
|
|
35
|
+
"api_key": "your-api-key-here",
|
|
36
|
+
"streaming": false,
|
|
37
|
+
"model_kwargs": {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Core settings for the GuardianHub Foundation SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
# Correct imports for Pydantic v2 and Pydantic-Settings v2
|
|
11
|
+
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
|
12
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_config_defaults(env: str = None) -> dict:
|
|
16
|
+
"""Load configuration defaults from JSON files based on environment."""
|
|
17
|
+
if env is None:
|
|
18
|
+
env = os.environ.get("ENVIRONMENT", "development").lower()
|
|
19
|
+
|
|
20
|
+
# Mapping common aliases
|
|
21
|
+
if env == "dev":
|
|
22
|
+
env = "development"
|
|
23
|
+
|
|
24
|
+
config_dir = Path(__file__).parent
|
|
25
|
+
possible_files = [f"config_{env}.json", "config_dev.json", "config_development.json"]
|
|
26
|
+
|
|
27
|
+
for filename in possible_files:
|
|
28
|
+
config_path = config_dir / filename
|
|
29
|
+
if config_path.exists():
|
|
30
|
+
try:
|
|
31
|
+
with open(config_path) as f:
|
|
32
|
+
return json.load(f)
|
|
33
|
+
except Exception:
|
|
34
|
+
continue
|
|
35
|
+
return {}
|
|
36
|
+
|
|
37
|
+
class ServiceInfo(BaseModel):
|
|
38
|
+
"""Metadata about the microservice using the SDK."""
|
|
39
|
+
name: str = "guardianhub-service"
|
|
40
|
+
version: str = "0.0.1"
|
|
41
|
+
id: str = "guardian-01"
|
|
42
|
+
host: str = "0.0.0.0"
|
|
43
|
+
port: int = 8001
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LoggingSettings(BaseModel):
|
|
47
|
+
"""Standardized logging configuration."""
|
|
48
|
+
level: str = "INFO"
|
|
49
|
+
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
from typing import Dict, Any, Optional
|
|
53
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
54
|
+
|
|
55
|
+
class ModelConfig(BaseModel):
|
|
56
|
+
"""Standardized schema for any LLM model configuration."""
|
|
57
|
+
model_name: str
|
|
58
|
+
provider: str = "OPENAI" # GEMINI, OLLAMA, OPENAI, etc.
|
|
59
|
+
base_url: str
|
|
60
|
+
api_key: str = "your-api-key-here"
|
|
61
|
+
temperature: float = 0.1
|
|
62
|
+
max_tokens: int = 2048
|
|
63
|
+
streaming: bool = False
|
|
64
|
+
headers: Dict[str, str] = {}
|
|
65
|
+
model_kwargs: Dict[str, Any] = {}
|
|
66
|
+
|
|
67
|
+
class LLMSettings(BaseModel):
|
|
68
|
+
"""Registry of models defined in the config JSON."""
|
|
69
|
+
# This matches your "model_configs": { "judge": {...}, "tuned": {...} }
|
|
70
|
+
model_configs: Dict[str, ModelConfig] = Field(default_factory=dict)
|
|
71
|
+
|
|
72
|
+
def get_config(self, key: str = "default") -> ModelConfig:
|
|
73
|
+
"""Helper to safely retrieve a model config by its key."""
|
|
74
|
+
return self.model_configs.get(key, self.model_configs.get("default"))
|
|
75
|
+
|
|
76
|
+
class ServiceEndpoints(BaseModel):
|
|
77
|
+
"""
|
|
78
|
+
Endpoints for essential shared services.
|
|
79
|
+
Supports dictionary-style access for backward compatibility.
|
|
80
|
+
"""
|
|
81
|
+
model_config = ConfigDict(extra="allow")
|
|
82
|
+
|
|
83
|
+
# Infrastructure & Databases
|
|
84
|
+
POSTGRES_URL: str = "postgresql://user:password@localhost:5432/guardianhub"
|
|
85
|
+
CONSUL_HTTP_ADDR: str = "http://localhost:8500"
|
|
86
|
+
|
|
87
|
+
# Shared GuardianHub Services
|
|
88
|
+
TOOL_REGISTRY_URL: str = "http://localhost:8000"
|
|
89
|
+
TOOL_EXECUTOR_URL: str = "http://localhost:8003"
|
|
90
|
+
LLM_URL: str = "http://localhost:8001"
|
|
91
|
+
LANGFUSE_HOST: str = "http://localhost:3000"
|
|
92
|
+
KG_INGESTION_URL: str = "http://localhost:8002"
|
|
93
|
+
GRAPH_DB_URL: str = "http://localhost:8009"
|
|
94
|
+
VECTOR_SERVICE_URL: str = "http://localhost:8005"
|
|
95
|
+
|
|
96
|
+
# Observability
|
|
97
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: str = "http://localhost:4318"
|
|
98
|
+
LANGFUSE_OTLP_TRACES_ENDPOINT: str = "http://localhost:4318"
|
|
99
|
+
ENVIRONMENT: str = "development"
|
|
100
|
+
|
|
101
|
+
def __init__(self, **data):
|
|
102
|
+
# Handle case-insensitive environment variables
|
|
103
|
+
processed_data = {}
|
|
104
|
+
for k, v in data.items():
|
|
105
|
+
# Convert to uppercase for case-insensitive matching
|
|
106
|
+
if k.upper() in self.model_fields:
|
|
107
|
+
processed_data[k.upper()] = v
|
|
108
|
+
else:
|
|
109
|
+
processed_data[k] = v
|
|
110
|
+
|
|
111
|
+
# Initialize the model with known fields
|
|
112
|
+
super().__init__(**{k: v for k, v in processed_data.items()
|
|
113
|
+
if k in self.model_fields})
|
|
114
|
+
|
|
115
|
+
# Store extra fields in __pydantic_extra__
|
|
116
|
+
extra_data = {k: v for k, v in processed_data.items()
|
|
117
|
+
if k not in self.model_fields}
|
|
118
|
+
if extra_data:
|
|
119
|
+
if not hasattr(self, '__pydantic_extra__'):
|
|
120
|
+
object.__setattr__(self, '__pydantic_extra__', {})
|
|
121
|
+
self.__pydantic_extra__.update(extra_data)
|
|
122
|
+
|
|
123
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
124
|
+
"""Dictionary-style get method for backward compatibility."""
|
|
125
|
+
# First try to get from model fields (case-insensitive)
|
|
126
|
+
key_upper = key.upper()
|
|
127
|
+
if key_upper in self.model_fields:
|
|
128
|
+
return getattr(self, key_upper, default)
|
|
129
|
+
|
|
130
|
+
# Then try to get from extra fields (case-sensitive)
|
|
131
|
+
if hasattr(self, '__pydantic_extra__'):
|
|
132
|
+
# Try exact match first
|
|
133
|
+
if key in self.__pydantic_extra__:
|
|
134
|
+
return self.__pydantic_extra__[key]
|
|
135
|
+
# Try case-insensitive match
|
|
136
|
+
for k, v in self.__pydantic_extra__.items():
|
|
137
|
+
if k.upper() == key_upper:
|
|
138
|
+
return v
|
|
139
|
+
|
|
140
|
+
# Finally, try direct attribute access
|
|
141
|
+
return getattr(self, key, default)
|
|
142
|
+
|
|
143
|
+
def __getitem__(self, key: str) -> Any:
|
|
144
|
+
"""Enable dictionary-style access."""
|
|
145
|
+
value = self.get(key)
|
|
146
|
+
if value is None and not hasattr(self, key) and not (hasattr(self, '__pydantic_extra__') and key in self.__pydantic_extra__):
|
|
147
|
+
raise KeyError(f"'{key}' not found in ServiceEndpoints")
|
|
148
|
+
return value
|
|
149
|
+
|
|
150
|
+
class VectorConfig(BaseModel):
|
|
151
|
+
"""Vector database configuration."""
|
|
152
|
+
default_collection: str = Field("", description="Default collection name")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class CoreSettings(BaseSettings):
|
|
156
|
+
"""
|
|
157
|
+
The Core Settings class. Automatically merges bundled JSON
|
|
158
|
+
profiles with environment variables.
|
|
159
|
+
"""
|
|
160
|
+
model_config = SettingsConfigDict(
|
|
161
|
+
env_prefix="GH_",
|
|
162
|
+
env_nested_delimiter="__",
|
|
163
|
+
extra="allow"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
service: ServiceInfo = Field(default_factory=ServiceInfo)
|
|
167
|
+
logging: LoggingSettings = Field(default_factory=LoggingSettings)
|
|
168
|
+
llm: LLMSettings = Field(default_factory=LLMSettings)
|
|
169
|
+
endpoints: ServiceEndpoints = Field(default_factory=ServiceEndpoints)
|
|
170
|
+
vector: VectorConfig = Field(default_factory=VectorConfig)
|
|
171
|
+
excluded_urls: Optional[str] = None
|
|
172
|
+
max_retries: int = 3
|
|
173
|
+
retry_delay: int = 2
|
|
174
|
+
http_timeout: int = 30
|
|
175
|
+
|
|
176
|
+
def __init__(self, **data):
|
|
177
|
+
# Handle environment variable overrides for ENVIRONMENT
|
|
178
|
+
env = os.environ.get("ENVIRONMENT")
|
|
179
|
+
if env:
|
|
180
|
+
if "endpoints" not in data:
|
|
181
|
+
data["endpoints"] = {}
|
|
182
|
+
data["endpoints"]["ENVIRONMENT"] = env
|
|
183
|
+
|
|
184
|
+
# Let Pydantic handle the main initialization
|
|
185
|
+
super().__init__(**data)
|
|
186
|
+
|
|
187
|
+
@model_validator(mode="before")
|
|
188
|
+
@classmethod
|
|
189
|
+
def _bootstrap_config(cls, data: Any) -> Any:
|
|
190
|
+
if isinstance(data, dict):
|
|
191
|
+
# Load the appropriate config file based on environment
|
|
192
|
+
env = data.get("ENVIRONMENT") or os.environ.get("ENVIRONMENT", "development")
|
|
193
|
+
config_data = _load_config_defaults(env)
|
|
194
|
+
|
|
195
|
+
if isinstance(config_data, dict):
|
|
196
|
+
# Create a copy of the input data to avoid modifying it
|
|
197
|
+
result = config_data.copy()
|
|
198
|
+
|
|
199
|
+
# Update with environment variables, but preserve the endpoints
|
|
200
|
+
if "endpoints" in data and isinstance(data["endpoints"], dict):
|
|
201
|
+
# Create a new endpoints dict with the config defaults
|
|
202
|
+
endpoints = result.get("endpoints", {}).copy()
|
|
203
|
+
# Update with any environment overrides
|
|
204
|
+
endpoints.update(data["endpoints"])
|
|
205
|
+
result["endpoints"] = endpoints
|
|
206
|
+
|
|
207
|
+
# Update with any other non-endpoint data
|
|
208
|
+
for key, value in data.items():
|
|
209
|
+
if key != "endpoints":
|
|
210
|
+
result[key] = value
|
|
211
|
+
|
|
212
|
+
return result
|
|
213
|
+
return data
|
|
214
|
+
|
|
215
|
+
@lru_cache()
|
|
216
|
+
def get_cached_settings() -> CoreSettings:
|
|
217
|
+
return CoreSettings()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Exported singleton
|
|
221
|
+
settings: CoreSettings = get_cached_settings()
|