airbyte-agent-mcp 0.1.33__py3-none-any.whl → 0.1.60__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 (31) hide show
  1. airbyte_agent_mcp/_vendored/connector_sdk/auth_strategies.py +2 -5
  2. airbyte_agent_mcp/_vendored/connector_sdk/auth_template.py +1 -1
  3. airbyte_agent_mcp/_vendored/connector_sdk/cloud_utils/client.py +26 -26
  4. airbyte_agent_mcp/_vendored/connector_sdk/connector_model_loader.py +11 -4
  5. airbyte_agent_mcp/_vendored/connector_sdk/constants.py +1 -1
  6. airbyte_agent_mcp/_vendored/connector_sdk/executor/hosted_executor.py +10 -11
  7. airbyte_agent_mcp/_vendored/connector_sdk/executor/local_executor.py +163 -34
  8. airbyte_agent_mcp/_vendored/connector_sdk/extensions.py +43 -5
  9. airbyte_agent_mcp/_vendored/connector_sdk/http/response.py +2 -0
  10. airbyte_agent_mcp/_vendored/connector_sdk/http_client.py +50 -43
  11. airbyte_agent_mcp/_vendored/connector_sdk/introspection.py +262 -0
  12. airbyte_agent_mcp/_vendored/connector_sdk/logging/logger.py +9 -9
  13. airbyte_agent_mcp/_vendored/connector_sdk/logging/types.py +10 -10
  14. airbyte_agent_mcp/_vendored/connector_sdk/observability/config.py +179 -0
  15. airbyte_agent_mcp/_vendored/connector_sdk/observability/models.py +6 -6
  16. airbyte_agent_mcp/_vendored/connector_sdk/observability/session.py +41 -32
  17. airbyte_agent_mcp/_vendored/connector_sdk/performance/metrics.py +3 -3
  18. airbyte_agent_mcp/_vendored/connector_sdk/schema/base.py +20 -18
  19. airbyte_agent_mcp/_vendored/connector_sdk/schema/components.py +59 -58
  20. airbyte_agent_mcp/_vendored/connector_sdk/schema/connector.py +22 -33
  21. airbyte_agent_mcp/_vendored/connector_sdk/schema/extensions.py +102 -9
  22. airbyte_agent_mcp/_vendored/connector_sdk/schema/operations.py +32 -32
  23. airbyte_agent_mcp/_vendored/connector_sdk/schema/security.py +44 -34
  24. airbyte_agent_mcp/_vendored/connector_sdk/secrets.py +2 -2
  25. airbyte_agent_mcp/_vendored/connector_sdk/telemetry/events.py +9 -8
  26. airbyte_agent_mcp/_vendored/connector_sdk/telemetry/tracker.py +9 -5
  27. airbyte_agent_mcp/_vendored/connector_sdk/types.py +7 -3
  28. airbyte_agent_mcp/server.py +34 -1
  29. {airbyte_agent_mcp-0.1.33.dist-info → airbyte_agent_mcp-0.1.60.dist-info}/METADATA +1 -1
  30. {airbyte_agent_mcp-0.1.33.dist-info → airbyte_agent_mcp-0.1.60.dist-info}/RECORD +31 -29
  31. {airbyte_agent_mcp-0.1.33.dist-info → airbyte_agent_mcp-0.1.60.dist-info}/WHEEL +0 -0
@@ -159,6 +159,38 @@ Example:
159
159
  ```
160
160
  """
161
161
 
162
+ AIRBYTE_STREAM_NAME = "x-airbyte-stream-name"
163
+ """
164
+ Extension: x-airbyte-stream-name
165
+ Location: Schema object (in components.schemas)
166
+ Type: string
167
+ Required: No
168
+
169
+ Description:
170
+ Specifies the Airbyte stream name for cache lookup purposes. This maps the entity
171
+ to the corresponding Airbyte stream, enabling cache-based data retrieval. When
172
+ specified, the EntityDefinition.stream_name field will be populated with this value.
173
+
174
+ This extension is placed on Schema objects alongside x-airbyte-entity-name, following
175
+ the same pattern. The stream name is an entity-level property (not operation-level)
176
+ since an entity maps to exactly one Airbyte stream.
177
+
178
+ Example:
179
+ ```yaml
180
+ components:
181
+ schemas:
182
+ Customer:
183
+ type: object
184
+ x-airbyte-entity-name: customers
185
+ x-airbyte-stream-name: customers
186
+ properties:
187
+ id:
188
+ type: string
189
+ name:
190
+ type: string
191
+ ```
192
+ """
193
+
162
194
  AIRBYTE_TOKEN_PATH = "x-airbyte-token-path"
163
195
  """
164
196
  Extension: x-airbyte-token-path
@@ -495,8 +527,8 @@ class ActionType(str, Enum):
495
527
  DELETE = "delete"
496
528
  """Delete a record"""
497
529
 
498
- SEARCH = "search"
499
- """Search for records matching specific query criteria"""
530
+ API_SEARCH = "api_search"
531
+ """Search for records matching specific query criteria via API"""
500
532
 
501
533
  DOWNLOAD = "download"
502
534
  """Download file content from a URL specified in the metadata response"""
@@ -514,7 +546,7 @@ class BodyType(str, Enum):
514
546
 
515
547
 
516
548
  # Type alias for use in Pydantic models
517
- ActionTypeLiteral = Literal["get", "list", "create", "update", "delete", "search", "download"]
549
+ ActionTypeLiteral = Literal["get", "list", "create", "update", "delete", "api_search", "download"]
518
550
 
519
551
 
520
552
  # =============================================================================
@@ -548,6 +580,7 @@ def get_all_extension_names() -> list[str]:
548
580
  AIRBYTE_ENTITY,
549
581
  AIRBYTE_ACTION,
550
582
  AIRBYTE_ENTITY_NAME,
583
+ AIRBYTE_STREAM_NAME,
551
584
  AIRBYTE_TOKEN_PATH,
552
585
  AIRBYTE_BODY_TYPE,
553
586
  AIRBYTE_PATH_OVERRIDE,
@@ -594,6 +627,12 @@ EXTENSION_REGISTRY = {
594
627
  "required": False,
595
628
  "description": "Links schema to an entity/stream",
596
629
  },
630
+ AIRBYTE_STREAM_NAME: {
631
+ "location": "schema",
632
+ "type": "string",
633
+ "required": False,
634
+ "description": "Maps entity to Airbyte stream for cache lookup",
635
+ },
597
636
  AIRBYTE_TOKEN_PATH: {
598
637
  "location": "securityScheme",
599
638
  "type": "string",
@@ -627,8 +666,7 @@ EXTENSION_REGISTRY = {
627
666
  "type": "dict[str, str]",
628
667
  "required": False,
629
668
  "description": (
630
- "Dictionary mapping field names to JSONPath expressions for extracting metadata "
631
- "(pagination, request IDs, etc.) from response envelopes"
669
+ "Dictionary mapping field names to JSONPath expressions for extracting metadata (pagination, request IDs, etc.) from response envelopes"
632
670
  ),
633
671
  },
634
672
  AIRBYTE_FILE_URL: {
@@ -80,6 +80,8 @@ class HTTPResponse:
80
80
  HTTPStatusError: For 4xx or 5xx status codes.
81
81
  """
82
82
  if 400 <= self._status_code < 600:
83
+ # NOTE: Import here intentionally to avoid circular import.
84
+ # exceptions.py imports HTTPResponse for type hints.
83
85
  from .exceptions import HTTPStatusError
84
86
 
85
87
  raise HTTPStatusError(
@@ -147,6 +147,9 @@ class HTTPClient:
147
147
  self.base_url = self.base_url.replace(f"{{{var_name}}}", var_value)
148
148
 
149
149
  self.auth_config = auth_config
150
+ assert (
151
+ self.auth_config.type is not None
152
+ ), "auth_config.type cannot be None" # Should never be None when instantiated via the local executor flow
150
153
  self.secrets = secrets
151
154
  self.logger = logger or NullLogger()
152
155
  self.metrics = HTTPMetrics()
@@ -296,12 +299,12 @@ class HTTPClient:
296
299
 
297
300
  # Support both sync and async callbacks
298
301
  callback_result = self.on_token_refresh(callback_data)
299
- if hasattr(callback_result, "__await__"):
302
+ if callback_result is not None and hasattr(callback_result, "__await__"):
300
303
  await callback_result
301
304
  except Exception as callback_error:
302
305
  self.logger.log_error(
303
306
  request_id=None,
304
- error=("Token refresh callback failed during initialization: " f"{callback_error!s}"),
307
+ error=(f"Token refresh callback failed during initialization: {callback_error!s}"),
305
308
  status_code=None,
306
309
  )
307
310
 
@@ -485,7 +488,7 @@ class HTTPClient:
485
488
  elif "application/json" in content_type or not content_type:
486
489
  response_data = await response.json()
487
490
  else:
488
- error_msg = f"Expected JSON response for {method.upper()} {url}, " f"got content-type: {content_type}"
491
+ error_msg = f"Expected JSON response for {method.upper()} {url}, got content-type: {content_type}"
489
492
  raise HTTPClientError(error_msg)
490
493
 
491
494
  except ValueError as e:
@@ -556,6 +559,7 @@ class HTTPClient:
556
559
  current_token = self.secrets.get("access_token")
557
560
  strategy = AuthStrategyFactory.get_strategy(self.auth_config.type)
558
561
 
562
+ # Try to refresh credentials
559
563
  try:
560
564
  result = await strategy.handle_auth_error(
561
565
  status_code=status_code,
@@ -564,53 +568,56 @@ class HTTPClient:
564
568
  config_values=self.config_values,
565
569
  http_client=None, # Let strategy create its own client
566
570
  )
567
-
568
- if result:
569
- # Notify callback if provided (for persistence)
570
- # Include both tokens AND extracted values for full persistence
571
- if self.on_token_refresh is not None:
572
- try:
573
- # Build callback data with both tokens and extracted values
574
- callback_data = dict(result.tokens)
575
- if result.extracted_values:
576
- callback_data.update(result.extracted_values)
577
-
578
- # Support both sync and async callbacks
579
- callback_result = self.on_token_refresh(callback_data)
580
- if hasattr(callback_result, "__await__"):
581
- await callback_result
582
- except Exception as callback_error:
583
- self.logger.log_error(
584
- request_id=request_id,
585
- error=f"Token refresh callback failed: {str(callback_error)}",
586
- status_code=status_code,
587
- )
588
-
589
- # Update secrets with new tokens (in-memory)
590
- self.secrets.update(result.tokens)
591
-
592
- # Update config_values and re-render base_url with extracted values
593
- if result.extracted_values:
594
- self._apply_token_extract(result.extracted_values)
595
-
596
- if self.secrets.get("access_token") != current_token:
597
- # Retry with new token - this will go through full retry logic
598
- return await self.request(
599
- method=method,
600
- path=path,
601
- params=params,
602
- json=json,
603
- data=data,
604
- headers=headers,
605
- )
606
-
607
571
  except Exception as refresh_error:
608
572
  self.logger.log_error(
609
573
  request_id=request_id,
610
574
  error=f"Credential refresh failed: {str(refresh_error)}",
611
575
  status_code=status_code,
612
576
  )
577
+ result = None
578
+
579
+ # If refresh succeeded, update tokens and retry
580
+ if result:
581
+ # Notify callback if provided (for persistence)
582
+ # Include both tokens AND extracted values for full persistence
583
+ if self.on_token_refresh is not None:
584
+ try:
585
+ # Build callback data with both tokens and extracted values
586
+ callback_data = dict(result.tokens)
587
+ if result.extracted_values:
588
+ callback_data.update(result.extracted_values)
589
+
590
+ # Support both sync and async callbacks
591
+ callback_result = self.on_token_refresh(callback_data)
592
+ if callback_result is not None and hasattr(callback_result, "__await__"):
593
+ await callback_result
594
+ except Exception as callback_error:
595
+ self.logger.log_error(
596
+ request_id=request_id,
597
+ error=f"Token refresh callback failed: {str(callback_error)}",
598
+ status_code=status_code,
599
+ )
600
+
601
+ # Update secrets with new tokens (in-memory)
602
+ self.secrets.update(result.tokens)
603
+
604
+ # Update config_values and re-render base_url with extracted values
605
+ if result.extracted_values:
606
+ self._apply_token_extract(result.extracted_values)
613
607
 
608
+ if self.secrets.get("access_token") != current_token:
609
+ # Retry with new token - this will go through full retry logic
610
+ # Any errors from this retry will propagate to the caller
611
+ return await self.request(
612
+ method=method,
613
+ path=path,
614
+ params=params,
615
+ json=json,
616
+ data=data,
617
+ headers=headers,
618
+ )
619
+
620
+ # Refresh failed or token didn't change, log and let original error propagate
614
621
  self.logger.log_error(request_id=request_id, error=str(error), status_code=status_code)
615
622
 
616
623
  async def request(
@@ -0,0 +1,262 @@
1
+ """
2
+ Shared introspection utilities for connector metadata.
3
+
4
+ This module provides utilities for introspecting connector metadata,
5
+ generating descriptions, and formatting parameter signatures. These
6
+ functions are used by both the runtime decorators and the generated
7
+ connector code.
8
+
9
+ The module is designed to work with any object conforming to the
10
+ ConnectorModel and EndpointDefinition interfaces from connector_sdk.types.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Protocol
16
+
17
+ # Constants
18
+ MAX_EXAMPLE_QUESTIONS = 5 # Maximum number of example questions to include in description
19
+
20
+
21
+ class EndpointProtocol(Protocol):
22
+ """Protocol defining the expected interface for endpoint parameters.
23
+
24
+ This allows functions to work with any endpoint-like object
25
+ that has these attributes, including EndpointDefinition and mock objects.
26
+ """
27
+
28
+ path_params: list[str]
29
+ path_params_schema: dict[str, dict[str, Any]]
30
+ query_params: list[str]
31
+ query_params_schema: dict[str, dict[str, Any]]
32
+ body_fields: list[str]
33
+ request_schema: dict[str, Any] | None
34
+
35
+
36
+ class EntityProtocol(Protocol):
37
+ """Protocol defining the expected interface for entity definitions."""
38
+
39
+ name: str
40
+ actions: list[Any]
41
+ endpoints: dict[Any, EndpointProtocol]
42
+
43
+
44
+ class ConnectorModelProtocol(Protocol):
45
+ """Protocol defining the expected interface for connector model parameters.
46
+
47
+ This allows functions to work with any connector-like object
48
+ that has these attributes, including ConnectorModel and mock objects.
49
+ """
50
+
51
+ @property
52
+ def entities(self) -> list[EntityProtocol]: ...
53
+
54
+ @property
55
+ def openapi_spec(self) -> Any: ...
56
+
57
+
58
+ def format_param_signature(endpoint: EndpointProtocol) -> str:
59
+ """Format parameter signature for an endpoint action.
60
+
61
+ Returns a string like: (id*) or (limit?, starting_after?, email?)
62
+ where * = required, ? = optional
63
+
64
+ Args:
65
+ endpoint: Object conforming to EndpointProtocol (e.g., EndpointDefinition)
66
+
67
+ Returns:
68
+ Formatted parameter signature string
69
+ """
70
+ params = []
71
+
72
+ # Defensive: safely access attributes with defaults for malformed endpoints
73
+ path_params = getattr(endpoint, "path_params", []) or []
74
+ query_params = getattr(endpoint, "query_params", []) or []
75
+ query_params_schema = getattr(endpoint, "query_params_schema", {}) or {}
76
+ body_fields = getattr(endpoint, "body_fields", []) or []
77
+ request_schema = getattr(endpoint, "request_schema", None)
78
+
79
+ # Path params (always required)
80
+ for name in path_params:
81
+ params.append(f"{name}*")
82
+
83
+ # Query params
84
+ for name in query_params:
85
+ schema = query_params_schema.get(name, {})
86
+ required = schema.get("required", False)
87
+ params.append(f"{name}{'*' if required else '?'}")
88
+
89
+ # Body fields
90
+ if request_schema:
91
+ required_fields = set(request_schema.get("required", []))
92
+ for name in body_fields:
93
+ params.append(f"{name}{'*' if name in required_fields else '?'}")
94
+
95
+ return f"({', '.join(params)})" if params else "()"
96
+
97
+
98
+ def describe_entities(model: ConnectorModelProtocol) -> list[dict[str, Any]]:
99
+ """Generate entity descriptions from ConnectorModel.
100
+
101
+ Returns a list of entity descriptions with detailed parameter information
102
+ for each action. This is used by generated connectors' describe() method.
103
+
104
+ Args:
105
+ model: Object conforming to ConnectorModelProtocol (e.g., ConnectorModel)
106
+
107
+ Returns:
108
+ List of entity description dicts with keys:
109
+ - entity_name: Name of the entity (e.g., "contacts", "deals")
110
+ - description: Entity description from the first endpoint
111
+ - available_actions: List of actions (e.g., ["list", "get", "create"])
112
+ - parameters: Dict mapping action -> list of parameter dicts
113
+ """
114
+ entities = []
115
+ for entity_def in model.entities:
116
+ description = ""
117
+ parameters: dict[str, list[dict[str, Any]]] = {}
118
+
119
+ endpoints = getattr(entity_def, "endpoints", {}) or {}
120
+ if endpoints:
121
+ for action, endpoint in endpoints.items():
122
+ # Get description from first endpoint that has one
123
+ if not description:
124
+ endpoint_desc = getattr(endpoint, "description", None)
125
+ if endpoint_desc:
126
+ description = endpoint_desc
127
+
128
+ action_params: list[dict[str, Any]] = []
129
+
130
+ # Defensive: safely access endpoint attributes
131
+ path_params = getattr(endpoint, "path_params", []) or []
132
+ path_params_schema = getattr(endpoint, "path_params_schema", {}) or {}
133
+ query_params = getattr(endpoint, "query_params", []) or []
134
+ query_params_schema = getattr(endpoint, "query_params_schema", {}) or {}
135
+ body_fields = getattr(endpoint, "body_fields", []) or []
136
+ request_schema = getattr(endpoint, "request_schema", None)
137
+
138
+ # Path params (always required)
139
+ for param_name in path_params:
140
+ schema = path_params_schema.get(param_name, {})
141
+ action_params.append(
142
+ {
143
+ "name": param_name,
144
+ "in": "path",
145
+ "required": True,
146
+ "type": schema.get("type", "string"),
147
+ "description": schema.get("description", ""),
148
+ }
149
+ )
150
+
151
+ # Query params
152
+ for param_name in query_params:
153
+ schema = query_params_schema.get(param_name, {})
154
+ action_params.append(
155
+ {
156
+ "name": param_name,
157
+ "in": "query",
158
+ "required": schema.get("required", False),
159
+ "type": schema.get("type", "string"),
160
+ "description": schema.get("description", ""),
161
+ }
162
+ )
163
+
164
+ # Body fields
165
+ if request_schema:
166
+ required_fields = request_schema.get("required", [])
167
+ properties = request_schema.get("properties", {})
168
+ for param_name in body_fields:
169
+ prop = properties.get(param_name, {})
170
+ action_params.append(
171
+ {
172
+ "name": param_name,
173
+ "in": "body",
174
+ "required": param_name in required_fields,
175
+ "type": prop.get("type", "string"),
176
+ "description": prop.get("description", ""),
177
+ }
178
+ )
179
+
180
+ if action_params:
181
+ # Action is an enum, use .value to get string
182
+ action_key = action.value if hasattr(action, "value") else str(action)
183
+ parameters[action_key] = action_params
184
+
185
+ actions = getattr(entity_def, "actions", []) or []
186
+ entities.append(
187
+ {
188
+ "entity_name": entity_def.name,
189
+ "description": description,
190
+ "available_actions": [a.value if hasattr(a, "value") else str(a) for a in actions],
191
+ "parameters": parameters,
192
+ }
193
+ )
194
+
195
+ return entities
196
+
197
+
198
+ def generate_tool_description(model: ConnectorModelProtocol) -> str:
199
+ """Generate AI tool description from connector metadata.
200
+
201
+ Produces a detailed description that includes:
202
+ - Per-entity/action parameter signatures with required (*) and optional (?) markers
203
+ - Response structure documentation with pagination hints
204
+ - Example questions if available in the OpenAPI spec
205
+
206
+ This is used by the Connector.describe class method decorator to populate
207
+ function docstrings for AI framework integration.
208
+
209
+ Args:
210
+ model: Object conforming to ConnectorModelProtocol (e.g., ConnectorModel)
211
+
212
+ Returns:
213
+ Formatted description string suitable for AI tool documentation
214
+ """
215
+ lines = []
216
+
217
+ # Entity/action parameter details (including pagination params like limit, starting_after)
218
+ lines.append("ENTITIES AND PARAMETERS:")
219
+ for entity in model.entities:
220
+ lines.append(f" {entity.name}:")
221
+ actions = getattr(entity, "actions", []) or []
222
+ endpoints = getattr(entity, "endpoints", {}) or {}
223
+ for action in actions:
224
+ action_str = action.value if hasattr(action, "value") else str(action)
225
+ endpoint = endpoints.get(action)
226
+ if endpoint:
227
+ param_sig = format_param_signature(endpoint)
228
+ lines.append(f" - {action_str}{param_sig}")
229
+ else:
230
+ lines.append(f" - {action_str}()")
231
+
232
+ # Response structure (brief, includes pagination hint)
233
+ lines.append("")
234
+ lines.append("RESPONSE STRUCTURE:")
235
+ lines.append(" - list/api_search: {data: [...], meta: {has_more: bool}}")
236
+ lines.append(" - get: Returns entity directly (no envelope)")
237
+ lines.append(" To paginate: pass starting_after=<last_id> while has_more is true")
238
+
239
+ # Add example questions if available in openapi_spec
240
+ openapi_spec = getattr(model, "openapi_spec", None)
241
+ if openapi_spec:
242
+ info = getattr(openapi_spec, "info", None)
243
+ if info:
244
+ example_questions = getattr(info, "x_airbyte_example_questions", None)
245
+ if example_questions:
246
+ supported = getattr(example_questions, "supported", None)
247
+ if supported:
248
+ lines.append("")
249
+ lines.append("EXAMPLE QUESTIONS:")
250
+ for q in supported[:MAX_EXAMPLE_QUESTIONS]:
251
+ lines.append(f" - {q}")
252
+
253
+ # Generic parameter description for function signature
254
+ lines.append("")
255
+ lines.append("FUNCTION PARAMETERS:")
256
+ lines.append(" - entity: Entity name (string)")
257
+ lines.append(" - action: Operation to perform (string)")
258
+ lines.append(" - params: Operation parameters (dict) - see entity details above")
259
+ lines.append("")
260
+ lines.append("Parameter markers: * = required, ? = optional")
261
+
262
+ return "\n".join(lines)
@@ -5,7 +5,7 @@ import json
5
5
  import time
6
6
  import uuid
7
7
  from pathlib import Path
8
- from typing import Any, Dict, Optional, Set
8
+ from typing import Any, Dict, Set
9
9
 
10
10
  from .types import LogSession, RequestLog
11
11
 
@@ -31,9 +31,9 @@ class RequestLogger:
31
31
 
32
32
  def __init__(
33
33
  self,
34
- log_file: Optional[str] = None,
35
- connector_name: Optional[str] = None,
36
- max_logs: Optional[int] = 10000,
34
+ log_file: str | None = None,
35
+ connector_name: str | None = None,
36
+ max_logs: int | None = 10000,
37
37
  ):
38
38
  """
39
39
  Initialize the request logger.
@@ -99,9 +99,9 @@ class RequestLogger:
99
99
  method: str,
100
100
  url: str,
101
101
  path: str,
102
- headers: Optional[Dict[str, str]] = None,
103
- params: Optional[Dict[str, Any]] = None,
104
- body: Optional[Any] = None,
102
+ headers: Dict[str, str] | None = None,
103
+ params: Dict[str, Any] | None = None,
104
+ body: Any | None = None,
105
105
  ) -> str:
106
106
  """
107
107
  Log the start of an HTTP request.
@@ -133,7 +133,7 @@ class RequestLogger:
133
133
  self,
134
134
  request_id: str,
135
135
  status_code: int,
136
- response_body: Optional[Any] = None,
136
+ response_body: Any | None = None,
137
137
  ) -> None:
138
138
  """
139
139
  Log a successful HTTP response.
@@ -176,7 +176,7 @@ class RequestLogger:
176
176
  self,
177
177
  request_id: str,
178
178
  error: str,
179
- status_code: Optional[int] = None,
179
+ status_code: int | None = None,
180
180
  ) -> None:
181
181
  """
182
182
  Log an HTTP request error.
@@ -2,7 +2,7 @@
2
2
 
3
3
  import base64
4
4
  from datetime import UTC, datetime
5
- from typing import Any, Dict, List, Optional
5
+ from typing import Any, Dict, List
6
6
 
7
7
  from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
8
8
 
@@ -27,12 +27,12 @@ class RequestLog(BaseModel):
27
27
  url: str
28
28
  path: str
29
29
  headers: Dict[str, str] = Field(default_factory=dict)
30
- params: Optional[Dict[str, Any]] = None
31
- body: Optional[Any] = None
32
- response_status: Optional[int] = None
33
- response_body: Optional[Any] = None
34
- timing_ms: Optional[float] = None
35
- error: Optional[str] = None
30
+ params: Dict[str, Any] | None = None
31
+ body: Any | None = None
32
+ response_status: int | None = None
33
+ response_body: Any | None = None
34
+ timing_ms: float | None = None
35
+ error: str | None = None
36
36
 
37
37
  @field_serializer("timestamp")
38
38
  def serialize_datetime(self, value: datetime) -> str:
@@ -50,9 +50,9 @@ class LogSession(BaseModel):
50
50
 
51
51
  session_id: str
52
52
  started_at: datetime = Field(default_factory=_utc_now)
53
- connector_name: Optional[str] = None
53
+ connector_name: str | None = None
54
54
  logs: List[RequestLog] = Field(default_factory=list)
55
- max_logs: Optional[int] = Field(
55
+ max_logs: int | None = Field(
56
56
  default=10000,
57
57
  description="Maximum number of logs to keep in memory. "
58
58
  "When limit is reached, oldest logs should be flushed before removal. "
@@ -60,7 +60,7 @@ class LogSession(BaseModel):
60
60
  )
61
61
  chunk_logs: List[bytes] = Field(
62
62
  default_factory=list,
63
- description="Captured chunks from streaming responses. " "Each chunk is logged when log_chunk_fetch() is called.",
63
+ description="Captured chunks from streaming responses. Each chunk is logged when log_chunk_fetch() is called.",
64
64
  )
65
65
 
66
66
  @field_validator("chunk_logs", mode="before")