solace-agent-mesh 1.5.1__py3-none-any.whl → 1.6.1__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.

Potentially problematic release.


This version of solace-agent-mesh might be problematic. Click here for more details.

Files changed (184) hide show
  1. solace_agent_mesh/agent/adk/callbacks.py +0 -5
  2. solace_agent_mesh/agent/adk/models/lite_llm.py +123 -8
  3. solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +245 -0
  4. solace_agent_mesh/agent/protocol/event_handlers.py +213 -31
  5. solace_agent_mesh/agent/proxies/__init__.py +0 -0
  6. solace_agent_mesh/agent/proxies/a2a/__init__.py +3 -0
  7. solace_agent_mesh/agent/proxies/a2a/app.py +55 -0
  8. solace_agent_mesh/agent/proxies/a2a/component.py +1115 -0
  9. solace_agent_mesh/agent/proxies/a2a/config.py +140 -0
  10. solace_agent_mesh/agent/proxies/a2a/oauth_token_cache.py +104 -0
  11. solace_agent_mesh/agent/proxies/base/__init__.py +3 -0
  12. solace_agent_mesh/agent/proxies/base/app.py +99 -0
  13. solace_agent_mesh/agent/proxies/base/component.py +650 -0
  14. solace_agent_mesh/agent/proxies/base/config.py +85 -0
  15. solace_agent_mesh/agent/proxies/base/proxy_task_context.py +17 -0
  16. solace_agent_mesh/agent/sac/app.py +58 -5
  17. solace_agent_mesh/agent/sac/component.py +238 -75
  18. solace_agent_mesh/agent/sac/task_execution_context.py +46 -0
  19. solace_agent_mesh/agent/tools/audio_tools.py +125 -8
  20. solace_agent_mesh/agent/tools/web_tools.py +10 -5
  21. solace_agent_mesh/agent/utils/artifact_helpers.py +141 -3
  22. solace_agent_mesh/assets/docs/404.html +3 -3
  23. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +1 -0
  25. solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +1 -0
  26. solace_agent_mesh/assets/docs/assets/js/77cf947d.48cb18a2.js +1 -0
  27. solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +1 -0
  28. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.570c057b.js +1 -0
  29. solace_agent_mesh/assets/docs/assets/js/{ad71b5ed.60668e9e.js → ad71b5ed.af3ecfd1.js} +1 -1
  30. solace_agent_mesh/assets/docs/assets/js/ceb2a7a6.5d92d7d0.js +1 -0
  31. solace_agent_mesh/assets/docs/assets/js/{da0b5bad.9d369087.js → da0b5bad.d08a9466.js} +1 -1
  32. solace_agent_mesh/assets/docs/assets/js/db924877.e98d12a1.js +1 -0
  33. solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +1 -0
  34. solace_agent_mesh/assets/docs/assets/js/{e3d9abda.2b916f9e.js → e3d9abda.6b9493d0.js} +1 -1
  35. solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +1 -0
  36. solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +1 -0
  37. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +1 -0
  38. solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js → main.b12eac43.js} +2 -2
  39. solace_agent_mesh/assets/docs/assets/js/runtime~main.e268214e.js +1 -0
  40. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +15 -4
  41. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +4 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +4 -4
  43. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +4 -4
  44. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +4 -4
  45. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +4 -4
  46. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +4 -4
  49. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +4 -4
  50. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +4 -4
  51. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +262 -0
  52. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +3 -3
  53. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +31 -3
  54. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +3 -3
  55. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +3 -3
  56. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +4 -4
  57. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +5 -5
  58. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +4 -4
  59. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +4 -4
  60. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +135 -0
  61. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +6 -4
  62. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +4 -4
  63. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +4 -4
  64. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +4 -4
  65. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +5 -5
  66. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +4 -4
  67. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +4 -4
  68. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +4 -4
  69. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +4 -4
  70. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +4 -4
  71. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +4 -4
  72. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +3 -3
  73. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +3 -3
  74. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +3 -3
  75. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +3 -3
  76. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +3 -3
  77. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +3 -3
  78. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  79. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
  80. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +6 -5
  81. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +3 -3
  82. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +3 -3
  83. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +100 -3
  84. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +3 -3
  85. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
  86. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
  87. solace_agent_mesh/assets/docs/lunr-index-1761248203150.json +1 -0
  88. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  89. solace_agent_mesh/assets/docs/search-doc-1761248203150.json +1 -0
  90. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  91. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  92. solace_agent_mesh/cli/__init__.py +1 -1
  93. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +2 -69
  94. solace_agent_mesh/cli/commands/eval_cmd.py +11 -49
  95. solace_agent_mesh/cli/commands/init_cmd/__init__.py +0 -5
  96. solace_agent_mesh/cli/commands/init_cmd/env_step.py +10 -12
  97. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +9 -61
  98. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +9 -49
  99. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +1 -2
  100. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-DwrxZE0E.js → authCallback-BTf6dqwp.js} +1 -1
  101. solace_agent_mesh/client/webui/frontend/static/assets/{client-DarGQzyw.js → client-CaY59VuC.js} +1 -1
  102. solace_agent_mesh/client/webui/frontend/static/assets/main-B32noGmR.js +342 -0
  103. solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +1 -0
  104. solace_agent_mesh/client/webui/frontend/static/assets/{vendor-BKIeiHj_.js → vendor-BEmvJSYz.js} +1 -1
  105. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  106. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  107. solace_agent_mesh/common/a2a/__init__.py +24 -0
  108. solace_agent_mesh/common/a2a/artifact.py +39 -0
  109. solace_agent_mesh/common/a2a/events.py +29 -0
  110. solace_agent_mesh/common/a2a/message.py +68 -0
  111. solace_agent_mesh/common/a2a/protocol.py +151 -1
  112. solace_agent_mesh/common/agent_registry.py +83 -3
  113. solace_agent_mesh/common/constants.py +3 -1
  114. solace_agent_mesh/common/sac/sam_component_base.py +383 -4
  115. solace_agent_mesh/common/utils/pydantic_utils.py +12 -0
  116. solace_agent_mesh/config_portal/backend/common.py +1 -1
  117. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +98 -0
  118. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-44d62be6.js → manifest-61038fc6.js} +1 -1
  119. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  120. solace_agent_mesh/evaluation/evaluator.py +128 -104
  121. solace_agent_mesh/evaluation/message_organizer.py +116 -110
  122. solace_agent_mesh/evaluation/report_data_processor.py +84 -86
  123. solace_agent_mesh/evaluation/report_generator.py +73 -79
  124. solace_agent_mesh/evaluation/run.py +421 -235
  125. solace_agent_mesh/evaluation/shared/__init__.py +92 -0
  126. solace_agent_mesh/evaluation/shared/constants.py +47 -0
  127. solace_agent_mesh/evaluation/shared/exceptions.py +50 -0
  128. solace_agent_mesh/evaluation/shared/helpers.py +35 -0
  129. solace_agent_mesh/evaluation/shared/test_case_loader.py +167 -0
  130. solace_agent_mesh/evaluation/shared/test_suite_loader.py +280 -0
  131. solace_agent_mesh/evaluation/subscriber.py +111 -232
  132. solace_agent_mesh/evaluation/summary_builder.py +227 -117
  133. solace_agent_mesh/gateway/base/app.py +16 -1
  134. solace_agent_mesh/gateway/base/component.py +112 -39
  135. solace_agent_mesh/gateway/http_sse/alembic/versions/20251015_add_session_performance_indexes.py +70 -0
  136. solace_agent_mesh/gateway/http_sse/component.py +99 -3
  137. solace_agent_mesh/gateway/http_sse/dependencies.py +4 -4
  138. solace_agent_mesh/gateway/http_sse/main.py +1 -0
  139. solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +12 -13
  140. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +15 -18
  141. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +25 -18
  142. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +30 -26
  143. solace_agent_mesh/gateway/http_sse/repository/task_repository.py +35 -44
  144. solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +4 -3
  145. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +95 -203
  146. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +4 -3
  147. solace_agent_mesh/gateway/http_sse/routers/sessions.py +2 -2
  148. solace_agent_mesh/gateway/http_sse/routers/tasks.py +33 -41
  149. solace_agent_mesh/gateway/http_sse/routers/users.py +47 -1
  150. solace_agent_mesh/gateway/http_sse/routers/visualization.py +17 -11
  151. solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +4 -4
  152. solace_agent_mesh/gateway/http_sse/services/feedback_service.py +51 -43
  153. solace_agent_mesh/gateway/http_sse/services/session_service.py +20 -20
  154. solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +8 -8
  155. solace_agent_mesh/gateway/http_sse/shared/base_repository.py +45 -71
  156. solace_agent_mesh/gateway/http_sse/shared/types.py +0 -18
  157. solace_agent_mesh/templates/gateway_config_template.yaml +0 -5
  158. solace_agent_mesh/templates/logging_config_template.ini +10 -6
  159. solace_agent_mesh/templates/plugin_gateway_config_template.yaml +0 -3
  160. solace_agent_mesh/templates/shared_config.yaml +40 -0
  161. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/METADATA +47 -21
  162. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/RECORD +166 -145
  163. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.e49689dd.js +0 -1
  164. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.39d5851d.js +0 -1
  165. solace_agent_mesh/assets/docs/assets/js/71da7b71.804d6567.js +0 -1
  166. solace_agent_mesh/assets/docs/assets/js/77cf947d.64c9bd6c.js +0 -1
  167. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.dd810042.js +0 -1
  168. solace_agent_mesh/assets/docs/assets/js/db924877.cbc66f02.js +0 -1
  169. solace_agent_mesh/assets/docs/assets/js/de915948.139b4b9c.js +0 -1
  170. solace_agent_mesh/assets/docs/assets/js/e6f9706b.582a78ca.js +0 -1
  171. solace_agent_mesh/assets/docs/assets/js/f284c35a.5766a13d.js +0 -1
  172. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.9c0297a6.js +0 -1
  173. solace_agent_mesh/assets/docs/assets/js/runtime~main.18dc45dd.js +0 -1
  174. solace_agent_mesh/assets/docs/lunr-index-1760121512891.json +0 -1
  175. solace_agent_mesh/assets/docs/search-doc-1760121512891.json +0 -1
  176. solace_agent_mesh/client/webui/frontend/static/assets/main-2nd1gbaH.js +0 -339
  177. solace_agent_mesh/client/webui/frontend/static/assets/main-DoKXctCM.css +0 -1
  178. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-BNuqpWDc.js +0 -98
  179. solace_agent_mesh/evaluation/config_loader.py +0 -657
  180. solace_agent_mesh/evaluation/test_case_loader.py +0 -714
  181. /solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js.LICENSE.txt → main.b12eac43.js.LICENSE.txt} +0 -0
  182. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/WHEEL +0 -0
  183. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/entry_points.txt +0 -0
  184. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,714 +0,0 @@
1
- """
2
- Refactored test case loader with comprehensive validation and error handling.
3
- This module provides robust test case loading with validation for every field.
4
- """
5
-
6
- import json
7
- import os
8
- import sys
9
- from dataclasses import dataclass, field
10
- from typing import Dict, List, Optional, Any, Union
11
- from enum import Enum
12
- from pathlib import Path
13
- import logging
14
-
15
- # Set up logging
16
- logging.basicConfig(level=logging.INFO)
17
- logger = logging.getLogger(__name__)
18
-
19
- SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
20
-
21
-
22
- class ValidationLevel(Enum):
23
- """Defines the validation level for test case fields."""
24
-
25
- REQUIRED = "required"
26
- OPTIONAL = "optional"
27
- CONDITIONAL = "conditional"
28
-
29
-
30
- class TestCaseError(Exception):
31
- """Base exception for test case-related errors."""
32
-
33
- pass
34
-
35
-
36
- class TestCaseFileNotFoundError(TestCaseError):
37
- """Raised when the test case file is not found."""
38
-
39
- pass
40
-
41
-
42
- class TestCaseParseError(TestCaseError):
43
- """Raised when the test case file cannot be parsed."""
44
-
45
- pass
46
-
47
-
48
- class TestCaseValidationError(TestCaseError):
49
- """Raised when test case validation fails."""
50
-
51
- pass
52
-
53
-
54
- class ArtifactValidationError(TestCaseError):
55
- """Raised when artifact validation fails."""
56
-
57
- pass
58
-
59
-
60
- @dataclass
61
- class ValidationReport:
62
- """Detailed validation report with all errors and warnings."""
63
-
64
- def __init__(self):
65
- self.errors: List[str] = []
66
- self.warnings: List[str] = []
67
-
68
- def add_error(self, field: str, message: str):
69
- """Add a validation error."""
70
- self.errors.append(f"Field '{field}': {message}")
71
-
72
- def add_warning(self, field: str, message: str):
73
- """Add a validation warning."""
74
- self.warnings.append(f"Field '{field}': {message}")
75
-
76
- def has_errors(self) -> bool:
77
- """Check if there are any validation errors."""
78
- return len(self.errors) > 0
79
-
80
- def get_summary(self) -> str:
81
- """Get human-readable validation summary."""
82
- summary = []
83
- if self.errors:
84
- summary.append("ERRORS:")
85
- summary.extend([f" - {error}" for error in self.errors])
86
- if self.warnings:
87
- summary.append("WARNINGS:")
88
- summary.extend([f" - {warning}" for warning in self.warnings])
89
- return "\n".join(summary) if summary else "Test case validation passed."
90
-
91
-
92
- @dataclass
93
- class FieldValidator:
94
- """Defines validation rules for a test case field."""
95
-
96
- name: str
97
- validation_level: ValidationLevel
98
- field_type: type
99
- default_value: Any = None
100
- min_length: Optional[int] = None
101
- max_length: Optional[int] = None
102
- min_value: Optional[Union[int, float]] = None
103
- max_value: Optional[Union[int, float]] = None
104
- allowed_values: Optional[List[Any]] = None
105
- pattern: Optional[str] = None
106
- custom_validator: Optional[str] = None
107
-
108
-
109
- @dataclass
110
- class ArtifactConfig:
111
- """Individual artifact configuration with validation."""
112
-
113
- artifact_type: str
114
- path: str
115
-
116
- def __post_init__(self):
117
- """Validate artifact configuration after initialization."""
118
- if not self.artifact_type or self.artifact_type.strip() == "":
119
- raise ArtifactValidationError("Artifact type cannot be empty")
120
-
121
- if not self.path or self.path.strip() == "":
122
- raise ArtifactValidationError("Artifact path cannot be empty")
123
-
124
- # Validate artifact type
125
- allowed_types = ["file", "url", "text"]
126
- if self.artifact_type not in allowed_types:
127
- raise ArtifactValidationError(
128
- f"Artifact type must be one of {allowed_types}, got '{self.artifact_type}'"
129
- )
130
-
131
- # Validate file path security (prevent directory traversal)
132
- if self.artifact_type == "file":
133
- normalized_path = os.path.normpath(self.path)
134
- if normalized_path.startswith("..") or os.path.isabs(normalized_path):
135
- raise ArtifactValidationError(
136
- f"Artifact path '{self.path}' is not safe (no absolute paths or directory traversal)"
137
- )
138
-
139
-
140
- @dataclass
141
- class EvaluationConfig:
142
- """Evaluation criteria configuration with validation."""
143
-
144
- expected_tools: List[str] = field(default_factory=list)
145
- expected_response: str = ""
146
- criterion: str = ""
147
-
148
- def __post_init__(self):
149
- """Validate evaluation configuration after initialization."""
150
- # Validate expected_tools
151
- if not isinstance(self.expected_tools, list):
152
- raise TestCaseValidationError("expected_tools must be a list")
153
-
154
- # Validate tool names format
155
- for tool in self.expected_tools:
156
- if not isinstance(tool, str) or not tool.strip():
157
- raise TestCaseValidationError(
158
- f"Tool name must be a non-empty string, got '{tool}'"
159
- )
160
-
161
- # Validate strings
162
- if not isinstance(self.expected_response, str):
163
- raise TestCaseValidationError("expected_response must be a string")
164
-
165
- if not isinstance(self.criterion, str):
166
- raise TestCaseValidationError("criterion must be a string")
167
-
168
-
169
- @dataclass
170
- class TestCase:
171
- """Complete test case configuration with validation."""
172
-
173
- test_case_id: str
174
- query: str
175
- target_agent: str
176
- category: str = "Other"
177
- description: str = "No description provided."
178
- wait_time: int = 60
179
- artifacts: List[ArtifactConfig] = field(default_factory=list)
180
- evaluation: EvaluationConfig = field(default_factory=EvaluationConfig)
181
-
182
- def __post_init__(self):
183
- """Validate the complete test case after initialization."""
184
- # Validate required string fields
185
- if not self.test_case_id or not self.test_case_id.strip():
186
- raise TestCaseValidationError("test_case_id cannot be empty")
187
-
188
- if not self.query or not self.query.strip():
189
- raise TestCaseValidationError("query cannot be empty")
190
-
191
- if not self.target_agent or not self.target_agent.strip():
192
- raise TestCaseValidationError("target_agent cannot be empty")
193
-
194
- # Validate wait_time
195
- if not isinstance(self.wait_time, int) or self.wait_time < 1:
196
- raise TestCaseValidationError("wait_time must be a positive integer")
197
-
198
- if self.wait_time > 300: # 5 minutes max
199
- raise TestCaseValidationError("wait_time cannot exceed 300 seconds")
200
-
201
- # Validate artifacts
202
- if not isinstance(self.artifacts, list):
203
- raise TestCaseValidationError("artifacts must be a list")
204
-
205
- def to_dict(self) -> Dict[str, Any]:
206
- """Convert test case to dictionary format for JSON serialization."""
207
- return {
208
- "test_case_id": self.test_case_id,
209
- "category": self.category,
210
- "description": self.description,
211
- "query": self.query,
212
- "target_agent": self.target_agent,
213
- "wait_time": self.wait_time,
214
- "artifacts": [
215
- {"type": artifact.artifact_type, "path": artifact.path}
216
- for artifact in self.artifacts
217
- ],
218
- "evaluation": {
219
- "expected_tools": self.evaluation.expected_tools,
220
- "expected_response": self.evaluation.expected_response,
221
- "criterion": self.evaluation.criterion,
222
- },
223
- }
224
-
225
-
226
- class TestCaseValidator:
227
- """Comprehensive test case validator with detailed error reporting."""
228
-
229
- # Validation rules for root-level fields
230
- ROOT_LEVEL_RULES = {
231
- "test_case_id": FieldValidator(
232
- name="test_case_id",
233
- validation_level=ValidationLevel.REQUIRED,
234
- field_type=str,
235
- min_length=1,
236
- max_length=100,
237
- ),
238
- "query": FieldValidator(
239
- name="query",
240
- validation_level=ValidationLevel.REQUIRED,
241
- field_type=str,
242
- min_length=1,
243
- max_length=2000,
244
- ),
245
- "target_agent": FieldValidator(
246
- name="target_agent",
247
- validation_level=ValidationLevel.REQUIRED,
248
- field_type=str,
249
- min_length=1,
250
- max_length=100,
251
- ),
252
- "category": FieldValidator(
253
- name="category",
254
- validation_level=ValidationLevel.OPTIONAL,
255
- field_type=str,
256
- default_value="Other",
257
- max_length=100,
258
- ),
259
- "description": FieldValidator(
260
- name="description",
261
- validation_level=ValidationLevel.OPTIONAL,
262
- field_type=str,
263
- default_value="No description provided.",
264
- max_length=1000,
265
- ),
266
- "wait_time": FieldValidator(
267
- name="wait_time",
268
- validation_level=ValidationLevel.OPTIONAL,
269
- field_type=int,
270
- default_value=60,
271
- min_value=1,
272
- max_value=300,
273
- ),
274
- "artifacts": FieldValidator(
275
- name="artifacts",
276
- validation_level=ValidationLevel.OPTIONAL,
277
- field_type=list,
278
- default_value=[],
279
- ),
280
- "evaluation": FieldValidator(
281
- name="evaluation",
282
- validation_level=ValidationLevel.OPTIONAL,
283
- field_type=dict,
284
- default_value={},
285
- ),
286
- }
287
-
288
- # Validation rules for evaluation fields
289
- EVALUATION_RULES = {
290
- "expected_tools": FieldValidator(
291
- name="expected_tools",
292
- validation_level=ValidationLevel.OPTIONAL,
293
- field_type=list,
294
- default_value=[],
295
- ),
296
- "expected_response": FieldValidator(
297
- name="expected_response",
298
- validation_level=ValidationLevel.OPTIONAL,
299
- field_type=str,
300
- default_value="",
301
- max_length=2000,
302
- ),
303
- "criterion": FieldValidator(
304
- name="criterion",
305
- validation_level=ValidationLevel.OPTIONAL,
306
- field_type=str,
307
- default_value="",
308
- max_length=1000,
309
- ),
310
- }
311
-
312
- # Validation rules for artifact fields
313
- ARTIFACT_RULES = {
314
- "type": FieldValidator(
315
- name="type",
316
- validation_level=ValidationLevel.REQUIRED,
317
- field_type=str,
318
- allowed_values=["file", "url", "text"],
319
- ),
320
- "path": FieldValidator(
321
- name="path",
322
- validation_level=ValidationLevel.REQUIRED,
323
- field_type=str,
324
- min_length=1,
325
- max_length=500,
326
- ),
327
- }
328
-
329
- def __init__(self, test_cases_dir: str):
330
- self.test_cases_dir = test_cases_dir
331
- self.report = ValidationReport()
332
-
333
- def validate_field(
334
- self,
335
- field_name: str,
336
- value: Any,
337
- rules: Dict[str, FieldValidator],
338
- context: str = "",
339
- ) -> Any:
340
- """Validate individual field with comprehensive checks."""
341
- rule = rules.get(field_name)
342
- if not rule:
343
- self.report.add_warning(
344
- f"{context}.{field_name}" if context else field_name,
345
- "Unknown field in test case",
346
- )
347
- return value
348
-
349
- full_field_name = f"{context}.{field_name}" if context else field_name
350
-
351
- # Handle missing required fields
352
- if value is None:
353
- if rule.validation_level == ValidationLevel.REQUIRED:
354
- self.report.add_error(full_field_name, "Required field is missing")
355
- return None
356
- else:
357
- return rule.default_value
358
-
359
- # Type validation
360
- if not isinstance(value, rule.field_type):
361
- self.report.add_error(
362
- full_field_name,
363
- f"Expected {rule.field_type.__name__}, got {type(value).__name__}",
364
- )
365
- return rule.default_value
366
-
367
- # Length validation for lists and strings
368
- if rule.min_length is not None:
369
- if hasattr(value, "__len__") and len(value) < rule.min_length:
370
- self.report.add_error(
371
- full_field_name,
372
- f"Minimum length is {rule.min_length}, got {len(value)}",
373
- )
374
- return rule.default_value
375
-
376
- if rule.max_length is not None:
377
- if hasattr(value, "__len__") and len(value) > rule.max_length:
378
- self.report.add_error(
379
- full_field_name,
380
- f"Maximum length is {rule.max_length}, got {len(value)}",
381
- )
382
- # Truncate for strings, return default for lists
383
- if isinstance(value, str):
384
- return value[: rule.max_length]
385
- else:
386
- return rule.default_value
387
-
388
- # Value range validation for numbers
389
- if rule.min_value is not None and isinstance(value, (int, float)):
390
- if value < rule.min_value:
391
- self.report.add_error(
392
- full_field_name, f"Minimum value is {rule.min_value}, got {value}"
393
- )
394
- return rule.default_value
395
-
396
- if rule.max_value is not None and isinstance(value, (int, float)):
397
- if value > rule.max_value:
398
- self.report.add_error(
399
- full_field_name, f"Maximum value is {rule.max_value}, got {value}"
400
- )
401
- return rule.default_value
402
-
403
- # Allowed values validation
404
- if rule.allowed_values is not None and value not in rule.allowed_values:
405
- self.report.add_error(
406
- full_field_name,
407
- f"Value must be one of {rule.allowed_values}, got '{value}'",
408
- )
409
- return rule.default_value
410
-
411
- return value
412
-
413
- def validate_artifact(
414
- self, artifact_data: Dict[str, Any], index: int
415
- ) -> Optional[ArtifactConfig]:
416
- """Validate individual artifact configuration."""
417
- context = f"artifacts[{index}]"
418
-
419
- # Validate required fields
420
- artifact_type = self.validate_field(
421
- "type", artifact_data.get("type"), self.ARTIFACT_RULES, context
422
- )
423
- path = self.validate_field(
424
- "path", artifact_data.get("path"), self.ARTIFACT_RULES, context
425
- )
426
-
427
- if not artifact_type or not path:
428
- self.report.add_error(context, "Invalid artifact configuration")
429
- return None
430
-
431
- try:
432
- artifact = ArtifactConfig(artifact_type=artifact_type, path=path)
433
-
434
- # Additional file existence check for file artifacts
435
- if artifact.artifact_type == "file":
436
- full_path = os.path.join(self.test_cases_dir, artifact.path)
437
- if not os.path.exists(full_path):
438
- self.report.add_warning(
439
- f"{context}.path", f"Artifact file does not exist: {full_path}"
440
- )
441
-
442
- return artifact
443
-
444
- except ArtifactValidationError as e:
445
- self.report.add_error(context, str(e))
446
- return None
447
-
448
- def validate_evaluation(self, eval_data: Dict[str, Any]) -> EvaluationConfig:
449
- """Validate evaluation configuration."""
450
- context = "evaluation"
451
-
452
- # Validate evaluation fields
453
- expected_tools = self.validate_field(
454
- "expected_tools",
455
- eval_data.get("expected_tools"),
456
- self.EVALUATION_RULES,
457
- context,
458
- )
459
- expected_response = self.validate_field(
460
- "expected_response",
461
- eval_data.get("expected_response"),
462
- self.EVALUATION_RULES,
463
- context,
464
- )
465
- criterion = self.validate_field(
466
- "criterion", eval_data.get("criterion"), self.EVALUATION_RULES, context
467
- )
468
-
469
- try:
470
- return EvaluationConfig(
471
- expected_tools=expected_tools or [],
472
- expected_response=expected_response or "",
473
- criterion=criterion or "",
474
- )
475
- except TestCaseValidationError as e:
476
- self.report.add_error(context, str(e))
477
- # Return default evaluation config if validation fails
478
- return EvaluationConfig()
479
-
480
-
481
- class TestCaseLoader:
482
- """Loads test case files with comprehensive error handling."""
483
-
484
- def __init__(self, test_cases_dir: str):
485
- self.test_cases_dir = test_cases_dir
486
-
487
- def load_file(self, test_case_id: str) -> Dict[str, Any]:
488
- """Load test case file with comprehensive error handling."""
489
- # Normalize test case ID
490
- if test_case_id.endswith(".test.json"):
491
- test_case_id = test_case_id.replace(".test.json", "")
492
-
493
- test_case_path = os.path.join(self.test_cases_dir, f"{test_case_id}.test.json")
494
-
495
- try:
496
- with open(test_case_path, "r") as f:
497
- return json.load(f)
498
- except FileNotFoundError:
499
- raise TestCaseFileNotFoundError(
500
- f"Test case file not found: {test_case_path}"
501
- )
502
- except json.JSONDecodeError as e:
503
- raise TestCaseParseError(
504
- f"Invalid JSON in test case file {test_case_path}: {e}"
505
- )
506
- except Exception as e:
507
- raise TestCaseError(f"Error reading test case file {test_case_path}: {e}")
508
-
509
-
510
- class TestCaseProcessor:
511
- """Main orchestrator for test case processing with comprehensive validation."""
512
-
513
- def __init__(self, test_cases_dir: str):
514
- self.loader = TestCaseLoader(test_cases_dir)
515
- self.validator = TestCaseValidator(test_cases_dir)
516
-
517
- def load_and_process(self, test_case_id: str) -> Dict[str, Any]:
518
- """Load and process test case, returning the same format as original."""
519
- try:
520
- # Load raw test case
521
- raw_test_case = self.loader.load_file(test_case_id)
522
-
523
- # Process and validate
524
- processed_test_case = self._process_test_case(raw_test_case, test_case_id)
525
-
526
- # Check for validation errors
527
- if self.validator.report.has_errors():
528
- error_summary = self.validator.report.get_summary()
529
- print(
530
- f"Test case validation failed for '{test_case_id}':\n{error_summary}"
531
- )
532
- sys.exit(1)
533
-
534
- # Log warnings if any
535
- if self.validator.report.warnings:
536
- for warning in self.validator.report.warnings:
537
- logger.warning(f"Test case '{test_case_id}': {warning}")
538
-
539
- return processed_test_case
540
-
541
- except TestCaseFileNotFoundError:
542
- print(
543
- f"Error: Test case file not found for '{test_case_id}' in {self.loader.test_cases_dir}"
544
- )
545
- sys.exit(1)
546
- except TestCaseParseError as e:
547
- print(f"Error: Could not decode JSON from test case '{test_case_id}': {e}")
548
- sys.exit(1)
549
- except Exception as e:
550
- print(f"Error loading test case '{test_case_id}': {e}")
551
- sys.exit(1)
552
-
553
- def _process_test_case(
554
- self, raw_test_case: Dict[str, Any], test_case_id: str
555
- ) -> Dict[str, Any]:
556
- """Process and validate the raw test case."""
557
- # Validate and set defaults for root-level fields
558
- validated_test_case_id = self.validator.validate_field(
559
- "test_case_id",
560
- raw_test_case.get("test_case_id"),
561
- self.validator.ROOT_LEVEL_RULES,
562
- )
563
-
564
- query = self.validator.validate_field(
565
- "query", raw_test_case.get("query"), self.validator.ROOT_LEVEL_RULES
566
- )
567
-
568
- target_agent = self.validator.validate_field(
569
- "target_agent",
570
- raw_test_case.get("target_agent"),
571
- self.validator.ROOT_LEVEL_RULES,
572
- )
573
-
574
- category = self.validator.validate_field(
575
- "category", raw_test_case.get("category"), self.validator.ROOT_LEVEL_RULES
576
- )
577
-
578
- description = self.validator.validate_field(
579
- "description",
580
- raw_test_case.get("description"),
581
- self.validator.ROOT_LEVEL_RULES,
582
- )
583
-
584
- wait_time = self.validator.validate_field(
585
- "wait_time", raw_test_case.get("wait_time"), self.validator.ROOT_LEVEL_RULES
586
- )
587
-
588
- # Process artifacts with validation
589
- artifacts_data = raw_test_case.get("artifacts", [])
590
- processed_artifacts = []
591
-
592
- if artifacts_data and isinstance(artifacts_data, list):
593
- for i, artifact_data in enumerate(artifacts_data):
594
- artifact = self.validator.validate_artifact(artifact_data, i)
595
- if artifact:
596
- processed_artifacts.append(
597
- {"type": artifact.artifact_type, "path": artifact.path}
598
- )
599
-
600
- # Process evaluation with validation
601
- evaluation_data = raw_test_case.get("evaluation", {})
602
- evaluation = self.validator.validate_evaluation(evaluation_data)
603
-
604
- processed_evaluation = {
605
- "expected_tools": evaluation.expected_tools,
606
- "expected_response": evaluation.expected_response,
607
- "criterion": evaluation.criterion,
608
- }
609
-
610
- # Return processed test case in original format
611
- return {
612
- "test_case_id": validated_test_case_id or test_case_id,
613
- "category": category or "Other",
614
- "description": description or "No description provided.",
615
- "query": query or "",
616
- "target_agent": target_agent or "",
617
- "wait_time": wait_time or 60,
618
- "artifacts": processed_artifacts,
619
- "evaluation": processed_evaluation,
620
- }
621
-
622
-
623
- # Main API function - same interface as original
624
- def load_test_case(test_case_path: str) -> Dict[str, Any]:
625
- """
626
- Load test case from a JSON file with comprehensive validation.
627
- Returns the same format as the original function.
628
-
629
- Args:
630
- test_case_path: The full path to the test case file.
631
-
632
- Returns:
633
- Dictionary containing the validated test case data
634
-
635
- Raises:
636
- SystemExit: If validation fails or file cannot be loaded
637
- """
638
- test_case_dir = str(Path(test_case_path).parent)
639
- test_case_filename = Path(test_case_path).name
640
- processor = TestCaseProcessor(test_cases_dir=test_case_dir)
641
- return processor.load_and_process(test_case_filename)
642
-
643
-
644
- def validate_test_case_file(test_case_path: str) -> ValidationReport:
645
- """
646
- Validate a test case file and return detailed validation report.
647
-
648
- Args:
649
- test_case_path: The full path to the test case file.
650
-
651
- Returns:
652
- ValidationReport with errors and warnings
653
- """
654
- try:
655
- test_case_dir = str(Path(test_case_path).parent)
656
- test_case_filename = Path(test_case_path).name
657
- processor = TestCaseProcessor(test_cases_dir=test_case_dir)
658
- processor.load_and_process(test_case_filename)
659
- return processor.validator.report
660
- except SystemExit:
661
- # Capture the validation report even if processing failed
662
- return processor.validator.report
663
- except Exception as e:
664
- report = ValidationReport()
665
- report.add_error("general", f"Unexpected error: {str(e)}")
666
- return report
667
-
668
-
669
- def main():
670
- """Main entry point for command-line usage and testing."""
671
- import sys
672
-
673
- if len(sys.argv) != 2:
674
- print("Usage: python test_case_loader.py <test_case_id>")
675
- print("Example: python test_case_loader.py hello_world")
676
- sys.exit(1)
677
-
678
- test_case_id = sys.argv[1]
679
-
680
- try:
681
- # Load and validate test case
682
- test_case = load_test_case(test_case_id)
683
-
684
- # Print results
685
- print(f"Successfully loaded test case: {test_case_id}")
686
- print(f"Target Agent: {test_case['target_agent']}")
687
- print(f"Category: {test_case['category']}")
688
- print(
689
- f"Query: {test_case['query'][:100]}{'...' if len(test_case['query']) > 100 else ''}"
690
- )
691
- print(f"Wait Time: {test_case['wait_time']} seconds")
692
- print(f"Artifacts: {len(test_case['artifacts'])} artifact(s)")
693
- print(
694
- f"Expected Tools: {len(test_case['evaluation']['expected_tools'])} tool(s)"
695
- )
696
-
697
- # Show validation report
698
- processor = TestCaseProcessor()
699
- report = validate_test_case_file(test_case_id)
700
- if report.warnings:
701
- print("\nWarnings:")
702
- for warning in report.warnings:
703
- print(f" - {warning}")
704
-
705
- except SystemExit:
706
- # Error already printed by load_test_case
707
- pass
708
- except Exception as e:
709
- print(f"Unexpected error: {e}")
710
- sys.exit(1)
711
-
712
-
713
- if __name__ == "__main__":
714
- main()