kailash 0.1.4__py3-none-any.whl → 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.
Files changed (83) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control.py +740 -0
  3. kailash/api/__main__.py +6 -0
  4. kailash/api/auth.py +668 -0
  5. kailash/api/custom_nodes.py +285 -0
  6. kailash/api/custom_nodes_secure.py +377 -0
  7. kailash/api/database.py +620 -0
  8. kailash/api/studio.py +915 -0
  9. kailash/api/studio_secure.py +893 -0
  10. kailash/mcp/__init__.py +53 -0
  11. kailash/mcp/__main__.py +13 -0
  12. kailash/mcp/ai_registry_server.py +712 -0
  13. kailash/mcp/client.py +447 -0
  14. kailash/mcp/client_new.py +334 -0
  15. kailash/mcp/server.py +293 -0
  16. kailash/mcp/server_new.py +336 -0
  17. kailash/mcp/servers/__init__.py +12 -0
  18. kailash/mcp/servers/ai_registry.py +289 -0
  19. kailash/nodes/__init__.py +4 -2
  20. kailash/nodes/ai/__init__.py +38 -0
  21. kailash/nodes/ai/a2a.py +1790 -0
  22. kailash/nodes/ai/agents.py +116 -2
  23. kailash/nodes/ai/ai_providers.py +206 -8
  24. kailash/nodes/ai/intelligent_agent_orchestrator.py +2108 -0
  25. kailash/nodes/ai/iterative_llm_agent.py +1280 -0
  26. kailash/nodes/ai/llm_agent.py +324 -1
  27. kailash/nodes/ai/self_organizing.py +1623 -0
  28. kailash/nodes/api/http.py +106 -25
  29. kailash/nodes/api/rest.py +116 -21
  30. kailash/nodes/base.py +15 -2
  31. kailash/nodes/base_async.py +45 -0
  32. kailash/nodes/base_cycle_aware.py +374 -0
  33. kailash/nodes/base_with_acl.py +338 -0
  34. kailash/nodes/code/python.py +135 -27
  35. kailash/nodes/data/readers.py +116 -53
  36. kailash/nodes/data/writers.py +16 -6
  37. kailash/nodes/logic/__init__.py +8 -0
  38. kailash/nodes/logic/async_operations.py +48 -9
  39. kailash/nodes/logic/convergence.py +642 -0
  40. kailash/nodes/logic/loop.py +153 -0
  41. kailash/nodes/logic/operations.py +212 -27
  42. kailash/nodes/logic/workflow.py +26 -18
  43. kailash/nodes/mixins/__init__.py +11 -0
  44. kailash/nodes/mixins/mcp.py +228 -0
  45. kailash/nodes/mixins.py +387 -0
  46. kailash/nodes/transform/__init__.py +8 -1
  47. kailash/nodes/transform/processors.py +119 -4
  48. kailash/runtime/__init__.py +2 -1
  49. kailash/runtime/access_controlled.py +458 -0
  50. kailash/runtime/local.py +106 -33
  51. kailash/runtime/parallel_cyclic.py +529 -0
  52. kailash/sdk_exceptions.py +90 -5
  53. kailash/security.py +845 -0
  54. kailash/tracking/manager.py +38 -15
  55. kailash/tracking/models.py +1 -1
  56. kailash/tracking/storage/filesystem.py +30 -2
  57. kailash/utils/__init__.py +8 -0
  58. kailash/workflow/__init__.py +18 -0
  59. kailash/workflow/convergence.py +270 -0
  60. kailash/workflow/cycle_analyzer.py +768 -0
  61. kailash/workflow/cycle_builder.py +573 -0
  62. kailash/workflow/cycle_config.py +709 -0
  63. kailash/workflow/cycle_debugger.py +760 -0
  64. kailash/workflow/cycle_exceptions.py +601 -0
  65. kailash/workflow/cycle_profiler.py +671 -0
  66. kailash/workflow/cycle_state.py +338 -0
  67. kailash/workflow/cyclic_runner.py +985 -0
  68. kailash/workflow/graph.py +500 -39
  69. kailash/workflow/migration.py +768 -0
  70. kailash/workflow/safety.py +365 -0
  71. kailash/workflow/templates.py +744 -0
  72. kailash/workflow/validation.py +693 -0
  73. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/METADATA +446 -13
  74. kailash-0.2.0.dist-info/RECORD +125 -0
  75. kailash/nodes/mcp/__init__.py +0 -11
  76. kailash/nodes/mcp/client.py +0 -554
  77. kailash/nodes/mcp/resource.py +0 -682
  78. kailash/nodes/mcp/server.py +0 -577
  79. kailash-0.1.4.dist-info/RECORD +0 -85
  80. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
  81. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
  82. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
  83. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -1,682 +0,0 @@
1
- """MCP Resource node for managing shared resources in Model Context Protocol."""
2
-
3
- import json
4
- from typing import Any, Dict, Optional
5
-
6
- from kailash.nodes.base import Node, NodeParameter, register_node
7
-
8
-
9
- @register_node()
10
- class MCPResource(Node):
11
- """
12
- Resource node for creating and managing Model Context Protocol (MCP) resources.
13
-
14
- Design Purpose and Philosophy:
15
- The MCPResource node provides a standardized way to create, update, and manage
16
- resources that can be shared through the MCP protocol. It abstracts resource
17
- lifecycle management while ensuring compatibility with MCP standards.
18
-
19
- Upstream Dependencies:
20
- - Source data to convert into MCP resources
21
- - Resource metadata and schema definitions
22
- - Authentication and access control settings
23
- - Content transformation and validation rules
24
-
25
- Downstream Consumers:
26
- - MCPServer nodes that host and expose resources
27
- - MCPClient nodes that consume resource data
28
- - AI models that need structured context data
29
- - External MCP-compatible applications and tools
30
-
31
- Usage Patterns:
32
- 1. Create resources from workflow data with proper metadata
33
- 2. Update existing resources with new content versions
34
- 3. Validate resource schemas and content formats
35
- 4. Transform data into MCP-compatible resource structures
36
- 5. Manage resource access permissions and visibility
37
-
38
- Implementation Details:
39
- - Supports all standard MCP resource types and formats
40
- - Implements proper resource URI schemes and namespacing
41
- - Provides content validation and schema enforcement
42
- - Handles resource versioning and update notifications
43
- - Manages metadata and discovery information efficiently
44
-
45
- Error Handling:
46
- - ResourceValidationError: When resource content fails validation
47
- - SchemaViolationError: When resource doesn't match expected schema
48
- - URIFormatError: When resource URI is malformed or invalid
49
- - ContentTypeError: When resource content type is unsupported
50
- - PermissionError: When access control rules are violated
51
-
52
- Side Effects:
53
- - Creates or updates resource entries in MCP registries
54
- - May cache resource content for improved performance
55
- - Logs resource access and modification events
56
- - Notifies subscribers about resource changes
57
-
58
- Examples:
59
- ```python
60
- # Create a simple text resource
61
- resource = MCPResource()
62
- result = resource.run(
63
- operation="create",
64
- uri="workflow://examples/data/customer_analysis.txt",
65
- content="Customer analysis results from Q4 2024...",
66
- metadata={
67
- "name": "Q4 Customer Analysis",
68
- "description": "Quarterly customer behavior analysis",
69
- "mimeType": "text/plain",
70
- "tags": ["analysis", "customers", "Q4"]
71
- }
72
- )
73
-
74
- # Create a structured data resource
75
- json_resource = MCPResource()
76
- result = json_resource.run(
77
- operation="create",
78
- uri="data://reports/summary.json",
79
- content={
80
- "total_customers": 15420,
81
- "revenue": 2450000,
82
- "top_products": ["Product A", "Product B"]
83
- },
84
- metadata={
85
- "name": "Summary Report",
86
- "mimeType": "application/json",
87
- "schema": "report_summary_v1"
88
- }
89
- )
90
-
91
- # Update an existing resource
92
- updated = MCPResource()
93
- result = updated.run(
94
- operation="update",
95
- uri="workflow://examples/data/customer_analysis.txt",
96
- content="Updated customer analysis with new data...",
97
- version="2.0"
98
- )
99
- ```
100
- """
101
-
102
- def get_parameters(self) -> Dict[str, NodeParameter]:
103
- return {
104
- "operation": NodeParameter(
105
- name="operation",
106
- type=str,
107
- required=False,
108
- default="create",
109
- description="Operation to perform: create, update, delete, validate, list",
110
- ),
111
- "uri": NodeParameter(
112
- name="uri",
113
- type=str,
114
- required=False,
115
- description="Resource URI (required for create, update, delete, validate)",
116
- ),
117
- "content": NodeParameter(
118
- name="content",
119
- type=object,
120
- required=False,
121
- description="Resource content (required for create, update)",
122
- ),
123
- "metadata": NodeParameter(
124
- name="metadata",
125
- type=dict,
126
- required=False,
127
- default={},
128
- description="Resource metadata (name, description, mimeType, etc.)",
129
- ),
130
- "schema": NodeParameter(
131
- name="schema",
132
- type=dict,
133
- required=False,
134
- description="JSON schema for content validation",
135
- ),
136
- "version": NodeParameter(
137
- name="version",
138
- type=str,
139
- required=False,
140
- description="Resource version (auto-generated if not provided)",
141
- ),
142
- "access_control": NodeParameter(
143
- name="access_control",
144
- type=dict,
145
- required=False,
146
- default={},
147
- description="Access control settings (permissions, visibility)",
148
- ),
149
- "cache_ttl": NodeParameter(
150
- name="cache_ttl",
151
- type=int,
152
- required=False,
153
- default=3600,
154
- description="Cache time-to-live in seconds",
155
- ),
156
- "auto_notify": NodeParameter(
157
- name="auto_notify",
158
- type=bool,
159
- required=False,
160
- default=True,
161
- description="Whether to notify subscribers of resource changes",
162
- ),
163
- }
164
-
165
- def run(self, **kwargs) -> Dict[str, Any]:
166
- operation = kwargs["operation"]
167
- uri = kwargs.get("uri")
168
- content = kwargs.get("content")
169
- metadata = kwargs.get("metadata", {})
170
- schema = kwargs.get("schema")
171
- version = kwargs.get("version")
172
- access_control = kwargs.get("access_control", {})
173
- cache_ttl = kwargs.get("cache_ttl", 3600)
174
- auto_notify = kwargs.get("auto_notify", True)
175
-
176
- try:
177
- if operation == "create":
178
- return self._create_resource(
179
- uri,
180
- content,
181
- metadata,
182
- schema,
183
- version,
184
- access_control,
185
- cache_ttl,
186
- auto_notify,
187
- )
188
- elif operation == "update":
189
- return self._update_resource(
190
- uri,
191
- content,
192
- metadata,
193
- schema,
194
- version,
195
- access_control,
196
- cache_ttl,
197
- auto_notify,
198
- )
199
- elif operation == "delete":
200
- return self._delete_resource(uri, auto_notify)
201
- elif operation == "validate":
202
- return self._validate_resource(uri, content, schema)
203
- elif operation == "list":
204
- return self._list_resources(metadata.get("filter"))
205
- else:
206
- return {
207
- "success": False,
208
- "error": f"Unsupported operation: {operation}",
209
- "supported_operations": [
210
- "create",
211
- "update",
212
- "delete",
213
- "validate",
214
- "list",
215
- ],
216
- }
217
-
218
- except Exception as e:
219
- return {
220
- "success": False,
221
- "error": str(e),
222
- "error_type": type(e).__name__,
223
- "operation": operation,
224
- "uri": uri,
225
- }
226
-
227
- def _create_resource(
228
- self,
229
- uri: Optional[str],
230
- content: Any,
231
- metadata: dict,
232
- schema: Optional[dict],
233
- version: Optional[str],
234
- access_control: dict,
235
- cache_ttl: int,
236
- auto_notify: bool,
237
- ) -> Dict[str, Any]:
238
- """Create a new MCP resource."""
239
- if not uri:
240
- return {"success": False, "error": "URI is required for create operation"}
241
-
242
- if content is None:
243
- return {
244
- "success": False,
245
- "error": "Content is required for create operation",
246
- }
247
-
248
- # Validate URI format
249
- if not self._validate_uri(uri):
250
- return {
251
- "success": False,
252
- "error": f"Invalid URI format: {uri}",
253
- "uri_requirements": [
254
- "Must include a scheme (e.g., 'file://', 'data://', 'workflow://')",
255
- "Must not contain invalid characters",
256
- "Should follow MCP URI conventions",
257
- ],
258
- }
259
-
260
- # Determine content type and serialize if needed
261
- content_str, mime_type = self._serialize_content(content)
262
-
263
- # Use provided mime type or auto-detected one
264
- final_mime_type = metadata.get("mimeType", mime_type)
265
-
266
- # Validate content against schema if provided
267
- if schema:
268
- validation_result = self._validate_against_schema(content, schema)
269
- if not validation_result["valid"]:
270
- return {
271
- "success": False,
272
- "error": "Content validation failed",
273
- "validation_errors": validation_result["errors"],
274
- }
275
-
276
- # Generate version if not provided
277
- if not version:
278
- import datetime
279
-
280
- version = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
281
-
282
- # Create resource object
283
- resource = {
284
- "uri": uri,
285
- "name": metadata.get("name", self._extract_name_from_uri(uri)),
286
- "description": metadata.get("description", ""),
287
- "mimeType": final_mime_type,
288
- "content": content_str,
289
- "version": version,
290
- "created_at": self._current_timestamp(),
291
- "updated_at": self._current_timestamp(),
292
- "size": len(content_str) if isinstance(content_str, str) else 0,
293
- "metadata": {
294
- "tags": metadata.get("tags", []),
295
- "category": metadata.get("category", "general"),
296
- "source": metadata.get("source", "kailash-workflow"),
297
- "author": metadata.get("author", "system"),
298
- **{
299
- k: v
300
- for k, v in metadata.items()
301
- if k
302
- not in [
303
- "name",
304
- "description",
305
- "mimeType",
306
- "tags",
307
- "category",
308
- "source",
309
- "author",
310
- ]
311
- },
312
- },
313
- "access_control": {
314
- "visibility": access_control.get("visibility", "public"),
315
- "permissions": access_control.get("permissions", ["read"]),
316
- "allowed_clients": access_control.get("allowed_clients", []),
317
- **{
318
- k: v
319
- for k, v in access_control.items()
320
- if k not in ["visibility", "permissions", "allowed_clients"]
321
- },
322
- },
323
- "cache": {"ttl": cache_ttl, "cacheable": cache_ttl > 0},
324
- }
325
-
326
- # Mock resource storage (in real implementation, this would persist to storage)
327
- storage_id = f"res_{hash(uri) % 100000}"
328
- storage_result = {
329
- "stored": True,
330
- "storage_id": storage_id,
331
- "storage_location": f"mock://storage/resources/{storage_id}",
332
- }
333
-
334
- return {
335
- "success": True,
336
- "operation": "create",
337
- "resource": resource,
338
- "storage": storage_result,
339
- "notifications": {
340
- "sent": auto_notify,
341
- "subscribers_notified": 3 if auto_notify else 0,
342
- },
343
- "next_actions": [
344
- "Resource is now available for MCP clients",
345
- "Use MCPServer to expose this resource",
346
- "Monitor resource access patterns",
347
- ],
348
- }
349
-
350
- def _update_resource(
351
- self,
352
- uri: Optional[str],
353
- content: Any,
354
- metadata: dict,
355
- schema: Optional[dict],
356
- version: Optional[str],
357
- access_control: dict,
358
- cache_ttl: int,
359
- auto_notify: bool,
360
- ) -> Dict[str, Any]:
361
- """Update an existing MCP resource."""
362
- if not uri:
363
- return {"success": False, "error": "URI is required for update operation"}
364
-
365
- # Mock resource lookup
366
- existing_resource = {
367
- "uri": uri,
368
- "name": "Existing Resource",
369
- "version": "1.0",
370
- "created_at": "2025-06-01T10:00:00Z",
371
- "content": "Previous content...",
372
- "mimeType": "text/plain",
373
- }
374
-
375
- # Update fields
376
- updates = {}
377
-
378
- if content is not None:
379
- content_str, mime_type = self._serialize_content(content)
380
- updates["content"] = content_str
381
- updates["mimeType"] = metadata.get("mimeType", mime_type)
382
- updates["size"] = len(content_str) if isinstance(content_str, str) else 0
383
-
384
- # Validate new content against schema if provided
385
- if schema:
386
- validation_result = self._validate_against_schema(content, schema)
387
- if not validation_result["valid"]:
388
- return {
389
- "success": False,
390
- "error": "Content validation failed",
391
- "validation_errors": validation_result["errors"],
392
- }
393
-
394
- if metadata:
395
- for key in ["name", "description", "tags", "category"]:
396
- if key in metadata:
397
- updates[key] = metadata[key]
398
-
399
- if version:
400
- updates["version"] = version
401
- else:
402
- # Auto-increment version
403
- old_version = existing_resource["version"]
404
- try:
405
- version_num = float(old_version) + 0.1
406
- updates["version"] = f"{version_num:.1f}"
407
- except (ValueError, TypeError):
408
- import datetime
409
-
410
- updates["version"] = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
411
-
412
- updates["updated_at"] = self._current_timestamp()
413
-
414
- # Apply access control updates
415
- if access_control:
416
- updates["access_control"] = {
417
- **existing_resource.get("access_control", {}),
418
- **access_control,
419
- }
420
-
421
- # Create updated resource
422
- updated_resource = {**existing_resource, **updates}
423
-
424
- return {
425
- "success": True,
426
- "operation": "update",
427
- "resource": updated_resource,
428
- "changes": updates,
429
- "previous_version": existing_resource["version"],
430
- "notifications": {
431
- "sent": auto_notify,
432
- "subscribers_notified": 5 if auto_notify else 0,
433
- "change_summary": f"Updated {len(updates)} fields",
434
- },
435
- }
436
-
437
- def _delete_resource(self, uri: Optional[str], auto_notify: bool) -> Dict[str, Any]:
438
- """Delete an MCP resource."""
439
- if not uri:
440
- return {"success": False, "error": "URI is required for delete operation"}
441
-
442
- # Mock resource deletion
443
- deleted_resource = {
444
- "uri": uri,
445
- "name": "Deleted Resource",
446
- "deleted_at": self._current_timestamp(),
447
- "final_version": "2.1",
448
- }
449
-
450
- return {
451
- "success": True,
452
- "operation": "delete",
453
- "deleted_resource": deleted_resource,
454
- "cleanup": {
455
- "cache_cleared": True,
456
- "references_updated": 3,
457
- "storage_freed": "1.2 MB",
458
- },
459
- "notifications": {
460
- "sent": auto_notify,
461
- "subscribers_notified": 7 if auto_notify else 0,
462
- },
463
- }
464
-
465
- def _validate_resource(
466
- self, uri: Optional[str], content: Any, schema: Optional[dict]
467
- ) -> Dict[str, Any]:
468
- """Validate a resource without creating or updating it."""
469
- validation_results = {
470
- "uri_valid": True,
471
- "content_valid": True,
472
- "schema_valid": True,
473
- "errors": [],
474
- "warnings": [],
475
- }
476
-
477
- # Validate URI
478
- if uri:
479
- if not self._validate_uri(uri):
480
- validation_results["uri_valid"] = False
481
- validation_results["errors"].append(f"Invalid URI format: {uri}")
482
- else:
483
- validation_results["errors"].append("URI is required for validation")
484
-
485
- # Validate content
486
- if content is not None:
487
- try:
488
- content_str, mime_type = self._serialize_content(content)
489
- if not content_str:
490
- validation_results["warnings"].append("Content is empty")
491
- except Exception as e:
492
- validation_results["content_valid"] = False
493
- validation_results["errors"].append(
494
- f"Content serialization failed: {e}"
495
- )
496
-
497
- # Validate against schema
498
- if schema:
499
- schema_validation = self._validate_against_schema(content, schema)
500
- if not schema_validation["valid"]:
501
- validation_results["schema_valid"] = False
502
- validation_results["errors"].extend(schema_validation["errors"])
503
- else:
504
- validation_results["warnings"].append("No content provided for validation")
505
-
506
- overall_valid = (
507
- validation_results["uri_valid"]
508
- and validation_results["content_valid"]
509
- and validation_results["schema_valid"]
510
- )
511
-
512
- return {
513
- "success": True,
514
- "operation": "validate",
515
- "valid": overall_valid,
516
- "results": validation_results,
517
- "summary": {
518
- "total_errors": len(validation_results["errors"]),
519
- "total_warnings": len(validation_results["warnings"]),
520
- "recommendation": (
521
- "Resource is valid"
522
- if overall_valid
523
- else "Fix errors before creating resource"
524
- ),
525
- },
526
- }
527
-
528
- def _list_resources(self, filter_criteria: Optional[dict] = None) -> Dict[str, Any]:
529
- """List available MCP resources."""
530
- # Mock resource listing
531
- mock_resources = [
532
- {
533
- "uri": "workflow://examples/data/customer_analysis.txt",
534
- "name": "Customer Analysis",
535
- "mimeType": "text/plain",
536
- "size": 1024,
537
- "created_at": "2025-06-01T10:00:00Z",
538
- "version": "1.0",
539
- },
540
- {
541
- "uri": "data://reports/summary.json",
542
- "name": "Summary Report",
543
- "mimeType": "application/json",
544
- "size": 512,
545
- "created_at": "2025-06-01T11:00:00Z",
546
- "version": "2.1",
547
- },
548
- {
549
- "uri": "file:///tmp/output.csv",
550
- "name": "Output Data",
551
- "mimeType": "text/csv",
552
- "size": 2048,
553
- "created_at": "2025-06-01T12:00:00Z",
554
- "version": "3.0",
555
- },
556
- ]
557
-
558
- # Apply filters if provided
559
- if filter_criteria:
560
- filtered_resources = []
561
- for resource in mock_resources:
562
- include = True
563
-
564
- if "mimeType" in filter_criteria:
565
- if resource["mimeType"] != filter_criteria["mimeType"]:
566
- include = False
567
-
568
- if "min_size" in filter_criteria:
569
- if resource["size"] < filter_criteria["min_size"]:
570
- include = False
571
-
572
- if "name_contains" in filter_criteria:
573
- if (
574
- filter_criteria["name_contains"].lower()
575
- not in resource["name"].lower()
576
- ):
577
- include = False
578
-
579
- if include:
580
- filtered_resources.append(resource)
581
-
582
- mock_resources = filtered_resources
583
-
584
- return {
585
- "success": True,
586
- "operation": "list",
587
- "resources": mock_resources,
588
- "total_count": len(mock_resources),
589
- "filter_applied": filter_criteria is not None,
590
- "filter_criteria": filter_criteria,
591
- "summary": {
592
- "mime_types": list(set(r["mimeType"] for r in mock_resources)),
593
- "total_size": sum(r["size"] for r in mock_resources),
594
- "version_range": (
595
- f"{min(r['version'] for r in mock_resources)} - {max(r['version'] for r in mock_resources)}"
596
- if mock_resources
597
- else "N/A"
598
- ),
599
- },
600
- }
601
-
602
- def _validate_uri(self, uri: str) -> bool:
603
- """Validate MCP resource URI format."""
604
- if not uri:
605
- return False
606
-
607
- # Check for scheme
608
- if "://" not in uri:
609
- return False
610
-
611
- # Check for valid characters (basic validation)
612
- invalid_chars = [" ", "\t", "\n", "\r"]
613
- for char in invalid_chars:
614
- if char in uri:
615
- return False
616
-
617
- return True
618
-
619
- def _serialize_content(self, content: Any) -> tuple[str, str]:
620
- """Serialize content and determine MIME type."""
621
- if isinstance(content, str):
622
- return content, "text/plain"
623
- elif isinstance(content, (dict, list)):
624
- return json.dumps(content, indent=2), "application/json"
625
- elif isinstance(content, bytes):
626
- try:
627
- return content.decode("utf-8"), "text/plain"
628
- except UnicodeDecodeError:
629
- return str(content), "application/octet-stream"
630
- else:
631
- return str(content), "text/plain"
632
-
633
- def _validate_against_schema(self, content: Any, schema: dict) -> Dict[str, Any]:
634
- """Validate content against JSON schema."""
635
- try:
636
- # Mock schema validation (in real implementation, use jsonschema library)
637
- if not isinstance(schema, dict):
638
- return {"valid": False, "errors": ["Schema must be a dictionary"]}
639
-
640
- # Basic type checking
641
- schema_type = schema.get("type")
642
- if schema_type:
643
- if schema_type == "object" and not isinstance(content, dict):
644
- return {
645
- "valid": False,
646
- "errors": [f"Expected object, got {type(content).__name__}"],
647
- }
648
- elif schema_type == "array" and not isinstance(content, list):
649
- return {
650
- "valid": False,
651
- "errors": [f"Expected array, got {type(content).__name__}"],
652
- }
653
- elif schema_type == "string" and not isinstance(content, str):
654
- return {
655
- "valid": False,
656
- "errors": [f"Expected string, got {type(content).__name__}"],
657
- }
658
-
659
- return {"valid": True, "errors": []}
660
-
661
- except Exception as e:
662
- return {"valid": False, "errors": [f"Schema validation error: {e}"]}
663
-
664
- def _extract_name_from_uri(self, uri: str) -> str:
665
- """Extract a reasonable name from URI."""
666
- # Extract the last part of the URI path
667
- if "/" in uri:
668
- name = uri.split("/")[-1]
669
- else:
670
- name = uri
671
-
672
- # Remove file extension for display
673
- if "." in name:
674
- name = name.rsplit(".", 1)[0]
675
-
676
- return name or "Unnamed Resource"
677
-
678
- def _current_timestamp(self) -> str:
679
- """Get current timestamp in ISO format."""
680
- import datetime
681
-
682
- return datetime.datetime.now().isoformat() + "Z"