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,92 @@
|
|
|
1
|
+
"""Langfuse evaluation client.
|
|
2
|
+
|
|
3
|
+
This module provides a client for running evaluations and scoring in Langfuse.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from langfuse import Langfuse
|
|
9
|
+
from guardianhub import get_logger
|
|
10
|
+
from .manager import LangfuseManager
|
|
11
|
+
|
|
12
|
+
LOGGER = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
class EvaluationClient:
|
|
15
|
+
"""Client for Langfuse evaluations."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
client: Optional[Langfuse] = None,
|
|
20
|
+
public_key: Optional[str] = None,
|
|
21
|
+
secret_key: Optional[str] = None,
|
|
22
|
+
host: Optional[str] = None,
|
|
23
|
+
**kwargs
|
|
24
|
+
):
|
|
25
|
+
"""Initialize the EvaluationClient.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
client: Optional Langfuse client instance. If not provided, will use LangfuseManager.
|
|
29
|
+
public_key: Langfuse public key. If not provided, will use LANGFUSE_PUBLIC_KEY from environment.
|
|
30
|
+
secret_key: Langfuse secret key. If not provided, will use LANGFUSE_SECRET_KEY from environment.
|
|
31
|
+
host: Langfuse host URL. If not provided, will use LANGFUSE_HOST from environment or default.
|
|
32
|
+
**kwargs: Additional arguments to pass to Langfuse client initialization.
|
|
33
|
+
"""
|
|
34
|
+
if client is not None:
|
|
35
|
+
self._client = client
|
|
36
|
+
else:
|
|
37
|
+
self._client = LangfuseManager.get_instance(
|
|
38
|
+
public_key=public_key,
|
|
39
|
+
secret_key=secret_key,
|
|
40
|
+
host=host,
|
|
41
|
+
**kwargs
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def score_trace(self, trace_id: str, name: str, value: float, comment: Optional[str] = None) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Scores a trace in Langfuse.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
trace_id: The ID of the trace to score.
|
|
50
|
+
name: The name of the score (e.g., "user-feedback").
|
|
51
|
+
value: The score value.
|
|
52
|
+
comment: An optional comment.
|
|
53
|
+
"""
|
|
54
|
+
if not self._client:
|
|
55
|
+
LOGGER.warning("Langfuse client not initialized. Cannot score trace.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
self._client.score(
|
|
60
|
+
trace_id=trace_id,
|
|
61
|
+
name=name,
|
|
62
|
+
value=value,
|
|
63
|
+
comment=comment,
|
|
64
|
+
)
|
|
65
|
+
LOGGER.info("Successfully scored trace %s with score '%s'", trace_id, name)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
LOGGER.error("Failed to score trace %s: %s", trace_id, e)
|
|
68
|
+
|
|
69
|
+
def score_span(self, span_id: str, name: str, value: float, comment: Optional[str] = None) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Scores a span in Langfuse.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
span_id: The ID of the span to score.
|
|
75
|
+
name: The name of the score.
|
|
76
|
+
value: The score value.
|
|
77
|
+
comment: An optional comment.
|
|
78
|
+
"""
|
|
79
|
+
if not self._client:
|
|
80
|
+
LOGGER.warning("Langfuse client not initialized. Cannot score span.")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
self._client.score(
|
|
85
|
+
observation_id=span_id,
|
|
86
|
+
name=name,
|
|
87
|
+
value=value,
|
|
88
|
+
comment=comment,
|
|
89
|
+
)
|
|
90
|
+
LOGGER.info("Successfully scored span %s with score '%s'", span_id, name)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
LOGGER.error("Failed to score span %s: %s", span_id, e)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Langfuse tracing client for observability.
|
|
2
|
+
|
|
3
|
+
This module provides a client for tracing and logging.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from langfuse import Langfuse, LangfuseSpan
|
|
12
|
+
from langfuse.api.client import TraceClient as Trace
|
|
13
|
+
|
|
14
|
+
from guardianhub import get_logger
|
|
15
|
+
from .manager import LangfuseManager
|
|
16
|
+
|
|
17
|
+
LOGGER = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TracingClient:
|
|
21
|
+
"""Client for Langfuse tracing, spans, and events."""
|
|
22
|
+
_current_trace: ContextVar[Optional[Trace]] = ContextVar("_current_trace", default=None)
|
|
23
|
+
_current_span: ContextVar[Optional[LangfuseSpan]] = ContextVar("_current_span", default=None)
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
client: Optional[Langfuse] = None,
|
|
28
|
+
public_key: Optional[str] = None,
|
|
29
|
+
secret_key: Optional[str] = None,
|
|
30
|
+
host: Optional[str] = None,
|
|
31
|
+
**kwargs
|
|
32
|
+
):
|
|
33
|
+
"""Initialize the TracingClient.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
client: Optional Langfuse client instance. If not provided, will use LangfuseManager.
|
|
37
|
+
public_key: Langfuse public key. If not provided, will use LANGFUSE_PUBLIC_KEY from environment.
|
|
38
|
+
secret_key: Langfuse secret key. If not provided, will use LANGFUSE_SECRET_KEY from environment.
|
|
39
|
+
host: Langfuse host URL. If not provided, will use LANGFUSE_HOST from environment or default.
|
|
40
|
+
**kwargs: Additional arguments to pass to Langfuse client initialization.
|
|
41
|
+
"""
|
|
42
|
+
if client is not None:
|
|
43
|
+
self._client = client
|
|
44
|
+
else:
|
|
45
|
+
self._client = LangfuseManager.get_instance(
|
|
46
|
+
public_key=public_key,
|
|
47
|
+
secret_key=secret_key,
|
|
48
|
+
host=host,
|
|
49
|
+
**kwargs
|
|
50
|
+
)
|
|
51
|
+
self.default_trace_tags: Dict[str, Any] = {}
|
|
52
|
+
self.default_span_tags: Dict[str, Any] = {}
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def is_initialized(self) -> bool:
|
|
56
|
+
return self._client is not None
|
|
57
|
+
|
|
58
|
+
def set_default_trace_tags(self, tags: Dict[str, Any]) -> None:
|
|
59
|
+
self.default_trace_tags.update(tags)
|
|
60
|
+
|
|
61
|
+
def set_default_span_tags(self, tags: Dict[str, Any]) -> None:
|
|
62
|
+
self.default_span_tags.update(tags)
|
|
63
|
+
|
|
64
|
+
def get_current_trace(self) -> Optional[Trace]:
|
|
65
|
+
return self._current_trace.get()
|
|
66
|
+
|
|
67
|
+
def get_current_span(self) -> Optional[LangfuseSpan]:
|
|
68
|
+
"""Get the current active span.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Optional[LangfuseSpan]: The current active span, or None if no span is active.
|
|
72
|
+
"""
|
|
73
|
+
return self._current_span.get()
|
|
74
|
+
|
|
75
|
+
def start_trace(
|
|
76
|
+
self,
|
|
77
|
+
name: str,
|
|
78
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
79
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
80
|
+
**kwargs
|
|
81
|
+
) -> Optional[Trace]:
|
|
82
|
+
if not self.is_initialized:
|
|
83
|
+
LOGGER.debug("Langfuse client not initialized. Skipping trace creation.")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
if not name or not isinstance(name, str):
|
|
87
|
+
raise ValueError("Trace name must be a non-empty string")
|
|
88
|
+
|
|
89
|
+
merged_tags = {**self.default_trace_tags, **(tags or {})}
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
trace = self._create_trace(
|
|
93
|
+
name=name,
|
|
94
|
+
tags=merged_tags,
|
|
95
|
+
metadata=metadata or {},
|
|
96
|
+
**kwargs
|
|
97
|
+
)
|
|
98
|
+
self._current_trace.set(trace)
|
|
99
|
+
LOGGER.debug("Started trace: %s", name)
|
|
100
|
+
return trace
|
|
101
|
+
except Exception as e:
|
|
102
|
+
LOGGER.error("Failed to start trace '%s': %s", name, str(e), exc_info=True)
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def end_trace(self, trace: Optional[Trace] = None) -> None:
|
|
106
|
+
trace_to_end = trace or self.get_current_trace()
|
|
107
|
+
if not trace_to_end:
|
|
108
|
+
LOGGER.debug("No active trace to end")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
self._end_trace(trace_to_end)
|
|
113
|
+
LOGGER.debug("Ended trace: %s", getattr(trace_to_end, 'id', 'unknown'))
|
|
114
|
+
except Exception as e:
|
|
115
|
+
LOGGER.exception("Error ending trace: %s", str(e))
|
|
116
|
+
finally:
|
|
117
|
+
if self.get_current_trace() is trace_to_end:
|
|
118
|
+
self._current_trace.set(None)
|
|
119
|
+
self._current_span.set(None)
|
|
120
|
+
|
|
121
|
+
def start_span(
|
|
122
|
+
self,
|
|
123
|
+
name: str,
|
|
124
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
125
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
126
|
+
**kwargs
|
|
127
|
+
) -> Optional[LangfuseSpan]:
|
|
128
|
+
if not self.is_initialized:
|
|
129
|
+
LOGGER.debug("Langfuse client not initialized. Skipping span creation.")
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
if not name or not isinstance(name, str):
|
|
133
|
+
raise ValueError("Span name must be a non-empty string")
|
|
134
|
+
|
|
135
|
+
trace = self.get_current_trace()
|
|
136
|
+
|
|
137
|
+
if not trace:
|
|
138
|
+
trace_name = f"auto-trace-{name}"
|
|
139
|
+
LOGGER.debug("No active trace found. Creating new trace: %s", trace_name)
|
|
140
|
+
trace = self.start_trace(name=trace_name, tags=tags, metadata=metadata)
|
|
141
|
+
if not trace:
|
|
142
|
+
LOGGER.error("Failed to auto-create trace for span '%s'", name)
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
merged_tags = {**self.default_span_tags, **(tags or {})}
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
span = self._create_span(trace, name, merged_tags, metadata or {}, **kwargs)
|
|
149
|
+
self._current_span.set(span)
|
|
150
|
+
LOGGER.debug("Started span '%s' in trace %s", name, getattr(trace, 'id', 'unknown'))
|
|
151
|
+
return span
|
|
152
|
+
except Exception as e:
|
|
153
|
+
LOGGER.error("Failed to start span '%s': %s", name, str(e), exc_info=True)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def end_span(self, span: Optional[LangfuseSpan] = None) -> None:
|
|
157
|
+
span_to_end = span or self.get_current_span()
|
|
158
|
+
if not span_to_end:
|
|
159
|
+
LOGGER.debug("No active span to end")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
self._end_span(span_to_end)
|
|
164
|
+
LOGGER.debug("Ended span: %s", getattr(span_to_end, 'id', 'unknown'))
|
|
165
|
+
except Exception as e:
|
|
166
|
+
LOGGER.exception("Error ending span: %s", str(e))
|
|
167
|
+
finally:
|
|
168
|
+
if self.get_current_span() is span_to_end:
|
|
169
|
+
self._current_span.set(None)
|
|
170
|
+
|
|
171
|
+
def start_agent_trace(self, agent_name: str, agent_version: Optional[str] = None,
|
|
172
|
+
task_type: Optional[str] = None, domain: Optional[str] = None,
|
|
173
|
+
metadata: Optional[Dict[str, Any]] = None) -> Optional[Trace]:
|
|
174
|
+
tags = {"agent_name": agent_name}
|
|
175
|
+
if agent_version:
|
|
176
|
+
tags["agent_version"] = agent_version
|
|
177
|
+
if task_type:
|
|
178
|
+
tags["task_type"] = task_type
|
|
179
|
+
if domain:
|
|
180
|
+
tags["domain"] = domain
|
|
181
|
+
|
|
182
|
+
return self.start_trace(name=f"agent:{agent_name}", tags=tags, metadata=metadata)
|
|
183
|
+
|
|
184
|
+
def log_agent_step(self, step_type: str, input_data: Optional[Any] = None, output_data: Optional[Any] = None,
|
|
185
|
+
metadata: Optional[Dict[str, Any]] = None) -> Optional[LangfuseSpan]:
|
|
186
|
+
metadata = metadata or {}
|
|
187
|
+
metadata.update({"input": input_data, "output": output_data})
|
|
188
|
+
return self.start_span(name=f"step:{step_type}", tags={"step_type": step_type}, metadata=metadata)
|
|
189
|
+
|
|
190
|
+
def log_tool_call(self, tool_name: str, status: str = "success", error: Optional[str] = None,
|
|
191
|
+
metadata: Optional[Dict[str, Any]] = None) -> Optional[LangfuseSpan]:
|
|
192
|
+
metadata = metadata or {}
|
|
193
|
+
if error:
|
|
194
|
+
metadata["error_message"] = error
|
|
195
|
+
span = self.start_span(name=f"tool:{tool_name}", tags={"tool_name": tool_name, "tool_status": status},
|
|
196
|
+
metadata=metadata)
|
|
197
|
+
if span:
|
|
198
|
+
self.end_span(span)
|
|
199
|
+
return span
|
|
200
|
+
|
|
201
|
+
def log_reflection_event(self, event_name: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
202
|
+
trace = self.get_current_trace()
|
|
203
|
+
if not trace:
|
|
204
|
+
LOGGER.warning("No active trace for reflection event %s", event_name)
|
|
205
|
+
return
|
|
206
|
+
try:
|
|
207
|
+
trace.event(name=event_name, metadata=metadata or {})
|
|
208
|
+
except Exception:
|
|
209
|
+
LOGGER.exception("Failed to log reflection event")
|
|
210
|
+
|
|
211
|
+
def _create_trace(
|
|
212
|
+
self,
|
|
213
|
+
name: str,
|
|
214
|
+
tags: Dict[str, Any],
|
|
215
|
+
metadata: Dict[str, Any],
|
|
216
|
+
**kwargs
|
|
217
|
+
) -> Trace:
|
|
218
|
+
if not self._client:
|
|
219
|
+
raise RuntimeError("Langfuse client is not initialized")
|
|
220
|
+
|
|
221
|
+
tag_list = [f"{k}:{v}" for k, v in tags.items()]
|
|
222
|
+
|
|
223
|
+
return self._client.trace(
|
|
224
|
+
name=name,
|
|
225
|
+
metadata=metadata,
|
|
226
|
+
tags=tag_list,
|
|
227
|
+
**kwargs
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _end_trace(self, trace: Trace) -> None:
|
|
231
|
+
trace.end()
|
|
232
|
+
|
|
233
|
+
def _create_span(
|
|
234
|
+
self,
|
|
235
|
+
trace: Trace,
|
|
236
|
+
name: str,
|
|
237
|
+
tags: Dict[str, Any],
|
|
238
|
+
metadata: Dict[str, Any],
|
|
239
|
+
**kwargs
|
|
240
|
+
) -> LangfuseSpan:
|
|
241
|
+
tag_list = [f"{k}:{v}" for k, v in tags.items()]
|
|
242
|
+
return trace.span(
|
|
243
|
+
name=name,
|
|
244
|
+
metadata=metadata,
|
|
245
|
+
tags=tag_list,
|
|
246
|
+
**kwargs
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _end_span(self, span: LangfuseSpan) -> None:
|
|
250
|
+
span.end()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Main Langfuse client for Guardian Hub SDK.
|
|
2
|
+
|
|
3
|
+
This module provides a centralized client that acts as a facade for various
|
|
4
|
+
Langfuse functionalities, including tracing, prompt management, and evaluations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .langfuse.score_evaluation_client import EvaluationClient
|
|
13
|
+
from .langfuse.manager import LangfuseManager
|
|
14
|
+
from .langfuse.prompt_client import PromptClient
|
|
15
|
+
from .langfuse.tracing_client import TracingClient
|
|
16
|
+
|
|
17
|
+
from guardianhub import get_logger
|
|
18
|
+
|
|
19
|
+
LOGGER = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LangfuseClient:
|
|
23
|
+
"""A facade client for Langfuse, providing access to tracing, prompts, and evaluations.
|
|
24
|
+
|
|
25
|
+
This client centralizes access to Langfuse functionalities by using a manager
|
|
26
|
+
to handle the singleton connection.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
public_key: Langfuse public key (optional, can be set via LANGFUSE_PUBLIC_KEY env var).
|
|
30
|
+
secret_key: Langfuse secret key (optional, can be set via LANGFUSE_SECRET_KEY env var).
|
|
31
|
+
host: Langfuse host URL (optional, can be set via LANGFUSE_HOST env var).
|
|
32
|
+
**kwargs: Additional arguments to pass to the Langfuse client constructor.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
public_key: Optional[str] = None,
|
|
38
|
+
secret_key: Optional[str] = None,
|
|
39
|
+
host: Optional[str] = None,
|
|
40
|
+
**kwargs
|
|
41
|
+
):
|
|
42
|
+
"""Initialize the main Langfuse client and its sub-clients."""
|
|
43
|
+
# Get the singleton Langfuse instance from the manager
|
|
44
|
+
self._client = LangfuseManager.get_instance(
|
|
45
|
+
public_key=public_key,
|
|
46
|
+
secret_key=secret_key,
|
|
47
|
+
host=host,
|
|
48
|
+
**kwargs
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Initialize specialized clients, passing the core client instance
|
|
52
|
+
self.tracing = TracingClient(self._client)
|
|
53
|
+
self.prompts = PromptClient(self._client)
|
|
54
|
+
self.evaluations = EvaluationClient(self._client)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_initialized(self) -> bool:
|
|
58
|
+
"""Check if the core Langfuse client is initialized."""
|
|
59
|
+
return self._client is not None
|
|
60
|
+
|
|
61
|
+
def flush(self) -> None:
|
|
62
|
+
"""Flush the Langfuse client to ensure all buffered data is sent."""
|
|
63
|
+
LangfuseManager.flush()
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# llm/llm_service.py
|
|
2
|
+
import json
|
|
3
|
+
from typing import Dict, Any, Optional, Type, TypeVar, List, Union
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from pydantic import BaseModel, ValidationError
|
|
7
|
+
|
|
8
|
+
from guardianhub import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
from guardianhub.config import settings
|
|
12
|
+
|
|
13
|
+
T = TypeVar('T', bound=BaseModel)
|
|
14
|
+
|
|
15
|
+
class LLMClient:
|
|
16
|
+
"""Unified client for LLM interactions with structured output support."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, base_url: str, api_key: Optional[str] = None):
|
|
19
|
+
self.base_url = base_url.rstrip('/')
|
|
20
|
+
self.client = httpx.AsyncClient(
|
|
21
|
+
base_url=base_url,
|
|
22
|
+
headers={"Authorization": f"Bearer {api_key}"} if api_key else {},
|
|
23
|
+
timeout=240.0
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
async def chat_completion(
|
|
27
|
+
self,
|
|
28
|
+
messages: List[Dict[str, str]],
|
|
29
|
+
model_key: str = None,
|
|
30
|
+
temperature: Optional[float] = None,
|
|
31
|
+
max_tokens: Optional[int] = None,
|
|
32
|
+
response_format: Optional[Dict[str, str]] = None,
|
|
33
|
+
response_model: Optional[Type[T]] = None,
|
|
34
|
+
**kwargs
|
|
35
|
+
) -> Union[Dict[str, Any], T]:
|
|
36
|
+
"""
|
|
37
|
+
Send a chat completion request to the LLM service.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
messages: List of message dictionaries with 'role' and 'content'
|
|
41
|
+
model_key: The model key to use
|
|
42
|
+
temperature: Sampling temperature (0-2)
|
|
43
|
+
max_tokens: Maximum number of tokens to generate
|
|
44
|
+
response_format: Optional format specification for the response
|
|
45
|
+
response_model: Optional Pydantic model for structured output
|
|
46
|
+
**kwargs: Additional arguments to pass to the API
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dict containing the API response or an instance of response_model if provided
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
RuntimeError: If the API request fails
|
|
53
|
+
ValidationError: If response_model is provided and the response doesn't match the schema
|
|
54
|
+
"""
|
|
55
|
+
# Set defaults from settings if not provided
|
|
56
|
+
model_key = model_key or getattr(settings.llm, 'model_key', 'default')
|
|
57
|
+
temperature = temperature if temperature is not None else getattr(settings.llm, 'temperature', 0.7)
|
|
58
|
+
max_tokens = max_tokens if max_tokens is not None else getattr(settings.llm, 'max_tokens', 1000)
|
|
59
|
+
|
|
60
|
+
# Prepare the base payload
|
|
61
|
+
payload = {
|
|
62
|
+
"model": getattr(settings.llm, 'model_name', 'gpt-3.5-turbo'),
|
|
63
|
+
"model_key": model_key,
|
|
64
|
+
"messages": messages,
|
|
65
|
+
"temperature": temperature,
|
|
66
|
+
"max_tokens": max_tokens,
|
|
67
|
+
**{k: v for k, v in kwargs.items() if v is not None}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# For structured output, include the schema in the request
|
|
71
|
+
if response_model is not None:
|
|
72
|
+
payload["response_schema"] = response_model.model_json_schema()
|
|
73
|
+
payload["response_model"] = response_model.__name__
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
response = await self.client.post(
|
|
77
|
+
"/v1/chat/completions",
|
|
78
|
+
json=payload,
|
|
79
|
+
timeout=360.0
|
|
80
|
+
)
|
|
81
|
+
response.raise_for_status()
|
|
82
|
+
response_data = response.json()
|
|
83
|
+
|
|
84
|
+
logger.debug(f"Received Response Data: {response_data}")
|
|
85
|
+
|
|
86
|
+
if "choices" in response_data:
|
|
87
|
+
content = response_data["choices"][0]["message"].get("content")
|
|
88
|
+
|
|
89
|
+
if content and isinstance(content, str):
|
|
90
|
+
try:
|
|
91
|
+
content = json.loads(content)
|
|
92
|
+
except json.JSONDecodeError:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
if response_model and content:
|
|
96
|
+
try:
|
|
97
|
+
return response_model.parse_obj(content)
|
|
98
|
+
except ValidationError as e:
|
|
99
|
+
logger.error(f"Response validation failed: {e}")
|
|
100
|
+
logger.debug(f"Response content: {content}")
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
return content or response_data
|
|
104
|
+
|
|
105
|
+
return response_data
|
|
106
|
+
|
|
107
|
+
except httpx.HTTPStatusError as e:
|
|
108
|
+
error_msg = "LLM API error ({}): {}".format(e.response.status_code, e.response.text)
|
|
109
|
+
logger.error(error_msg)
|
|
110
|
+
raise RuntimeError("LLM service error: {}".format(error_msg)) from e
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error("Unexpected error: {}".format(str(e)), exc_info=True)
|
|
113
|
+
raise RuntimeError("LLM client error: {}".format(str(e))) from e
|
|
114
|
+
|
|
115
|
+
async def generate_structured(
|
|
116
|
+
self,
|
|
117
|
+
messages: List[Dict[str, str]],
|
|
118
|
+
response_model: Type[T],
|
|
119
|
+
**kwargs
|
|
120
|
+
) -> T:
|
|
121
|
+
"""
|
|
122
|
+
Generate a structured response using the specified model.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
messages: List of message dictionaries with 'role' and 'content'
|
|
126
|
+
response_model: Pydantic model class for the expected output structure
|
|
127
|
+
**kwargs: Additional arguments to pass to chat_completion
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
An instance of the provided response_model with the generated content
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
RuntimeError: If the API request fails
|
|
134
|
+
ValidationError: If the response doesn't match the schema
|
|
135
|
+
"""
|
|
136
|
+
return await self.chat_completion(
|
|
137
|
+
messages=messages,
|
|
138
|
+
response_model=response_model,
|
|
139
|
+
**kwargs
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
async def close(self):
|
|
143
|
+
"""Close the HTTP client."""
|
|
144
|
+
await self.client.aclose()
|