capiscio-sdk 0.2.0__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.
@@ -0,0 +1,299 @@
1
+ """Availability scorer for operational readiness.
2
+
3
+ Calculates availability score (0-100) based on:
4
+ - Primary endpoint (50 points)
5
+ - Transport protocol support (30 points)
6
+ - Response quality (20 points)
7
+
8
+ Only calculated when network tests are enabled.
9
+ """
10
+
11
+ from typing import List, Optional
12
+ from ..types import ValidationIssue
13
+ from .types import (
14
+ AvailabilityScore,
15
+ AvailabilityBreakdown,
16
+ PrimaryEndpointBreakdown,
17
+ TransportSupportBreakdown,
18
+ ResponseQualityBreakdown,
19
+ get_availability_rating,
20
+ )
21
+
22
+
23
+ class AvailabilityScorer:
24
+ """Calculates availability scores for operational readiness."""
25
+
26
+ def score_not_tested(self, reason: str = "Network tests not enabled") -> AvailabilityScore:
27
+ """Create availability score for when testing was skipped.
28
+
29
+ Args:
30
+ reason: Why availability wasn't tested
31
+
32
+ Returns:
33
+ AvailabilityScore with tested=False
34
+ """
35
+ return AvailabilityScore(
36
+ total=None,
37
+ rating=None,
38
+ breakdown=None,
39
+ issues=[],
40
+ tested=False,
41
+ not_tested_reason=reason
42
+ )
43
+
44
+ def score_endpoint_test(
45
+ self,
46
+ endpoint_responded: bool,
47
+ response_time: Optional[float] = None,
48
+ has_cors: Optional[bool] = None,
49
+ valid_tls: Optional[bool] = None,
50
+ issues: Optional[List[ValidationIssue]] = None
51
+ ) -> AvailabilityScore:
52
+ """Calculate availability score based on endpoint test results.
53
+
54
+ Args:
55
+ endpoint_responded: Whether primary endpoint responded
56
+ response_time: Response time in seconds (if responded)
57
+ has_cors: Whether CORS headers present
58
+ valid_tls: Whether TLS certificate is valid
59
+ issues: List of validation issues found
60
+
61
+ Returns:
62
+ AvailabilityScore with detailed breakdown
63
+ """
64
+ if issues is None:
65
+ issues = []
66
+
67
+ # Score primary endpoint (50 points)
68
+ primary = self._score_primary_endpoint(
69
+ endpoint_responded,
70
+ response_time,
71
+ has_cors,
72
+ valid_tls,
73
+ issues
74
+ )
75
+
76
+ # Score transport support (30 points)
77
+ # For now, basic scoring - can be enhanced with actual transport tests
78
+ transport = self._score_transport_support(endpoint_responded, issues)
79
+
80
+ # Score response quality (20 points)
81
+ response_quality = self._score_response_quality(endpoint_responded, issues)
82
+
83
+ # Calculate total
84
+ total = primary.score + transport.score + response_quality.score
85
+ total = max(0, min(100, total))
86
+
87
+ # Create breakdown
88
+ breakdown = AvailabilityBreakdown(
89
+ primary_endpoint=primary,
90
+ transport_support=transport,
91
+ response_quality=response_quality
92
+ )
93
+
94
+ # Extract issue messages
95
+ issue_messages = [
96
+ issue.message for issue in issues
97
+ if self._is_availability_issue(issue)
98
+ ]
99
+
100
+ return AvailabilityScore(
101
+ total=total,
102
+ rating=get_availability_rating(total),
103
+ breakdown=breakdown,
104
+ issues=issue_messages,
105
+ tested=True
106
+ )
107
+
108
+ def _score_primary_endpoint(
109
+ self,
110
+ responded: bool,
111
+ response_time: Optional[float],
112
+ has_cors: Optional[bool],
113
+ valid_tls: Optional[bool],
114
+ issues: List[ValidationIssue]
115
+ ) -> PrimaryEndpointBreakdown:
116
+ """Score primary endpoint (50 points).
117
+
118
+ Args:
119
+ responded: Whether endpoint responded
120
+ response_time: Response time in seconds
121
+ has_cors: Whether CORS present
122
+ valid_tls: Whether TLS valid
123
+ issues: Validation issues
124
+
125
+ Returns:
126
+ PrimaryEndpointBreakdown
127
+ """
128
+ score = 0
129
+ errors = []
130
+
131
+ # Responds (30 points)
132
+ if responded:
133
+ score += 30
134
+ else:
135
+ errors.append("Endpoint did not respond")
136
+
137
+ # Response time (10 points)
138
+ if response_time is not None:
139
+ if response_time < 2.0: # Under 2 seconds
140
+ score += 10
141
+ elif response_time < 5.0: # Under 5 seconds
142
+ score += 5
143
+ else:
144
+ errors.append(f"Slow response time: {response_time:.2f}s")
145
+
146
+ # Valid TLS (5 points)
147
+ if valid_tls:
148
+ score += 5
149
+ elif valid_tls is False:
150
+ errors.append("Invalid TLS certificate")
151
+
152
+ # CORS support (5 points)
153
+ if has_cors:
154
+ score += 5
155
+ elif has_cors is False:
156
+ errors.append("Missing CORS headers")
157
+
158
+ return PrimaryEndpointBreakdown(
159
+ score=score,
160
+ max_score=50,
161
+ responds=responded,
162
+ response_time=response_time,
163
+ has_cors=has_cors,
164
+ valid_tls=valid_tls,
165
+ errors=errors
166
+ )
167
+
168
+ def _score_transport_support(
169
+ self,
170
+ endpoint_responded: bool,
171
+ issues: List[ValidationIssue]
172
+ ) -> TransportSupportBreakdown:
173
+ """Score transport protocol support (30 points).
174
+
175
+ Args:
176
+ endpoint_responded: Whether primary endpoint worked
177
+ issues: Validation issues
178
+
179
+ Returns:
180
+ TransportSupportBreakdown
181
+ """
182
+ score = 0
183
+
184
+ # Preferred transport works (20 points)
185
+ preferred_works = endpoint_responded and not self._has_issue_code(
186
+ issues,
187
+ "TRANSPORT_FAILED"
188
+ )
189
+ if preferred_works:
190
+ score += 20
191
+
192
+ # Additional interfaces (10 points)
193
+ # This would require testing multiple transports
194
+ # For now, give partial credit if primary works
195
+ if preferred_works:
196
+ score += 10
197
+ additional_working = 1
198
+ additional_failed = 0
199
+ else:
200
+ additional_working = 0
201
+ additional_failed = 0
202
+
203
+ return TransportSupportBreakdown(
204
+ score=score,
205
+ max_score=30,
206
+ preferred_transport_works=preferred_works,
207
+ additional_interfaces_working=additional_working,
208
+ additional_interfaces_failed=additional_failed
209
+ )
210
+
211
+ def _score_response_quality(
212
+ self,
213
+ endpoint_responded: bool,
214
+ issues: List[ValidationIssue]
215
+ ) -> ResponseQualityBreakdown:
216
+ """Score response quality (20 points).
217
+
218
+ Args:
219
+ endpoint_responded: Whether endpoint responded
220
+ issues: Validation issues
221
+
222
+ Returns:
223
+ ResponseQualityBreakdown
224
+ """
225
+ score = 0
226
+
227
+ # Valid structure (10 points)
228
+ valid_structure = endpoint_responded and not self._has_issue_code(
229
+ issues,
230
+ ["INVALID_RESPONSE_STRUCTURE", "MALFORMED_JSON"]
231
+ )
232
+ if valid_structure:
233
+ score += 10
234
+
235
+ # Proper content type (5 points)
236
+ proper_content_type = endpoint_responded and not self._has_issue_code(
237
+ issues,
238
+ "INVALID_CONTENT_TYPE"
239
+ )
240
+ if proper_content_type:
241
+ score += 5
242
+
243
+ # Proper error handling (5 points)
244
+ # Check that errors are properly formatted
245
+ proper_errors = endpoint_responded and not self._has_issue_code(
246
+ issues,
247
+ "IMPROPER_ERROR_FORMAT"
248
+ )
249
+ if proper_errors:
250
+ score += 5
251
+
252
+ return ResponseQualityBreakdown(
253
+ score=score,
254
+ max_score=20,
255
+ valid_structure=valid_structure,
256
+ proper_content_type=proper_content_type,
257
+ proper_error_handling=proper_errors
258
+ )
259
+
260
+ def _is_availability_issue(self, issue: ValidationIssue) -> bool:
261
+ """Check if issue is availability-related.
262
+
263
+ Args:
264
+ issue: Validation issue to check
265
+
266
+ Returns:
267
+ True if availability-related
268
+ """
269
+ availability_codes = {
270
+ "ENDPOINT_UNREACHABLE",
271
+ "TRANSPORT_FAILED",
272
+ "TIMEOUT",
273
+ "INVALID_RESPONSE_STRUCTURE",
274
+ "MALFORMED_JSON",
275
+ "INVALID_CONTENT_TYPE",
276
+ "IMPROPER_ERROR_FORMAT",
277
+ "TLS_ERROR",
278
+ "CORS_ERROR",
279
+ }
280
+ return issue.code in availability_codes
281
+
282
+ def _has_issue_code(
283
+ self,
284
+ issues: List[ValidationIssue],
285
+ codes: str | List[str]
286
+ ) -> bool:
287
+ """Check if any issue has given code(s).
288
+
289
+ Args:
290
+ issues: List of validation issues
291
+ codes: Single code or list of codes to check
292
+
293
+ Returns:
294
+ True if any issue matches
295
+ """
296
+ if isinstance(codes, str):
297
+ codes = [codes]
298
+ code_set = set(codes)
299
+ return any(issue.code in code_set for issue in issues)
@@ -0,0 +1,314 @@
1
+ """Compliance scorer for A2A protocol specification adherence.
2
+
3
+ Calculates compliance score (0-100) based on:
4
+ - Core required fields (60 points)
5
+ - Skills quality (20 points)
6
+ - Format compliance (15 points)
7
+ - Data quality (5 points)
8
+ """
9
+
10
+ from typing import Any, Dict, List
11
+ from ..types import ValidationIssue
12
+ from .types import (
13
+ ComplianceScore,
14
+ ComplianceBreakdown,
15
+ CoreFieldsBreakdown,
16
+ SkillsQualityBreakdown,
17
+ FormatComplianceBreakdown,
18
+ DataQualityBreakdown,
19
+ get_compliance_rating,
20
+ )
21
+
22
+
23
+ class ComplianceScorer:
24
+ """Calculates compliance scores for A2A protocol adherence."""
25
+
26
+ # Required fields per A2A specification
27
+ REQUIRED_FIELDS = [
28
+ "name",
29
+ "description",
30
+ "url",
31
+ "version",
32
+ "protocolVersion",
33
+ "preferredTransport",
34
+ "capabilities",
35
+ "provider",
36
+ "skills"
37
+ ]
38
+
39
+ POINTS_PER_CORE_FIELD = 60 / len(REQUIRED_FIELDS) # ~6.67 points each
40
+
41
+ def score_agent_card(
42
+ self,
43
+ card_data: Dict[str, Any],
44
+ issues: List[ValidationIssue]
45
+ ) -> ComplianceScore:
46
+ """Calculate compliance score for an agent card.
47
+
48
+ Args:
49
+ card_data: Agent card data dictionary
50
+ issues: List of validation issues found
51
+
52
+ Returns:
53
+ ComplianceScore with detailed breakdown
54
+ """
55
+ # Calculate each component
56
+ core_fields = self._score_core_fields(card_data)
57
+ skills_quality = self._score_skills_quality(card_data)
58
+ format_compliance = self._score_format_compliance(card_data, issues)
59
+ data_quality = self._score_data_quality(card_data, issues)
60
+
61
+ # Calculate total
62
+ total = (
63
+ core_fields.score +
64
+ skills_quality.score +
65
+ format_compliance.score +
66
+ data_quality.score
67
+ )
68
+ total = max(0, min(100, total)) # Clamp to 0-100
69
+
70
+ # Create breakdown
71
+ breakdown = ComplianceBreakdown(
72
+ core_fields=core_fields,
73
+ skills_quality=skills_quality,
74
+ format_compliance=format_compliance,
75
+ data_quality=data_quality
76
+ )
77
+
78
+ # Extract issue messages
79
+ issue_messages = [
80
+ issue.message for issue in issues
81
+ if self._is_compliance_issue(issue)
82
+ ]
83
+
84
+ return ComplianceScore(
85
+ total=total,
86
+ rating=get_compliance_rating(total),
87
+ breakdown=breakdown,
88
+ issues=issue_messages
89
+ )
90
+
91
+ def _score_core_fields(self, card_data: Dict[str, Any]) -> CoreFieldsBreakdown:
92
+ """Score core required fields (60 points).
93
+
94
+ Args:
95
+ card_data: Agent card data
96
+
97
+ Returns:
98
+ CoreFieldsBreakdown with present/missing fields
99
+ """
100
+ present = []
101
+ missing = []
102
+
103
+ for field in self.REQUIRED_FIELDS:
104
+ if field in card_data and card_data[field]:
105
+ present.append(field)
106
+ else:
107
+ missing.append(field)
108
+
109
+ score = int(len(present) * self.POINTS_PER_CORE_FIELD)
110
+
111
+ return CoreFieldsBreakdown(
112
+ score=score,
113
+ max_score=60,
114
+ present=present,
115
+ missing=missing
116
+ )
117
+
118
+ def _score_skills_quality(self, card_data: Dict[str, Any]) -> SkillsQualityBreakdown:
119
+ """Score skills quality (20 points).
120
+
121
+ Args:
122
+ card_data: Agent card data
123
+
124
+ Returns:
125
+ SkillsQualityBreakdown with quality metrics
126
+ """
127
+ skills = card_data.get("skills", [])
128
+ score = 0
129
+ issue_count = 0
130
+
131
+ # Ensure skills is a list (defensive coding for validation errors)
132
+ if not isinstance(skills, list):
133
+ skills = []
134
+
135
+ skills_present = bool(skills)
136
+ all_have_required = True
137
+ all_have_tags = True
138
+
139
+ if skills_present:
140
+ score += 5 # Skills array present
141
+
142
+ # Check all skills have required fields
143
+ required_skill_fields = ["id", "name", "description"]
144
+ for skill in skills:
145
+ # Defensive: ensure skill is a dict
146
+ if not isinstance(skill, dict):
147
+ all_have_required = False
148
+ issue_count += 1
149
+ continue
150
+
151
+ if not all(field in skill for field in required_skill_fields):
152
+ all_have_required = False
153
+ issue_count += 1
154
+
155
+ if all_have_required:
156
+ score += 10
157
+
158
+ # Check all skills have tags
159
+ for skill in skills:
160
+ # Defensive: ensure skill is a dict
161
+ if not isinstance(skill, dict):
162
+ all_have_tags = False
163
+ issue_count += 1
164
+ continue
165
+
166
+ if not skill.get("tags"):
167
+ all_have_tags = False
168
+ issue_count += 1
169
+
170
+ if all_have_tags:
171
+ score += 5
172
+ else:
173
+ all_have_required = False
174
+ all_have_tags = False
175
+
176
+ return SkillsQualityBreakdown(
177
+ score=score,
178
+ max_score=20,
179
+ skills_present=skills_present,
180
+ all_skills_have_required_fields=all_have_required,
181
+ all_skills_have_tags=all_have_tags,
182
+ issue_count=issue_count
183
+ )
184
+
185
+ def _score_format_compliance(
186
+ self,
187
+ card_data: Dict[str, Any],
188
+ issues: List[ValidationIssue]
189
+ ) -> FormatComplianceBreakdown:
190
+ """Score format compliance (15 points).
191
+
192
+ Args:
193
+ card_data: Agent card data
194
+ issues: Validation issues to check
195
+
196
+ Returns:
197
+ FormatComplianceBreakdown with format checks
198
+ """
199
+ score = 0
200
+
201
+ # Check for format-related issues (3 points each)
202
+ valid_semver = not self._has_issue_code(issues, "INVALID_SEMVER")
203
+ if valid_semver:
204
+ score += 3
205
+
206
+ valid_protocol_version = not self._has_issue_code(issues, "INVALID_PROTOCOL_VERSION")
207
+ if valid_protocol_version:
208
+ score += 3
209
+
210
+ valid_url = not self._has_issue_code(issues, ["INVALID_URL", "INSECURE_URL"])
211
+ if valid_url:
212
+ score += 3
213
+
214
+ valid_transports = not self._has_issue_code(issues, "INVALID_TRANSPORT")
215
+ if valid_transports:
216
+ score += 3
217
+
218
+ valid_mime_types = not self._has_issue_code(issues, "INVALID_MIME_TYPE")
219
+ if valid_mime_types:
220
+ score += 3
221
+
222
+ return FormatComplianceBreakdown(
223
+ score=score,
224
+ max_score=15,
225
+ valid_semver=valid_semver,
226
+ valid_protocol_version=valid_protocol_version,
227
+ valid_url=valid_url,
228
+ valid_transports=valid_transports,
229
+ valid_mime_types=valid_mime_types
230
+ )
231
+
232
+ def _score_data_quality(
233
+ self,
234
+ card_data: Dict[str, Any],
235
+ issues: List[ValidationIssue]
236
+ ) -> DataQualityBreakdown:
237
+ """Score data quality (5 points).
238
+
239
+ Args:
240
+ card_data: Agent card data
241
+ issues: Validation issues to check
242
+
243
+ Returns:
244
+ DataQualityBreakdown with quality checks
245
+ """
246
+ score = 0
247
+
248
+ # Check for duplicate skill IDs (2 points)
249
+ skills = card_data.get("skills", [])
250
+ # Ensure skills is a list (defensive coding)
251
+ if not isinstance(skills, list):
252
+ skills = []
253
+ # Ensure each skill is a dict
254
+ skill_ids = [s.get("id") for s in skills if isinstance(s, dict) and s.get("id")]
255
+ no_duplicates = len(skill_ids) == len(set(skill_ids))
256
+ if no_duplicates:
257
+ score += 2
258
+
259
+ # Check field lengths (2 points)
260
+ field_lengths_valid = not self._has_issue_code(issues, "FIELD_LENGTH_EXCEEDED")
261
+ if field_lengths_valid:
262
+ score += 2
263
+
264
+ # Check for SSRF risks (1 point)
265
+ no_ssrf_risks = not self._has_issue_code(issues, ["SSRF_RISK", "PRIVATE_IP"])
266
+ if no_ssrf_risks:
267
+ score += 1
268
+
269
+ return DataQualityBreakdown(
270
+ score=score,
271
+ max_score=5,
272
+ no_duplicate_skill_ids=no_duplicates,
273
+ field_lengths_valid=field_lengths_valid,
274
+ no_ssrf_risks=no_ssrf_risks
275
+ )
276
+
277
+ def _is_compliance_issue(self, issue: ValidationIssue) -> bool:
278
+ """Check if issue is compliance-related.
279
+
280
+ Args:
281
+ issue: Validation issue to check
282
+
283
+ Returns:
284
+ True if compliance-related
285
+ """
286
+ compliance_codes = {
287
+ "MISSING_REQUIRED_FIELD",
288
+ "INVALID_SEMVER",
289
+ "INVALID_PROTOCOL_VERSION",
290
+ "INVALID_TRANSPORT",
291
+ "INVALID_MIME_TYPE",
292
+ "FIELD_LENGTH_EXCEEDED",
293
+ "DUPLICATE_SKILL_ID",
294
+ }
295
+ return issue.code in compliance_codes
296
+
297
+ def _has_issue_code(
298
+ self,
299
+ issues: List[ValidationIssue],
300
+ codes: str | List[str]
301
+ ) -> bool:
302
+ """Check if any issue has given code(s).
303
+
304
+ Args:
305
+ issues: List of validation issues
306
+ codes: Single code or list of codes to check
307
+
308
+ Returns:
309
+ True if any issue matches
310
+ """
311
+ if isinstance(codes, str):
312
+ codes = [codes]
313
+ code_set = set(codes)
314
+ return any(issue.code in code_set for issue in issues)