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
guardianhub/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""GuardianHub SDK - Unified SDK for Local AI Guardian
|
|
2
|
+
|
|
3
|
+
This package provides core functionality for the GuardianHub platform,
|
|
4
|
+
including logging, metrics, and FastAPI utilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Version
|
|
8
|
+
from ._version import __version__ # version exported for users
|
|
9
|
+
# Core functionality
|
|
10
|
+
from .logging import get_logger, setup_logging
|
|
11
|
+
from .observability.instrumentation import configure_instrumentation
|
|
12
|
+
from .utils.app_state import AppState
|
|
13
|
+
from .utils.fastapi_utils import initialize_guardian_app
|
|
14
|
+
from .utils.metrics import setup_metrics, get_metrics_registry
|
|
15
|
+
|
|
16
|
+
# Public API
|
|
17
|
+
__all__ = [
|
|
18
|
+
# Version
|
|
19
|
+
"__version__",
|
|
20
|
+
|
|
21
|
+
# Logging
|
|
22
|
+
"get_logger",
|
|
23
|
+
"setup_logging",
|
|
24
|
+
|
|
25
|
+
"AppState",
|
|
26
|
+
"setup_metrics","get_metrics_registry",
|
|
27
|
+
"setup_middleware","setup_health_endpoint","setup_metrics_endpoint",
|
|
28
|
+
"configure_instrumentation",
|
|
29
|
+
]
|
guardianhub/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.1.87'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# guardianhub_sdk/agents/runtime.py
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AgentRuntime:
|
|
6
|
+
def __init__(self, clients: Dict[str, Any]):
|
|
7
|
+
self.clients = clients
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def run(self, spec: Dict[str, Any]):
|
|
11
|
+
# stub: execute agent spec using clients
|
|
12
|
+
return {"status": "ok", "spec": spec}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# guardianhub_sdk/auth/token_provider.py
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TokenProvider:
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
"""Simple token provider stub. Replace with your vault/service account integration."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
12
|
+
self.api_key = api_key
|
|
13
|
+
self._cached_token = api_key
|
|
14
|
+
self._expires_at = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def get_token(self) -> str:
|
|
18
|
+
# For server-to-server use, you may want to implement JWT/service-account exchange
|
|
19
|
+
if self._cached_token:
|
|
20
|
+
return self._cached_token
|
|
21
|
+
# fallback placeholder
|
|
22
|
+
return "placeholder"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Minio client with graceful error handling and connection management."""
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from typing import Optional, Union, Dict, Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from guardianhub.config.settings import settings
|
|
9
|
+
from guardianhub.logging import get_logger
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
class ClassificationClient:
|
|
12
|
+
"""Minio client wrapper with graceful error handling."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, base_url:str, poll_interval: int = 5, poll_timeout: int = 300):
|
|
15
|
+
self.initialized = False
|
|
16
|
+
self.client = None
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
self.api_url = base_url.rstrip('/')
|
|
20
|
+
self.headers = {
|
|
21
|
+
"Accept": "application/json",
|
|
22
|
+
}
|
|
23
|
+
self.poll_interval = poll_interval
|
|
24
|
+
self.poll_timeout = poll_timeout
|
|
25
|
+
|
|
26
|
+
# Initialize the persistent httpx client here.
|
|
27
|
+
# DO NOT use it in an 'async with' block in methods, or it will be closed.
|
|
28
|
+
self.client = httpx.AsyncClient(headers=self.headers, base_url=self.api_url,
|
|
29
|
+
timeout=self.poll_timeout + 60)
|
|
30
|
+
self.initialized = True
|
|
31
|
+
logger.info(f"✅ Classification client connected to {settings.endpoints.CLASSIFICATION_ENDPOINT}")
|
|
32
|
+
|
|
33
|
+
except Exception as e:
|
|
34
|
+
logger.warning(
|
|
35
|
+
f"⚠️ Failed to initialize Classification client: {str(e)}. "
|
|
36
|
+
"Classification operations will be disabled."
|
|
37
|
+
)
|
|
38
|
+
self.initialized = False
|
|
39
|
+
|
|
40
|
+
async def classify(self, text: str,metadata:Dict[str, Any]) -> Dict[str, Any]:
|
|
41
|
+
"""Generate a presigned URL for uploading an object."""
|
|
42
|
+
if not self.initialized:
|
|
43
|
+
logger.warning("Classification client not initialized, cannot generate presigned URL")
|
|
44
|
+
return {}
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
response = await self.client.post("/v1/document/classify", json={"text": text, "metadata":metadata})
|
|
48
|
+
return response.json()
|
|
49
|
+
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error(f"Failed to classify document: {str(e)}")
|
|
52
|
+
return {}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# services/clients/neo4j_client.py (New File/Class)
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import yaml # Must be imported for YAML output
|
|
8
|
+
# Import all model classes to make them available at the package level
|
|
9
|
+
from guardianhub.models.template.suggestion import TemplateSchemaSuggestion
|
|
10
|
+
|
|
11
|
+
from guardianhub.config.settings import settings
|
|
12
|
+
from guardianhub import get_logger
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
# NOTE: The actual driver must be injected/managed by the calling service (doc-template service)
|
|
16
|
+
class GraphDBClient:
|
|
17
|
+
def __init__(self, base_url: str, poll_interval: int = 5, poll_timeout: int = 300):
|
|
18
|
+
"""
|
|
19
|
+
Initializes the Paperless client.
|
|
20
|
+
"""
|
|
21
|
+
self.api_url = base_url.rstrip('/')
|
|
22
|
+
self.api_token = settings.endpoints.GRAPH_DB_URL
|
|
23
|
+
self.headers = {
|
|
24
|
+
"Accept": "application/json",
|
|
25
|
+
}
|
|
26
|
+
self.poll_interval = poll_interval
|
|
27
|
+
self.poll_timeout = poll_timeout
|
|
28
|
+
|
|
29
|
+
# Initialize the persistent httpx client here.
|
|
30
|
+
# DO NOT use it in an 'async with' block in methods, or it will be closed.
|
|
31
|
+
self.client = httpx.AsyncClient(headers=self.headers, base_url=self.api_url, timeout=self.poll_timeout + 60)
|
|
32
|
+
logger.info("PaperlessClient initialized for URL: %s", self.api_url)
|
|
33
|
+
|
|
34
|
+
async def save_document_template(self, template: TemplateSchemaSuggestion) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Creates a new DocumentTemplate node and links it to the Doc-Template Service
|
|
37
|
+
node by submitting a YAML payload to the ingestion endpoint.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# 1. Prepare Node Properties
|
|
41
|
+
template_properties = {
|
|
42
|
+
"template_id": template.template_id,
|
|
43
|
+
"document_type": template.document_type,
|
|
44
|
+
"template_name": template.template_name,
|
|
45
|
+
"required_keywords": template.required_keywords,
|
|
46
|
+
# The ingestion endpoint requires the schema to be stored as a property value.
|
|
47
|
+
# We must serialize the JSON schema dictionary to a string/YAML-safe string.
|
|
48
|
+
"json_schema_str": json.dumps(template.json_schema)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# 2. Construct the Graph Ingestion Dictionary (The YAML Payload Structure)
|
|
52
|
+
ingestion_payload = {
|
|
53
|
+
"nodes": [
|
|
54
|
+
{
|
|
55
|
+
"type": "DocumentTemplate",
|
|
56
|
+
"properties": template_properties
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"relationships": [
|
|
60
|
+
{
|
|
61
|
+
"from": {
|
|
62
|
+
"type": "PlatformService",
|
|
63
|
+
"property": "name",
|
|
64
|
+
"value": "doc-template-service" # Matching the service node created at startup
|
|
65
|
+
},
|
|
66
|
+
"to": {
|
|
67
|
+
"type": "DocumentTemplate",
|
|
68
|
+
"property": "template_id",
|
|
69
|
+
"value": template.template_id
|
|
70
|
+
},
|
|
71
|
+
"type": "MANAGES_TEMPLATE",
|
|
72
|
+
"properties": {
|
|
73
|
+
"link_date": datetime.datetime.now()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# 3. Convert to YAML
|
|
80
|
+
yaml_payload = yaml.dump(ingestion_payload, sort_keys=False)
|
|
81
|
+
|
|
82
|
+
# 4. POST to the ingestion endpoint
|
|
83
|
+
try:
|
|
84
|
+
response = await self.client.post(
|
|
85
|
+
"/ingest-yaml-schema",
|
|
86
|
+
content=yaml_payload,
|
|
87
|
+
headers={'Content-Type': 'application/x-yaml'},
|
|
88
|
+
timeout=30.0
|
|
89
|
+
)
|
|
90
|
+
response.raise_for_status()
|
|
91
|
+
|
|
92
|
+
response_json = response.json()
|
|
93
|
+
if response_json.get("status") == "success":
|
|
94
|
+
logger.info(f"✅ Template {template.template_id} successfully persisted via YAML ingestion.")
|
|
95
|
+
return True
|
|
96
|
+
else:
|
|
97
|
+
logger.error(
|
|
98
|
+
f"Graph DB Service returned non-success status for template {template.template_id}: {response_json.get('message')}")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
except httpx.HTTPStatusError as e:
|
|
102
|
+
logger.error(
|
|
103
|
+
f"HTTP error during YAML ingestion (Template {template.template_id}). Status: {e.response.status_code}. Detail: {e.response.text}",
|
|
104
|
+
exc_info=True
|
|
105
|
+
)
|
|
106
|
+
return False
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Failed to ingest template YAML for {template.template_id}: {e}", exc_info=True)
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
# services/clients/neo4j_client.py (Corrected get_template_by_id)
|
|
112
|
+
|
|
113
|
+
async def get_template_by_id(self, template_id: str) -> Optional[Dict[str, Any]]:
|
|
114
|
+
"""
|
|
115
|
+
Retrieves the properties of a DocumentTemplate node by its ID via the
|
|
116
|
+
/query-cypher endpoint.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
# 1. Define the Cypher query
|
|
120
|
+
cypher_query = """
|
|
121
|
+
MATCH (t:DocumentTemplate {template_id: $template_id})
|
|
122
|
+
RETURN properties(t) as template
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# 2. Prepare the JSON payload for the read endpoint
|
|
126
|
+
payload = {
|
|
127
|
+
"query": cypher_query,
|
|
128
|
+
"parameters": {"template_id": template_id}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
# 🛠️ FIX: Target the correct read endpoint and send JSON payload
|
|
133
|
+
response = await self.client.post(
|
|
134
|
+
"/query-cypher", # The correct read endpoint
|
|
135
|
+
json=payload, # Send as JSON
|
|
136
|
+
timeout=30.0
|
|
137
|
+
)
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
|
|
140
|
+
response_json = response.json()
|
|
141
|
+
|
|
142
|
+
if response_json.get("status") == "success" and response_json.get("results"):
|
|
143
|
+
# The result is typically a list of dicts from Cypher execution
|
|
144
|
+
record = response_json["results"][0]
|
|
145
|
+
template_data = record["template"]
|
|
146
|
+
|
|
147
|
+
# Convert the stored JSON Schema string back to a dictionary
|
|
148
|
+
template_data['json_schema'] = json.loads(template_data.pop('json_schema_str'))
|
|
149
|
+
|
|
150
|
+
logger.info(f"✅ Template {template_id} successfully retrieved from GraphDB.")
|
|
151
|
+
return template_data
|
|
152
|
+
else:
|
|
153
|
+
logger.info(f"Template {template_id} not found or query failed: {response_json.get('message')}")
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
except httpx.HTTPStatusError as e:
|
|
157
|
+
logger.error(f"HTTP error retrieving template {template_id}. Status: {e.response.status_code}")
|
|
158
|
+
return None
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Failed to retrieve template {template_id} from Neo4j: {e}", exc_info=True)
|
|
161
|
+
return None
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Langfuse dataset management client.
|
|
2
|
+
|
|
3
|
+
This module provides a client for managing datasets in Langfuse.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from langfuse import Langfuse
|
|
11
|
+
from langfuse._client.datasets import DatasetClient as LangfuseDatasetClient
|
|
12
|
+
from langfuse.model import Dataset, DatasetItem, DatasetRun
|
|
13
|
+
|
|
14
|
+
from guardianhub import get_logger
|
|
15
|
+
from .manager import LangfuseManager
|
|
16
|
+
|
|
17
|
+
LOGGER = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DatasetClient:
|
|
21
|
+
"""Client for Langfuse dataset management."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
client: Optional[Langfuse] = None,
|
|
26
|
+
public_key: Optional[str] = None,
|
|
27
|
+
secret_key: Optional[str] = None,
|
|
28
|
+
host: Optional[str] = None,
|
|
29
|
+
**kwargs
|
|
30
|
+
):
|
|
31
|
+
"""Initialize the DatasetClient.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
client: Optional Langfuse client instance. If not provided, will use LangfuseManager.
|
|
35
|
+
public_key: Langfuse public key. If not provided, will use LANGFUSE_PUBLIC_KEY from environment.
|
|
36
|
+
secret_key: Langfuse secret key. If not provided, will use LANGFUSE_SECRET_KEY from environment.
|
|
37
|
+
host: Langfuse host URL. If not provided, will use LANGFUSE_HOST from environment or default.
|
|
38
|
+
**kwargs: Additional arguments to pass to Langfuse client initialization.
|
|
39
|
+
"""
|
|
40
|
+
if client is not None:
|
|
41
|
+
self._client = client
|
|
42
|
+
else:
|
|
43
|
+
self._client = LangfuseManager.get_instance(
|
|
44
|
+
public_key=public_key,
|
|
45
|
+
secret_key=secret_key,
|
|
46
|
+
host=host,
|
|
47
|
+
**kwargs
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def create_dataset(self, name: str) -> Dataset:
|
|
51
|
+
"""Create a new dataset.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
name: Name of the dataset to create.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The created Dataset object.
|
|
58
|
+
"""
|
|
59
|
+
return self._client.create_dataset(name=name)
|
|
60
|
+
|
|
61
|
+
def get_dataset(self, name: str) -> LangfuseDatasetClient:
|
|
62
|
+
"""Get a dataset by name.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
name: Name of the dataset to retrieve.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The Dataset object if found, None otherwise.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If the Langfuse client is not initialized.
|
|
72
|
+
"""
|
|
73
|
+
if self._client is None:
|
|
74
|
+
raise ValueError("Langfuse client is not initialized")
|
|
75
|
+
return self._client.get_dataset(name=name)
|
|
76
|
+
|
|
77
|
+
def list_datasets(self) -> List[Dataset]:
|
|
78
|
+
"""List all datasets.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of Dataset objects.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: If the Langfuse client is not initialized.
|
|
85
|
+
"""
|
|
86
|
+
if self._client is None:
|
|
87
|
+
raise ValueError("Langfuse client is not initialized")
|
|
88
|
+
return self._client.list_datasets()
|
|
89
|
+
|
|
90
|
+
def create_dataset_item(
|
|
91
|
+
self,
|
|
92
|
+
dataset_name: str,
|
|
93
|
+
input: Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
94
|
+
expected_output: Optional[Union[Dict[str, Any], List[Dict[str, Any]]]] = None,
|
|
95
|
+
**kwargs
|
|
96
|
+
) -> DatasetItem:
|
|
97
|
+
"""Create a new item in a dataset.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
dataset_name: Name of the dataset to add the item to.
|
|
101
|
+
input: Input data for the dataset item.
|
|
102
|
+
expected_output: Expected output for the dataset item.
|
|
103
|
+
**kwargs: Additional arguments for the dataset item.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The created DatasetItem object.
|
|
107
|
+
"""
|
|
108
|
+
return self._client.create_dataset_item(
|
|
109
|
+
dataset_name=dataset_name,
|
|
110
|
+
input=input,
|
|
111
|
+
expected_output=expected_output,
|
|
112
|
+
**kwargs
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def get_dataset_items(self, dataset_name: str) -> List[DatasetItem]:
|
|
116
|
+
"""Get all items in a dataset.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
dataset_name: Name of the dataset.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List of DatasetItem objects.
|
|
123
|
+
"""
|
|
124
|
+
return self._client.get_dataset_items(dataset_name=dataset_name)
|
|
125
|
+
|
|
126
|
+
def create_dataset_run(
|
|
127
|
+
self,
|
|
128
|
+
dataset_name: str,
|
|
129
|
+
run_name: str,
|
|
130
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
131
|
+
) -> DatasetRun:
|
|
132
|
+
"""Create a new dataset run.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
dataset_name: Name of the dataset.
|
|
136
|
+
run_name: Name for the run.
|
|
137
|
+
metadata: Optional metadata for the run.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The created DatasetRun object.
|
|
141
|
+
"""
|
|
142
|
+
return self._client.create_dataset_run(
|
|
143
|
+
dataset_name=dataset_name,
|
|
144
|
+
run_name=run_name,
|
|
145
|
+
metadata=metadata or {}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def get_dataset_run(self, run_id: str) -> Optional[DatasetRun]:
|
|
149
|
+
"""Get a dataset run by ID.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
run_id: ID of the run to retrieve.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The DatasetRun object if found, None otherwise.
|
|
156
|
+
"""
|
|
157
|
+
return self._client.get_dataset_run(id=run_id)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Langfuse Manager for singleton connection management.
|
|
2
|
+
|
|
3
|
+
This module provides a manager that ensures only one instance of the Langfuse
|
|
4
|
+
client is created and used throughout the application.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from guardianhub import get_logger
|
|
14
|
+
from langfuse import Langfuse
|
|
15
|
+
|
|
16
|
+
LOGGER = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
# TODO: For future reference - ThreadPool impl to handle multiple connection
|
|
19
|
+
class LangfuseManager:
|
|
20
|
+
"""Manages the singleton instance of the Langfuse client."""
|
|
21
|
+
_client: Optional[Langfuse] = None
|
|
22
|
+
_initialized = False
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def get_instance(
|
|
26
|
+
cls,
|
|
27
|
+
public_key: Optional[str] = None,
|
|
28
|
+
secret_key: Optional[str] = None,
|
|
29
|
+
host: Optional[str] = None,
|
|
30
|
+
**kwargs
|
|
31
|
+
) -> Optional[Langfuse]:
|
|
32
|
+
"""
|
|
33
|
+
Get the singleton Langfuse client instance.
|
|
34
|
+
|
|
35
|
+
Initializes the client on the first call and returns the existing
|
|
36
|
+
instance on subsequent calls.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
public_key: Langfuse public key.
|
|
40
|
+
secret_key: Langfuse secret key.
|
|
41
|
+
host: Langfuse host URL.
|
|
42
|
+
**kwargs: Additional arguments for the Langfuse client.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The initialized Langfuse client instance, or None if initialization fails.
|
|
46
|
+
"""
|
|
47
|
+
LOGGER.info("🔍 Initializing Langfuse Service...")
|
|
48
|
+
|
|
49
|
+
if cls._initialized:
|
|
50
|
+
return cls._client
|
|
51
|
+
|
|
52
|
+
cls._initialized = True # Mark as initialized even if it fails, to prevent retries.
|
|
53
|
+
|
|
54
|
+
public_key = public_key or os.getenv("LANGFUSE_PUBLIC_KEY")
|
|
55
|
+
secret_key = secret_key or os.getenv("LANGFUSE_SECRET_KEY")
|
|
56
|
+
host = host or os.getenv("LANGFUSE_HOST")
|
|
57
|
+
|
|
58
|
+
if not public_key or not secret_key:
|
|
59
|
+
LOGGER.warning(
|
|
60
|
+
"Langfuse credentials not provided. Set LANGFUSE_PUBLIC_KEY and "
|
|
61
|
+
"LANGFUSE_SECRET_KEY. Langfuse client will not be initialized."
|
|
62
|
+
)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
cls._client = Langfuse(
|
|
67
|
+
public_key=public_key,
|
|
68
|
+
secret_key=secret_key,
|
|
69
|
+
host=host,
|
|
70
|
+
**kwargs
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if cls.check_langfuse_connection():
|
|
74
|
+
LOGGER.info("✅ Successfully initialized singleton Langfuse client.")
|
|
75
|
+
else:
|
|
76
|
+
LOGGER.warning("⚠️ Langfuse client initialization failed ")
|
|
77
|
+
|
|
78
|
+
return cls._client
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
LOGGER.exception("Failed to initialize singleton Langfuse client: %s", exc)
|
|
81
|
+
cls._client = None
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def check_langfuse_connection(cls) -> bool:
|
|
86
|
+
"""
|
|
87
|
+
Check if the Langfuse connection is valid by attempting to authenticate.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
bool: True if the connection is valid, False otherwise
|
|
91
|
+
"""
|
|
92
|
+
if not cls._client:
|
|
93
|
+
LOGGER.warning("Langfuse client is not initialized")
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Test the connection with a timeout to prevent hanging
|
|
98
|
+
is_authenticated = cls._client.auth_check()
|
|
99
|
+
if is_authenticated:
|
|
100
|
+
LOGGER.info("✅ Successfully authenticated with Langfuse")
|
|
101
|
+
return True
|
|
102
|
+
else:
|
|
103
|
+
LOGGER.warning("❌ Failed to authenticate with Langfuse - Invalid credentials")
|
|
104
|
+
return False
|
|
105
|
+
except httpx.ConnectError as e:
|
|
106
|
+
LOGGER.error(f"❌ Could not connect to Langfuse server: {str(e)}. "
|
|
107
|
+
"Please check your network connection and Langfuse server status.")
|
|
108
|
+
return False
|
|
109
|
+
except Exception as e:
|
|
110
|
+
LOGGER.error(f"❌ Error connecting to Langfuse: {str(e)}", exc_info=True)
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def flush(cls) -> None:
|
|
115
|
+
"""Flushes the managed Langfuse client instance."""
|
|
116
|
+
if cls._client:
|
|
117
|
+
cls._client.flush()
|
|
118
|
+
LOGGER.info("Langfuse manager flushed the client.")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Langfuse prompt management client.
|
|
2
|
+
|
|
3
|
+
This module provides a client for managing prompts in Langfuse.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Optional, Union
|
|
9
|
+
|
|
10
|
+
from langfuse import Langfuse
|
|
11
|
+
|
|
12
|
+
from guardianhub import get_logger
|
|
13
|
+
from .manager import LangfuseManager
|
|
14
|
+
|
|
15
|
+
LOGGER = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PromptClient:
|
|
19
|
+
"""Client for Langfuse prompt management."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
client: Optional[Langfuse] = None,
|
|
24
|
+
public_key: Optional[str] = None,
|
|
25
|
+
secret_key: Optional[str] = None,
|
|
26
|
+
host: Optional[str] = None,
|
|
27
|
+
**kwargs
|
|
28
|
+
):
|
|
29
|
+
"""Initialize the PromptClient.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
client: Optional Langfuse client instance. If not provided, will use LangfuseManager.
|
|
33
|
+
public_key: Langfuse public key. If not provided, will use LANGFUSE_PUBLIC_KEY from environment.
|
|
34
|
+
secret_key: Langfuse secret key. If not provided, will use LANGFUSE_SECRET_KEY from environment.
|
|
35
|
+
host: Langfuse host URL. If not provided, will use LANGFUSE_HOST from environment or default.
|
|
36
|
+
**kwargs: Additional arguments to pass to Langfuse client initialization.
|
|
37
|
+
"""
|
|
38
|
+
if client is not None:
|
|
39
|
+
self._client = client
|
|
40
|
+
else:
|
|
41
|
+
self._client = LangfuseManager.get_instance(
|
|
42
|
+
public_key=public_key,
|
|
43
|
+
secret_key=secret_key,
|
|
44
|
+
host=host,
|
|
45
|
+
**kwargs
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def get_prompt(self, name: str, version: Optional[int] = None) -> Optional[str]:
|
|
49
|
+
"""
|
|
50
|
+
Retrieves a prompt from Langfuse.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
name: The name of the prompt.
|
|
54
|
+
version: The version of the prompt (optional).
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The prompt string, or None if not found.
|
|
58
|
+
"""
|
|
59
|
+
if not self._client:
|
|
60
|
+
LOGGER.warning("Langfuse client not initialized. Cannot fetch prompt.")
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
prompt = self._client.get_prompt(name, version)
|
|
65
|
+
return prompt.prompt
|
|
66
|
+
except Exception as e:
|
|
67
|
+
LOGGER.error("Failed to retrieve prompt '%s': %s", name, e)
|
|
68
|
+
return None
|