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,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()