python-base-agent 2026.2.13__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.
- base_agent/__init__.py +49 -0
- base_agent/agent.py +1060 -0
- base_agent/config.py +220 -0
- base_agent/contracts/__init__.py +209 -0
- base_agent/contracts/access_control_summary_part.py +42 -0
- base_agent/contracts/agent_loop_part.py +87 -0
- base_agent/contracts/agent_tool_part.py +15 -0
- base_agent/contracts/append_entity_class_part.py +15 -0
- base_agent/contracts/broadcast_message.py +30 -0
- base_agent/contracts/broadcast_message_type.py +13 -0
- base_agent/contracts/cache_stability_metadata.py +27 -0
- base_agent/contracts/code_generation.py +18 -0
- base_agent/contracts/context_management_notification.py +20 -0
- base_agent/contracts/context_management_pipeline.py +21 -0
- base_agent/contracts/cross_vector_similarity_search_request.py +14 -0
- base_agent/contracts/data_classification_tag.py +12 -0
- base_agent/contracts/data_lineage_part.py +46 -0
- base_agent/contracts/data_part.py +15 -0
- base_agent/contracts/data_pointer_part.py +20 -0
- base_agent/contracts/embedding_request.py +15 -0
- base_agent/contracts/embedding_response.py +21 -0
- base_agent/contracts/entity_matching_form_data.py +79 -0
- base_agent/contracts/entity_vector_text_search_match.py +12 -0
- base_agent/contracts/entity_vector_text_search_request.py +14 -0
- base_agent/contracts/entity_vector_text_search_result.py +24 -0
- base_agent/contracts/entity_vectorization_job_info.py +24 -0
- base_agent/contracts/entity_vectorization_jobs_result.py +30 -0
- base_agent/contracts/entity_vectorization_progress.py +14 -0
- base_agent/contracts/entity_vectorization_request.py +15 -0
- base_agent/contracts/entity_vectorization_result.py +17 -0
- base_agent/contracts/envelope.py +20 -0
- base_agent/contracts/error_part.py +24 -0
- base_agent/contracts/error_part_type.py +14 -0
- base_agent/contracts/file_part.py +17 -0
- base_agent/contracts/file_pointer_part.py +17 -0
- base_agent/contracts/file_revectorization_request_event.py +19 -0
- base_agent/contracts/file_vectorization_request_event.py +22 -0
- base_agent/contracts/for_each_part.py +30 -0
- base_agent/contracts/fuzzy_matching_params.py +21 -0
- base_agent/contracts/group_plan.py +16 -0
- base_agent/contracts/header.py +69 -0
- base_agent/contracts/human_input_request.py +16 -0
- base_agent/contracts/inspector_pipeline_item.py +19 -0
- base_agent/contracts/list_entity_vectorization_jobs_request.py +12 -0
- base_agent/contracts/llm_accounting_part.py +33 -0
- base_agent/contracts/llm_error_notification.py +13 -0
- base_agent/contracts/llm_judge_job.py +84 -0
- base_agent/contracts/llm_judge_params.py +15 -0
- base_agent/contracts/llm_judge_progress.py +20 -0
- base_agent/contracts/llm_model_part.py +72 -0
- base_agent/contracts/llm_reasoning_summary.py +23 -0
- base_agent/contracts/llm_response.py +25 -0
- base_agent/contracts/llm_response_part.py +54 -0
- base_agent/contracts/match_entities_job.py +43 -0
- base_agent/contracts/match_result.py +52 -0
- base_agent/contracts/notification_error_type.py +12 -0
- base_agent/contracts/notification_severity.py +14 -0
- base_agent/contracts/on_trigger_fired.py +17 -0
- base_agent/contracts/parallel_execution_plan.py +25 -0
- base_agent/contracts/part.py +30 -0
- base_agent/contracts/resume_entity_vectorization_request.py +11 -0
- base_agent/contracts/room_dataset_catalog.py +44 -0
- base_agent/contracts/room_dataset_catalog_entry.py +38 -0
- base_agent/contracts/room_dataset_column.py +12 -0
- base_agent/contracts/room_dataset_schema.py +24 -0
- base_agent/contracts/scratch_pad_content.py +14 -0
- base_agent/contracts/search_request.py +27 -0
- base_agent/contracts/search_response.py +39 -0
- base_agent/contracts/search_result.py +21 -0
- base_agent/contracts/search_type_result.py +13 -0
- base_agent/contracts/semantic_search_progress.py +18 -0
- base_agent/contracts/semantic_search_result.py +25 -0
- base_agent/contracts/stream_content_message.py +35 -0
- base_agent/contracts/stream_event_type.py +12 -0
- base_agent/contracts/stream_message.py +14 -0
- base_agent/contracts/stream_metadata.py +28 -0
- base_agent/contracts/task_artifact_update.py +20 -0
- base_agent/contracts/task_error_response.py +22 -0
- base_agent/contracts/task_ref.py +19 -0
- base_agent/contracts/task_request.py +21 -0
- base_agent/contracts/task_response.py +21 -0
- base_agent/contracts/task_status_update.py +31 -0
- base_agent/contracts/text_part.py +15 -0
- base_agent/contracts/tool_authorization_request.py +21 -0
- base_agent/contracts/tool_call_index.py +42 -0
- base_agent/contracts/tool_call_index_entry.py +32 -0
- base_agent/contracts/tool_call_initiation.py +27 -0
- base_agent/contracts/tool_learning_request.py +34 -0
- base_agent/contracts/tool_response_completion.py +19 -0
- base_agent/contracts/tool_response_part.py +31 -0
- base_agent/contracts/usage_metadata.py +11 -0
- base_agent/contracts/user_data_notification.py +13 -0
- base_agent/contracts/user_notification.py +21 -0
- base_agent/contracts/vector_similarity_search_request.py +13 -0
- base_agent/contracts/vector_similarity_search_result.py +25 -0
- base_agent/contracts/workflow_part.py +17 -0
- base_agent/exceptions.py +39 -0
- base_agent/health/__init__.py +5 -0
- base_agent/health/server.py +191 -0
- base_agent/messaging/__init__.py +5 -0
- base_agent/messaging/kafka_client.py +572 -0
- base_agent/ordering/__init__.py +5 -0
- base_agent/ordering/vector_clock.py +176 -0
- base_agent/prompts/__init__.py +10 -0
- base_agent/prompts/prompt_manager.py +213 -0
- base_agent/registration/__init__.py +5 -0
- base_agent/registration/registration_client.py +364 -0
- base_agent/schemas/__init__.py +14 -0
- base_agent/schemas/models.py +30 -0
- base_agent/schemas/schema_registry_client.py +493 -0
- base_agent/schemas/source_type.py +24 -0
- base_agent/schemas/technical_name_validator.py +147 -0
- base_agent/state/__init__.py +13 -0
- base_agent/state/logical_clock_tracker.py +50 -0
- base_agent/state/session_tracker.py +91 -0
- base_agent/state/store.py +333 -0
- base_agent/storage/__init__.py +27 -0
- base_agent/storage/azure_store.py +615 -0
- base_agent/storage/exceptions.py +26 -0
- base_agent/storage/models.py +73 -0
- base_agent/storage/object_store.py +248 -0
- base_agent/storage/object_store_factory.py +136 -0
- base_agent/storage/s3_store.py +411 -0
- base_agent/telemetry/__init__.py +1 -0
- base_agent/tools/__init__.py +12 -0
- base_agent/tools/models.py +66 -0
- base_agent/tools/tool_registry_client.py +607 -0
- base_agent/utils/__init__.py +5 -0
- base_agent/utils/logger.py +146 -0
- base_agent/utils/version_utils.py +92 -0
- python_base_agent-2026.2.13.dist-info/METADATA +536 -0
- python_base_agent-2026.2.13.dist-info/RECORD +134 -0
- python_base_agent-2026.2.13.dist-info/WHEEL +5 -0
- python_base_agent-2026.2.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""Schema registry client for registering Pydantic models with the platform.
|
|
2
|
+
|
|
3
|
+
This is the Python equivalent of Java's SchemaRegistryClient.
|
|
4
|
+
Reference: base-agent/src/main/java/one/ai/platform/baseagent/agent/schemas/SchemaRegistryClient.java
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import random
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import TypeVar
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
from base_agent.schemas.source_type import SourceType
|
|
19
|
+
from base_agent.schemas.technical_name_validator import to_technical_name_label
|
|
20
|
+
from base_agent.utils.logger import sanitize_for_logging
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SchemaRegistryClient:
|
|
28
|
+
"""
|
|
29
|
+
Client for registering schemas with the platform schema registry.
|
|
30
|
+
|
|
31
|
+
Provides feature parity with Java SchemaRegistryClient:
|
|
32
|
+
- JSON schema generation from Pydantic models
|
|
33
|
+
- Schema registration with admin-gateway
|
|
34
|
+
- Schema hash lookup by class or name
|
|
35
|
+
- Thread-safe operations
|
|
36
|
+
- Health check integration via is_live property
|
|
37
|
+
- Retry logic with exponential backoff
|
|
38
|
+
|
|
39
|
+
Reference: Java SchemaRegistryClient.java
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
API_PATH = "/api/v1/schema-registry"
|
|
43
|
+
MAX_RETRY_DURATION_SECONDS = 30
|
|
44
|
+
INITIAL_BACKOFF_MS = 500
|
|
45
|
+
BACKOFF_MULTIPLIER = 1.5
|
|
46
|
+
|
|
47
|
+
def __init__(self, admin_gateway_url: str):
|
|
48
|
+
"""
|
|
49
|
+
Initialize the schema registry client.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
admin_gateway_url: Base URL of the admin gateway (e.g., "http://localhost:8888")
|
|
53
|
+
"""
|
|
54
|
+
if not admin_gateway_url:
|
|
55
|
+
raise ValueError("admin_gateway_url is required")
|
|
56
|
+
|
|
57
|
+
self._admin_gateway_url = admin_gateway_url.rstrip("/")
|
|
58
|
+
self._is_live = True
|
|
59
|
+
self._lock = threading.Lock()
|
|
60
|
+
self._client = httpx.Client(
|
|
61
|
+
base_url=self._admin_gateway_url,
|
|
62
|
+
timeout=30.0,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
logger.info(
|
|
66
|
+
f"SchemaRegistryClient initialized at {sanitize_for_logging(admin_gateway_url)}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_live(self) -> bool:
|
|
71
|
+
"""Check if the client is live (healthy)."""
|
|
72
|
+
return self._is_live
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def get_schema_name_for_class(model: type[BaseModel]) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Convert a Pydantic model class name to its RFC1035 compliant schema name.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
model: The Pydantic model class
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The RFC1035 compliant schema name
|
|
84
|
+
"""
|
|
85
|
+
return to_technical_name_label(model.__name__)
|
|
86
|
+
|
|
87
|
+
def register_schemas(
|
|
88
|
+
self,
|
|
89
|
+
models: list[type[BaseModel]],
|
|
90
|
+
group_name: str | None = None,
|
|
91
|
+
source_type: SourceType = SourceType.PLATFORM_AGENT,
|
|
92
|
+
) -> dict[str, bool]:
|
|
93
|
+
"""
|
|
94
|
+
Register schemas for a list of Pydantic model classes.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
models: List of Pydantic model classes to register schemas for
|
|
98
|
+
group_name: Group name for the schemas (optional)
|
|
99
|
+
source_type: The source type of the schema
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict mapping schema names to their registration status
|
|
103
|
+
"""
|
|
104
|
+
results: dict[str, bool] = {}
|
|
105
|
+
|
|
106
|
+
for model in models:
|
|
107
|
+
try:
|
|
108
|
+
registered = self.register_schema(model, group_name, source_type)
|
|
109
|
+
schema_name = to_technical_name_label(model.__name__)
|
|
110
|
+
results[schema_name] = registered
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(
|
|
113
|
+
f"Failed to register schema for class: "
|
|
114
|
+
f"{sanitize_for_logging(model.__name__)}: {sanitize_for_logging(str(e))}"
|
|
115
|
+
)
|
|
116
|
+
schema_name = to_technical_name_label(model.__name__)
|
|
117
|
+
results[schema_name] = False
|
|
118
|
+
|
|
119
|
+
return results
|
|
120
|
+
|
|
121
|
+
def register_schema(
|
|
122
|
+
self,
|
|
123
|
+
model: type[BaseModel],
|
|
124
|
+
group_name: str | None = None,
|
|
125
|
+
source_type: SourceType = SourceType.PLATFORM_AGENT,
|
|
126
|
+
) -> bool:
|
|
127
|
+
"""
|
|
128
|
+
Register a schema for a Pydantic model class.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
model: The Pydantic model class to generate a schema for
|
|
132
|
+
group_name: Group name for the schema (optional)
|
|
133
|
+
source_type: The source type of the schema
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if a new schema was registered, False if no changes were needed
|
|
137
|
+
"""
|
|
138
|
+
with self._lock:
|
|
139
|
+
# Convert class name to RFC1035 compliant schema name
|
|
140
|
+
schema_name = to_technical_name_label(model.__name__)
|
|
141
|
+
logger.debug(
|
|
142
|
+
f"Converting class name '{sanitize_for_logging(model.__name__)}' "
|
|
143
|
+
f"to RFC1035 compliant schema name '{sanitize_for_logging(schema_name)}'"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Generate JSON schema for the model
|
|
147
|
+
json_schema = model.model_json_schema()
|
|
148
|
+
schema_text = json.dumps(json_schema, sort_keys=True)
|
|
149
|
+
|
|
150
|
+
# Check if schema already exists and compare content
|
|
151
|
+
existing_schema = self._get_existing_schema(schema_name, group_name)
|
|
152
|
+
|
|
153
|
+
if existing_schema is not None:
|
|
154
|
+
# Compare normalized schemas (to ignore formatting differences)
|
|
155
|
+
try:
|
|
156
|
+
existing_schema_obj = json.loads(existing_schema)
|
|
157
|
+
new_schema_obj = json.loads(schema_text)
|
|
158
|
+
|
|
159
|
+
if existing_schema_obj == new_schema_obj:
|
|
160
|
+
logger.debug(
|
|
161
|
+
f"Schema for {sanitize_for_logging(schema_name)} is unchanged, "
|
|
162
|
+
"skipping registration"
|
|
163
|
+
)
|
|
164
|
+
return False
|
|
165
|
+
except json.JSONDecodeError:
|
|
166
|
+
# If we can't parse, assume they're different
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
# Schema is new or different, register it
|
|
170
|
+
return self._create_or_update_schema(schema_name, group_name, schema_text, source_type)
|
|
171
|
+
|
|
172
|
+
def get_schema_hash_for_class(
|
|
173
|
+
self,
|
|
174
|
+
model: type[BaseModel],
|
|
175
|
+
group_name: str | None = None,
|
|
176
|
+
) -> str | None:
|
|
177
|
+
"""
|
|
178
|
+
Get the schema hash for a given Pydantic model class.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
model: The Pydantic model class to get the schema hash for
|
|
182
|
+
group_name: Group name for the schema (optional)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The SHA-256 hash of the schema, or None if not found
|
|
186
|
+
"""
|
|
187
|
+
# Convert class name to RFC1035 compliant schema name
|
|
188
|
+
schema_name = to_technical_name_label(model.__name__)
|
|
189
|
+
logger.debug(
|
|
190
|
+
f"Getting schema hash for class: {sanitize_for_logging(model.__name__)} "
|
|
191
|
+
f"-> schema: {sanitize_for_logging(schema_name)} "
|
|
192
|
+
f"(group: {sanitize_for_logging(group_name)})"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return self.get_schema_hash_by_name(schema_name, group_name)
|
|
196
|
+
|
|
197
|
+
def get_schema_hash_by_name(
|
|
198
|
+
self,
|
|
199
|
+
name: str,
|
|
200
|
+
group_name: str | None = None,
|
|
201
|
+
) -> str | None:
|
|
202
|
+
"""
|
|
203
|
+
Get the schema hash for a given schema name and group.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
name: The schema name to look up
|
|
207
|
+
group_name: Group name for the schema (optional, defaults to "default")
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The SHA-256 hash of the schema, or None if not found
|
|
211
|
+
"""
|
|
212
|
+
logger.info(
|
|
213
|
+
f"Getting schema hash by name: {sanitize_for_logging(name)} "
|
|
214
|
+
f"(group: {sanitize_for_logging(group_name)})"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def operation() -> str | None:
|
|
218
|
+
encoded_group = group_name if group_name else "default"
|
|
219
|
+
|
|
220
|
+
response = self._client.get(
|
|
221
|
+
self.API_PATH,
|
|
222
|
+
params={
|
|
223
|
+
"name": name,
|
|
224
|
+
"group": encoded_group,
|
|
225
|
+
"latest_only": "true",
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if response.status_code == 200:
|
|
230
|
+
schemas = response.json()
|
|
231
|
+
if schemas:
|
|
232
|
+
schema_hash = schemas[0].get("schema_hash")
|
|
233
|
+
logger.debug(
|
|
234
|
+
f"Found schema hash for {sanitize_for_logging(name)}: "
|
|
235
|
+
f"{sanitize_for_logging(schema_hash)}"
|
|
236
|
+
)
|
|
237
|
+
return schema_hash
|
|
238
|
+
else:
|
|
239
|
+
logger.warning(
|
|
240
|
+
f"No schema found for name: {sanitize_for_logging(name)} "
|
|
241
|
+
f"(group: {sanitize_for_logging(group_name)})"
|
|
242
|
+
)
|
|
243
|
+
return None
|
|
244
|
+
else:
|
|
245
|
+
logger.warning(
|
|
246
|
+
f"Failed to get schema hash for name {sanitize_for_logging(name)} "
|
|
247
|
+
f"(group {sanitize_for_logging(group_name)}): "
|
|
248
|
+
f"HTTP {response.status_code}"
|
|
249
|
+
)
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
return self._execute_with_retries(operation)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(f"Failed to get schema hash: {sanitize_for_logging(str(e))}")
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
def get_schema_by_hash(
|
|
259
|
+
self,
|
|
260
|
+
schema_hash: str,
|
|
261
|
+
latest_only: bool = True,
|
|
262
|
+
) -> str | None:
|
|
263
|
+
"""
|
|
264
|
+
Get a schema by its hash value.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
schema_hash: The hash value of the schema
|
|
268
|
+
latest_only: Whether to return only the latest version with this hash
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Schema text content, or None if not found
|
|
272
|
+
"""
|
|
273
|
+
logger.debug(f"Getting schema by hash: {sanitize_for_logging(schema_hash)}")
|
|
274
|
+
|
|
275
|
+
def operation() -> str | None:
|
|
276
|
+
response = self._client.get(
|
|
277
|
+
f"{self.API_PATH}/by-hash/{schema_hash}",
|
|
278
|
+
params={"latest_only": str(latest_only).lower()},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if response.status_code == 200:
|
|
282
|
+
data = response.json()
|
|
283
|
+
return data.get("schema_text")
|
|
284
|
+
else:
|
|
285
|
+
logger.warning(
|
|
286
|
+
f"Failed to get schema by hash {sanitize_for_logging(schema_hash)}: "
|
|
287
|
+
f"HTTP {response.status_code}"
|
|
288
|
+
)
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
return self._execute_with_retries(operation)
|
|
293
|
+
except Exception as e:
|
|
294
|
+
logger.error(f"Failed to get schema by hash: {sanitize_for_logging(str(e))}")
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
def _is_retryable_exception(self, e: Exception) -> bool:
|
|
298
|
+
"""Check if an exception is retryable."""
|
|
299
|
+
if isinstance(e, (httpx.ConnectError, httpx.ConnectTimeout, httpx.ReadTimeout)):
|
|
300
|
+
return True
|
|
301
|
+
if e.__cause__ is not None and isinstance(e.__cause__, Exception):
|
|
302
|
+
return self._is_retryable_exception(e.__cause__)
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
def _execute_with_retries(self, operation: Callable[[], T]) -> T:
|
|
306
|
+
"""
|
|
307
|
+
Execute an operation with retries.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
operation: The operation to execute
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
The result of the operation
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
Exception: If all retries are exhausted
|
|
317
|
+
"""
|
|
318
|
+
start_time = time.time()
|
|
319
|
+
end_time = start_time + self.MAX_RETRY_DURATION_SECONDS
|
|
320
|
+
|
|
321
|
+
attempt = 0
|
|
322
|
+
backoff_ms = self.INITIAL_BACKOFF_MS
|
|
323
|
+
last_exception: Exception | None = None
|
|
324
|
+
|
|
325
|
+
while time.time() < end_time:
|
|
326
|
+
attempt += 1
|
|
327
|
+
try:
|
|
328
|
+
return operation()
|
|
329
|
+
except Exception as e:
|
|
330
|
+
last_exception = e
|
|
331
|
+
if not self._is_retryable_exception(e):
|
|
332
|
+
logger.warning(
|
|
333
|
+
f"Non-retryable exception occurred, abandoning retry: "
|
|
334
|
+
f"{sanitize_for_logging(str(e))}"
|
|
335
|
+
)
|
|
336
|
+
break
|
|
337
|
+
|
|
338
|
+
if time.time() + (backoff_ms / 1000) < end_time:
|
|
339
|
+
logger.info(
|
|
340
|
+
f"Attempt {attempt} failed with retryable error: "
|
|
341
|
+
f"{sanitize_for_logging(str(e))}. Retrying in {backoff_ms}ms..."
|
|
342
|
+
)
|
|
343
|
+
time.sleep(backoff_ms / 1000)
|
|
344
|
+
# Apply exponential backoff with jitter
|
|
345
|
+
jitter = 0.9 + random.random() * 0.2
|
|
346
|
+
backoff_ms = int(backoff_ms * self.BACKOFF_MULTIPLIER * jitter)
|
|
347
|
+
else:
|
|
348
|
+
logger.warning(f"Retry time budget exceeded after {attempt} attempts")
|
|
349
|
+
break
|
|
350
|
+
|
|
351
|
+
# If we got here, all retries failed
|
|
352
|
+
logger.error(f"Schema registry operation failed after {attempt} attempts")
|
|
353
|
+
self._is_live = False
|
|
354
|
+
|
|
355
|
+
if last_exception is not None:
|
|
356
|
+
raise last_exception
|
|
357
|
+
raise RuntimeError(f"Failed after {attempt} attempts with unknown error")
|
|
358
|
+
|
|
359
|
+
def _get_existing_schema(
|
|
360
|
+
self,
|
|
361
|
+
name: str,
|
|
362
|
+
group_name: str | None,
|
|
363
|
+
) -> str | None:
|
|
364
|
+
"""
|
|
365
|
+
Get an existing schema from the registry.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
name: Schema name
|
|
369
|
+
group_name: Group name
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
The schema text, or None if not found
|
|
373
|
+
"""
|
|
374
|
+
logger.debug(
|
|
375
|
+
f"Getting existing schema: {sanitize_for_logging(name)} "
|
|
376
|
+
f"({sanitize_for_logging(group_name)})"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
|
|
381
|
+
def operation() -> str | None:
|
|
382
|
+
encoded_group = group_name if group_name else "default"
|
|
383
|
+
|
|
384
|
+
response = self._client.get(
|
|
385
|
+
self.API_PATH,
|
|
386
|
+
params={
|
|
387
|
+
"name": name,
|
|
388
|
+
"group": encoded_group,
|
|
389
|
+
"latest_only": "true",
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if response.status_code == 200:
|
|
394
|
+
schemas = response.json()
|
|
395
|
+
if schemas:
|
|
396
|
+
schema_id = schemas[0].get("schema_id")
|
|
397
|
+
# Get the schema text content
|
|
398
|
+
return self._get_schema_by_id(schema_id)
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
return self._execute_with_retries(operation)
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.warning(
|
|
404
|
+
f"Failed to get existing schema: {sanitize_for_logging(name)} "
|
|
405
|
+
f"({sanitize_for_logging(str(e))})"
|
|
406
|
+
)
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
def _get_schema_by_id(self, schema_id: int) -> str | None:
|
|
410
|
+
"""
|
|
411
|
+
Get a schema by its ID.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
schema_id: Schema ID
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Schema text content, or None if not found
|
|
418
|
+
"""
|
|
419
|
+
logger.debug(f"Getting schema by ID: {schema_id}")
|
|
420
|
+
|
|
421
|
+
def operation() -> str | None:
|
|
422
|
+
response = self._client.get(f"{self.API_PATH}/{schema_id}")
|
|
423
|
+
|
|
424
|
+
if response.status_code == 200:
|
|
425
|
+
data = response.json()
|
|
426
|
+
return data.get("schema_text")
|
|
427
|
+
else:
|
|
428
|
+
logger.warning(
|
|
429
|
+
f"Failed to get schema by ID {schema_id}: HTTP {response.status_code}"
|
|
430
|
+
)
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
return self._execute_with_retries(operation)
|
|
434
|
+
|
|
435
|
+
def _create_or_update_schema(
|
|
436
|
+
self,
|
|
437
|
+
name: str,
|
|
438
|
+
group_name: str | None,
|
|
439
|
+
schema_text: str,
|
|
440
|
+
source_type: SourceType,
|
|
441
|
+
) -> bool:
|
|
442
|
+
"""
|
|
443
|
+
Create or update a schema in the registry.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
name: Schema name
|
|
447
|
+
group_name: Group name
|
|
448
|
+
schema_text: Schema text content
|
|
449
|
+
source_type: The source type of the schema
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
True if successful, False otherwise
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
def operation() -> bool:
|
|
456
|
+
payload = {
|
|
457
|
+
"schema_data": {
|
|
458
|
+
"name": name,
|
|
459
|
+
"group": group_name if group_name else "default",
|
|
460
|
+
"format": "JSON_SCHEMA",
|
|
461
|
+
"compatibility": "FULL",
|
|
462
|
+
"source_type": source_type.value,
|
|
463
|
+
},
|
|
464
|
+
"schema_text": {
|
|
465
|
+
"schema_text": schema_text,
|
|
466
|
+
},
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
response = self._client.post(
|
|
470
|
+
f"{self.API_PATH}/create-with-text",
|
|
471
|
+
json=payload,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
if response.status_code == 201:
|
|
475
|
+
logger.info(f"Successfully registered schema for {sanitize_for_logging(name)}")
|
|
476
|
+
return True
|
|
477
|
+
else:
|
|
478
|
+
logger.error(
|
|
479
|
+
f"Failed to register schema: HTTP {response.status_code} - "
|
|
480
|
+
f"{sanitize_for_logging(response.text)}"
|
|
481
|
+
)
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
return self._execute_with_retries(operation)
|
|
486
|
+
except Exception as e:
|
|
487
|
+
logger.error(f"Failed to create/update schema: {sanitize_for_logging(str(e))}")
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
def close(self) -> None:
|
|
491
|
+
"""Close the HTTP client."""
|
|
492
|
+
self._client.close()
|
|
493
|
+
logger.info("SchemaRegistryClient closed")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Source type enum for schema registry.
|
|
2
|
+
|
|
3
|
+
This module provides the SourceType enum matching Java's SourceType.java.
|
|
4
|
+
Reference: base-agent/src/main/java/one/ai/platform/baseagent/agent/schemas/SourceType.java
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SourceType(str, Enum):
|
|
11
|
+
"""
|
|
12
|
+
Schema source types for the schema registry.
|
|
13
|
+
|
|
14
|
+
These values correspond to the source_type field in the schema registry API.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
NAMED_QUERY = "NAMED_QUERY"
|
|
18
|
+
PDF = "PDF"
|
|
19
|
+
WORKFLOW = "WORKFLOW"
|
|
20
|
+
TASK_AGENT = "TASK_AGENT"
|
|
21
|
+
UNSTRUCTURED_DATA_METADATA = "UNSTRUCTURED_DATA_METADATA"
|
|
22
|
+
PLATFORM_AGENT = "PLATFORM_AGENT"
|
|
23
|
+
PYTHON_SANDBOX_AGENT = "PYTHON_SANDBOX_AGENT"
|
|
24
|
+
CUSTOM_AGENT = "CUSTOM_AGENT"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Technical name validation and conversion utilities.
|
|
2
|
+
|
|
3
|
+
This module provides RFC1035-compliant name validation and conversion,
|
|
4
|
+
porting Java's TechnicalNameValidator.
|
|
5
|
+
Reference: base-agent/src/main/java/one/ai/platform/baseagent/utils/TechnicalNameValidator.java
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
MAX_LABEL_LENGTH = 63
|
|
11
|
+
|
|
12
|
+
# Regex patterns matching Java implementation
|
|
13
|
+
START_WITH_LETTER = re.compile(r"^[a-zA-Z]")
|
|
14
|
+
END_WITH_LETTER_OR_DIGIT = re.compile(r"[a-zA-Z0-9]$")
|
|
15
|
+
VALID_CHARACTERS = re.compile(r"^[a-zA-Z0-9\-_]+$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_valid_label(label: str) -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Validate if a string is a valid technical name.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
label: The label to validate
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
True if the label is valid, False otherwise
|
|
27
|
+
"""
|
|
28
|
+
if not label or len(label) > MAX_LABEL_LENGTH:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
# Must start with a letter
|
|
32
|
+
if not START_WITH_LETTER.match(label):
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
# Must end with a letter or digit
|
|
36
|
+
if not END_WITH_LETTER_OR_DIGIT.search(label):
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
# Can only contain letters, digits, hyphens, and underscores
|
|
40
|
+
return bool(VALID_CHARACTERS.match(label))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def to_technical_name_label(class_name: str) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Convert a class name to a technical name compliant label.
|
|
46
|
+
|
|
47
|
+
This method converts camelCase/PascalCase to kebab-case and ensures
|
|
48
|
+
the result is RFC1035 compliant.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
class_name: The class name to convert (e.g., 'GetIssueInput')
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Technical name compliant label (e.g., 'get-issue-input')
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: If class_name is empty or cannot be converted
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
>>> to_technical_name_label('GetIssueInput')
|
|
61
|
+
'get-issue-input'
|
|
62
|
+
>>> to_technical_name_label('XMLParser')
|
|
63
|
+
'xml-parser'
|
|
64
|
+
>>> to_technical_name_label('Test123Class')
|
|
65
|
+
'test-123-class'
|
|
66
|
+
"""
|
|
67
|
+
if not class_name:
|
|
68
|
+
raise ValueError("Class name cannot be null or empty")
|
|
69
|
+
|
|
70
|
+
result = class_name
|
|
71
|
+
|
|
72
|
+
# Replace underscores with hyphens first
|
|
73
|
+
result = result.replace("_", "-")
|
|
74
|
+
|
|
75
|
+
# Insert hyphen before uppercase letters (except at the beginning)
|
|
76
|
+
result = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", result)
|
|
77
|
+
|
|
78
|
+
# Handle consecutive uppercase letters (e.g., XMLParser -> XML-Parser)
|
|
79
|
+
result = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1-\2", result)
|
|
80
|
+
|
|
81
|
+
# Insert hyphen between letters and numbers
|
|
82
|
+
result = re.sub(r"([a-zA-Z])([0-9])", r"\1-\2", result)
|
|
83
|
+
result = re.sub(r"([0-9])([a-zA-Z])", r"\1-\2", result)
|
|
84
|
+
|
|
85
|
+
# Convert to lowercase
|
|
86
|
+
result = result.lower()
|
|
87
|
+
|
|
88
|
+
# Replace other special characters with hyphens
|
|
89
|
+
result = re.sub(r"[^a-z0-9\-]", "-", result)
|
|
90
|
+
|
|
91
|
+
# Remove leading/trailing hyphens
|
|
92
|
+
result = re.sub(r"^-+|-+$", "", result)
|
|
93
|
+
|
|
94
|
+
# Replace multiple consecutive hyphens with a single hyphen
|
|
95
|
+
result = re.sub(r"-+", "-", result)
|
|
96
|
+
|
|
97
|
+
# Ensure it starts with a letter
|
|
98
|
+
if result and not result[0].isalpha():
|
|
99
|
+
result = "schema-" + result
|
|
100
|
+
|
|
101
|
+
# Ensure it ends with a letter or digit
|
|
102
|
+
while result and result.endswith("-"):
|
|
103
|
+
result = result[:-1]
|
|
104
|
+
|
|
105
|
+
# Truncate if too long
|
|
106
|
+
if len(result) > MAX_LABEL_LENGTH:
|
|
107
|
+
result = result[:MAX_LABEL_LENGTH]
|
|
108
|
+
# Ensure it still ends with a letter or digit after truncation
|
|
109
|
+
while result and not result[-1].isalnum():
|
|
110
|
+
result = result[:-1]
|
|
111
|
+
|
|
112
|
+
# Final validation
|
|
113
|
+
if not is_valid_label(result):
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"Unable to convert class name '{class_name}' to technical name "
|
|
116
|
+
f"compliant label. Result: '{result}'"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_validation_error(label: str) -> str | None:
|
|
123
|
+
"""
|
|
124
|
+
Get a validation error message for a label.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
label: The label to validate
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Error message if invalid, None if valid
|
|
131
|
+
"""
|
|
132
|
+
if not label:
|
|
133
|
+
return "Label cannot be null or empty"
|
|
134
|
+
|
|
135
|
+
if len(label) > MAX_LABEL_LENGTH:
|
|
136
|
+
return "Label must be 63 characters or less"
|
|
137
|
+
|
|
138
|
+
if not START_WITH_LETTER.match(label):
|
|
139
|
+
return "Label must start with a letter"
|
|
140
|
+
|
|
141
|
+
if not END_WITH_LETTER_OR_DIGIT.search(label):
|
|
142
|
+
return "Label must end with a letter or digit"
|
|
143
|
+
|
|
144
|
+
if not VALID_CHARACTERS.match(label):
|
|
145
|
+
return "Label can only contain letters, digits, hyphens, and underscores"
|
|
146
|
+
|
|
147
|
+
return None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""State management module for BaseAgent."""
|
|
2
|
+
|
|
3
|
+
from .logical_clock_tracker import LogicalClockTracker
|
|
4
|
+
from .session_tracker import SessionTracker
|
|
5
|
+
from .store import InMemoryStateStore, RedisStateStore, StateStore
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"StateStore",
|
|
9
|
+
"InMemoryStateStore",
|
|
10
|
+
"RedisStateStore",
|
|
11
|
+
"SessionTracker",
|
|
12
|
+
"LogicalClockTracker",
|
|
13
|
+
]
|