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.
Files changed (134) hide show
  1. base_agent/__init__.py +49 -0
  2. base_agent/agent.py +1060 -0
  3. base_agent/config.py +220 -0
  4. base_agent/contracts/__init__.py +209 -0
  5. base_agent/contracts/access_control_summary_part.py +42 -0
  6. base_agent/contracts/agent_loop_part.py +87 -0
  7. base_agent/contracts/agent_tool_part.py +15 -0
  8. base_agent/contracts/append_entity_class_part.py +15 -0
  9. base_agent/contracts/broadcast_message.py +30 -0
  10. base_agent/contracts/broadcast_message_type.py +13 -0
  11. base_agent/contracts/cache_stability_metadata.py +27 -0
  12. base_agent/contracts/code_generation.py +18 -0
  13. base_agent/contracts/context_management_notification.py +20 -0
  14. base_agent/contracts/context_management_pipeline.py +21 -0
  15. base_agent/contracts/cross_vector_similarity_search_request.py +14 -0
  16. base_agent/contracts/data_classification_tag.py +12 -0
  17. base_agent/contracts/data_lineage_part.py +46 -0
  18. base_agent/contracts/data_part.py +15 -0
  19. base_agent/contracts/data_pointer_part.py +20 -0
  20. base_agent/contracts/embedding_request.py +15 -0
  21. base_agent/contracts/embedding_response.py +21 -0
  22. base_agent/contracts/entity_matching_form_data.py +79 -0
  23. base_agent/contracts/entity_vector_text_search_match.py +12 -0
  24. base_agent/contracts/entity_vector_text_search_request.py +14 -0
  25. base_agent/contracts/entity_vector_text_search_result.py +24 -0
  26. base_agent/contracts/entity_vectorization_job_info.py +24 -0
  27. base_agent/contracts/entity_vectorization_jobs_result.py +30 -0
  28. base_agent/contracts/entity_vectorization_progress.py +14 -0
  29. base_agent/contracts/entity_vectorization_request.py +15 -0
  30. base_agent/contracts/entity_vectorization_result.py +17 -0
  31. base_agent/contracts/envelope.py +20 -0
  32. base_agent/contracts/error_part.py +24 -0
  33. base_agent/contracts/error_part_type.py +14 -0
  34. base_agent/contracts/file_part.py +17 -0
  35. base_agent/contracts/file_pointer_part.py +17 -0
  36. base_agent/contracts/file_revectorization_request_event.py +19 -0
  37. base_agent/contracts/file_vectorization_request_event.py +22 -0
  38. base_agent/contracts/for_each_part.py +30 -0
  39. base_agent/contracts/fuzzy_matching_params.py +21 -0
  40. base_agent/contracts/group_plan.py +16 -0
  41. base_agent/contracts/header.py +69 -0
  42. base_agent/contracts/human_input_request.py +16 -0
  43. base_agent/contracts/inspector_pipeline_item.py +19 -0
  44. base_agent/contracts/list_entity_vectorization_jobs_request.py +12 -0
  45. base_agent/contracts/llm_accounting_part.py +33 -0
  46. base_agent/contracts/llm_error_notification.py +13 -0
  47. base_agent/contracts/llm_judge_job.py +84 -0
  48. base_agent/contracts/llm_judge_params.py +15 -0
  49. base_agent/contracts/llm_judge_progress.py +20 -0
  50. base_agent/contracts/llm_model_part.py +72 -0
  51. base_agent/contracts/llm_reasoning_summary.py +23 -0
  52. base_agent/contracts/llm_response.py +25 -0
  53. base_agent/contracts/llm_response_part.py +54 -0
  54. base_agent/contracts/match_entities_job.py +43 -0
  55. base_agent/contracts/match_result.py +52 -0
  56. base_agent/contracts/notification_error_type.py +12 -0
  57. base_agent/contracts/notification_severity.py +14 -0
  58. base_agent/contracts/on_trigger_fired.py +17 -0
  59. base_agent/contracts/parallel_execution_plan.py +25 -0
  60. base_agent/contracts/part.py +30 -0
  61. base_agent/contracts/resume_entity_vectorization_request.py +11 -0
  62. base_agent/contracts/room_dataset_catalog.py +44 -0
  63. base_agent/contracts/room_dataset_catalog_entry.py +38 -0
  64. base_agent/contracts/room_dataset_column.py +12 -0
  65. base_agent/contracts/room_dataset_schema.py +24 -0
  66. base_agent/contracts/scratch_pad_content.py +14 -0
  67. base_agent/contracts/search_request.py +27 -0
  68. base_agent/contracts/search_response.py +39 -0
  69. base_agent/contracts/search_result.py +21 -0
  70. base_agent/contracts/search_type_result.py +13 -0
  71. base_agent/contracts/semantic_search_progress.py +18 -0
  72. base_agent/contracts/semantic_search_result.py +25 -0
  73. base_agent/contracts/stream_content_message.py +35 -0
  74. base_agent/contracts/stream_event_type.py +12 -0
  75. base_agent/contracts/stream_message.py +14 -0
  76. base_agent/contracts/stream_metadata.py +28 -0
  77. base_agent/contracts/task_artifact_update.py +20 -0
  78. base_agent/contracts/task_error_response.py +22 -0
  79. base_agent/contracts/task_ref.py +19 -0
  80. base_agent/contracts/task_request.py +21 -0
  81. base_agent/contracts/task_response.py +21 -0
  82. base_agent/contracts/task_status_update.py +31 -0
  83. base_agent/contracts/text_part.py +15 -0
  84. base_agent/contracts/tool_authorization_request.py +21 -0
  85. base_agent/contracts/tool_call_index.py +42 -0
  86. base_agent/contracts/tool_call_index_entry.py +32 -0
  87. base_agent/contracts/tool_call_initiation.py +27 -0
  88. base_agent/contracts/tool_learning_request.py +34 -0
  89. base_agent/contracts/tool_response_completion.py +19 -0
  90. base_agent/contracts/tool_response_part.py +31 -0
  91. base_agent/contracts/usage_metadata.py +11 -0
  92. base_agent/contracts/user_data_notification.py +13 -0
  93. base_agent/contracts/user_notification.py +21 -0
  94. base_agent/contracts/vector_similarity_search_request.py +13 -0
  95. base_agent/contracts/vector_similarity_search_result.py +25 -0
  96. base_agent/contracts/workflow_part.py +17 -0
  97. base_agent/exceptions.py +39 -0
  98. base_agent/health/__init__.py +5 -0
  99. base_agent/health/server.py +191 -0
  100. base_agent/messaging/__init__.py +5 -0
  101. base_agent/messaging/kafka_client.py +572 -0
  102. base_agent/ordering/__init__.py +5 -0
  103. base_agent/ordering/vector_clock.py +176 -0
  104. base_agent/prompts/__init__.py +10 -0
  105. base_agent/prompts/prompt_manager.py +213 -0
  106. base_agent/registration/__init__.py +5 -0
  107. base_agent/registration/registration_client.py +364 -0
  108. base_agent/schemas/__init__.py +14 -0
  109. base_agent/schemas/models.py +30 -0
  110. base_agent/schemas/schema_registry_client.py +493 -0
  111. base_agent/schemas/source_type.py +24 -0
  112. base_agent/schemas/technical_name_validator.py +147 -0
  113. base_agent/state/__init__.py +13 -0
  114. base_agent/state/logical_clock_tracker.py +50 -0
  115. base_agent/state/session_tracker.py +91 -0
  116. base_agent/state/store.py +333 -0
  117. base_agent/storage/__init__.py +27 -0
  118. base_agent/storage/azure_store.py +615 -0
  119. base_agent/storage/exceptions.py +26 -0
  120. base_agent/storage/models.py +73 -0
  121. base_agent/storage/object_store.py +248 -0
  122. base_agent/storage/object_store_factory.py +136 -0
  123. base_agent/storage/s3_store.py +411 -0
  124. base_agent/telemetry/__init__.py +1 -0
  125. base_agent/tools/__init__.py +12 -0
  126. base_agent/tools/models.py +66 -0
  127. base_agent/tools/tool_registry_client.py +607 -0
  128. base_agent/utils/__init__.py +5 -0
  129. base_agent/utils/logger.py +146 -0
  130. base_agent/utils/version_utils.py +92 -0
  131. python_base_agent-2026.2.13.dist-info/METADATA +536 -0
  132. python_base_agent-2026.2.13.dist-info/RECORD +134 -0
  133. python_base_agent-2026.2.13.dist-info/WHEEL +5 -0
  134. 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
+ ]