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.
Files changed (64) hide show
  1. guardianhub/__init__.py +29 -0
  2. guardianhub/_version.py +1 -0
  3. guardianhub/agents/runtime.py +12 -0
  4. guardianhub/auth/token_provider.py +22 -0
  5. guardianhub/clients/__init__.py +2 -0
  6. guardianhub/clients/classification_client.py +52 -0
  7. guardianhub/clients/graph_db_client.py +161 -0
  8. guardianhub/clients/langfuse/dataset_client.py +157 -0
  9. guardianhub/clients/langfuse/manager.py +118 -0
  10. guardianhub/clients/langfuse/prompt_client.py +68 -0
  11. guardianhub/clients/langfuse/score_evaluation_client.py +92 -0
  12. guardianhub/clients/langfuse/tracing_client.py +250 -0
  13. guardianhub/clients/langfuse_client.py +63 -0
  14. guardianhub/clients/llm_client.py +144 -0
  15. guardianhub/clients/llm_service.py +295 -0
  16. guardianhub/clients/metadata_extractor_client.py +53 -0
  17. guardianhub/clients/ocr_client.py +81 -0
  18. guardianhub/clients/paperless_client.py +515 -0
  19. guardianhub/clients/registry_client.py +18 -0
  20. guardianhub/clients/text_cleaner_client.py +58 -0
  21. guardianhub/clients/vector_client.py +344 -0
  22. guardianhub/config/__init__.py +0 -0
  23. guardianhub/config/config_development.json +84 -0
  24. guardianhub/config/config_prod.json +39 -0
  25. guardianhub/config/settings.py +221 -0
  26. guardianhub/http/http_client.py +26 -0
  27. guardianhub/logging/__init__.py +2 -0
  28. guardianhub/logging/logging.py +168 -0
  29. guardianhub/logging/logging_filters.py +35 -0
  30. guardianhub/models/__init__.py +0 -0
  31. guardianhub/models/agent_models.py +153 -0
  32. guardianhub/models/base.py +2 -0
  33. guardianhub/models/registry/client.py +16 -0
  34. guardianhub/models/registry/dynamic_loader.py +73 -0
  35. guardianhub/models/registry/loader.py +37 -0
  36. guardianhub/models/registry/registry.py +17 -0
  37. guardianhub/models/registry/signing.py +70 -0
  38. guardianhub/models/template/__init__.py +0 -0
  39. guardianhub/models/template/agent_plan.py +65 -0
  40. guardianhub/models/template/agent_response_evaluation.py +67 -0
  41. guardianhub/models/template/extraction.py +29 -0
  42. guardianhub/models/template/reflection_critique.py +206 -0
  43. guardianhub/models/template/suggestion.py +42 -0
  44. guardianhub/observability/__init__.py +1 -0
  45. guardianhub/observability/instrumentation.py +271 -0
  46. guardianhub/observability/otel_helper.py +43 -0
  47. guardianhub/observability/otel_middlewares.py +73 -0
  48. guardianhub/prompts/base.py +7 -0
  49. guardianhub/prompts/providers/langfuse_provider.py +13 -0
  50. guardianhub/prompts/providers/local_provider.py +22 -0
  51. guardianhub/prompts/registry.py +14 -0
  52. guardianhub/scripts/script.sh +31 -0
  53. guardianhub/services/base.py +15 -0
  54. guardianhub/template/__init__.py +0 -0
  55. guardianhub/tools/gh_registry_cli.py +171 -0
  56. guardianhub/utils/__init__.py +0 -0
  57. guardianhub/utils/app_state.py +74 -0
  58. guardianhub/utils/fastapi_utils.py +152 -0
  59. guardianhub/utils/json_utils.py +137 -0
  60. guardianhub/utils/metrics.py +60 -0
  61. guardianhub-0.1.88.dist-info/METADATA +240 -0
  62. guardianhub-0.1.88.dist-info/RECORD +64 -0
  63. guardianhub-0.1.88.dist-info/WHEEL +4 -0
  64. guardianhub-0.1.88.dist-info/licenses/LICENSE +21 -0
@@ -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
+ ]
@@ -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,2 @@
1
+ from .vector_client import VectorClient
2
+ from .llm_client import LLMClient
@@ -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