largestack 1.0.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.
- largestack/__init__.py +128 -0
- largestack/_a2a/__init__.py +548 -0
- largestack/_a2a/multimodal.py +273 -0
- largestack/_a2a/v03.py +442 -0
- largestack/_cli/cli_v09.py +393 -0
- largestack/_cli/cli_v120.py +328 -0
- largestack/_cli/cli_v130_compliance.py +504 -0
- largestack/_cli/commands.py +117 -0
- largestack/_cli/dev_server.py +346 -0
- largestack/_cli/main.py +1016 -0
- largestack/_cli/scaffold.py +1265 -0
- largestack/_compliance/dpdp_breach.py +472 -0
- largestack/_core/__init__.py +0 -0
- largestack/_core/a2a_server.py +73 -0
- largestack/_core/a2a_v1.py +252 -0
- largestack/_core/ag_ui.py +190 -0
- largestack/_core/agent_roles.py +241 -0
- largestack/_core/agui_v1.py +292 -0
- largestack/_core/browser_tool.py +94 -0
- largestack/_core/budget.py +254 -0
- largestack/_core/builtin_tools/__init__.py +11 -0
- largestack/_core/builtin_tools/_url_validator.py +95 -0
- largestack/_core/builtin_tools/browser.py +56 -0
- largestack/_core/builtin_tools/calc.py +128 -0
- largestack/_core/builtin_tools/code.py +183 -0
- largestack/_core/builtin_tools/db.py +147 -0
- largestack/_core/builtin_tools/files.py +35 -0
- largestack/_core/builtin_tools/http_tool.py +81 -0
- largestack/_core/builtin_tools/shell.py +122 -0
- largestack/_core/builtin_tools/time_tool.py +9 -0
- largestack/_core/builtin_tools/voice.py +43 -0
- largestack/_core/builtin_tools/web.py +103 -0
- largestack/_core/circuit_breaker.py +74 -0
- largestack/_core/citation_sandbox.py +341 -0
- largestack/_core/code_agent.py +147 -0
- largestack/_core/code_agent_v11.py +256 -0
- largestack/_core/composio_adapter.py +60 -0
- largestack/_core/config.py +102 -0
- largestack/_core/context.py +49 -0
- largestack/_core/cost.py +66 -0
- largestack/_core/database.py +334 -0
- largestack/_core/e2b_sandbox.py +175 -0
- largestack/_core/engine.py +565 -0
- largestack/_core/events.py +33 -0
- largestack/_core/feature_flags.py +42 -0
- largestack/_core/gateway.py +342 -0
- largestack/_core/health.py +95 -0
- largestack/_core/hitl.py +74 -0
- largestack/_core/license.py +244 -0
- largestack/_core/litellm_router.py +143 -0
- largestack/_core/loop_guard.py +62 -0
- largestack/_core/mcp_client.py +138 -0
- largestack/_core/mcp_server.py +99 -0
- largestack/_core/mcp_streamable.py +289 -0
- largestack/_core/multiagent.py +468 -0
- largestack/_core/optimizer.py +109 -0
- largestack/_core/parsers.py +288 -0
- largestack/_core/plugin_host.py +58 -0
- largestack/_core/prompt_templates.py +261 -0
- largestack/_core/providers/__init__.py +28 -0
- largestack/_core/providers/ai21_prov.py +17 -0
- largestack/_core/providers/anthropic_prov.py +96 -0
- largestack/_core/providers/anyscale_prov.py +17 -0
- largestack/_core/providers/azure_prov.py +30 -0
- largestack/_core/providers/base.py +25 -0
- largestack/_core/providers/bedrock_prov.py +104 -0
- largestack/_core/providers/cerebras_prov.py +19 -0
- largestack/_core/providers/cloudflare_prov.py +23 -0
- largestack/_core/providers/cohere_prov.py +75 -0
- largestack/_core/providers/databricks_prov.py +20 -0
- largestack/_core/providers/deepseek_prov.py +13 -0
- largestack/_core/providers/fireworks_prov.py +22 -0
- largestack/_core/providers/google_prov.py +104 -0
- largestack/_core/providers/groq_prov.py +29 -0
- largestack/_core/providers/lepton_prov.py +20 -0
- largestack/_core/providers/litellm_prov.py +227 -0
- largestack/_core/providers/mistral_prov.py +8 -0
- largestack/_core/providers/nvidia_prov.py +22 -0
- largestack/_core/providers/ollama_prov.py +76 -0
- largestack/_core/providers/openai_prov.py +144 -0
- largestack/_core/providers/openrouter_prov.py +30 -0
- largestack/_core/providers/perplexity_prov.py +25 -0
- largestack/_core/providers/replicate_prov.py +19 -0
- largestack/_core/providers/sambanova_prov.py +19 -0
- largestack/_core/providers/together_prov.py +30 -0
- largestack/_core/providers/voyage_prov.py +29 -0
- largestack/_core/providers/xai_prov.py +24 -0
- largestack/_core/reasoning.py +358 -0
- largestack/_core/registry.py +52 -0
- largestack/_core/resilience.py +306 -0
- largestack/_core/semantic_cache.py +90 -0
- largestack/_core/session.py +255 -0
- largestack/_core/smart_router.py +81 -0
- largestack/_core/steering.py +31 -0
- largestack/_core/streaming.py +34 -0
- largestack/_core/structured.py +139 -0
- largestack/_core/structured_output.py +190 -0
- largestack/_core/tools.py +335 -0
- largestack/_core/typed_agent.py +282 -0
- largestack/_core/versioning.py +76 -0
- largestack/_core/vision.py +38 -0
- largestack/_core/voice_agent.py +57 -0
- largestack/_core/ws_stream.py +62 -0
- largestack/_core/yaml_agent.py +133 -0
- largestack/_core/yaml_schema.py +275 -0
- largestack/_dashboard/README.md +64 -0
- largestack/_dashboard/__init__.py +0 -0
- largestack/_dashboard/api.py +185 -0
- largestack/_dashboard/app.py +576 -0
- largestack/_dashboard/auth.py +60 -0
- largestack/_dashboard/frontend.jsx +446 -0
- largestack/_dashboard/rate_limit.py +250 -0
- largestack/_dashboard/spa/App.jsx +446 -0
- largestack/_dashboard/spa/README.md +59 -0
- largestack/_dashboard/spa/index.html +17 -0
- largestack/_dashboard/spa/main.jsx +5 -0
- largestack/_distributed/__init__.py +3 -0
- largestack/_distributed/event_sourcing.py +343 -0
- largestack/_distributed/outbox.py +248 -0
- largestack/_distributed/saga.py +270 -0
- largestack/_enterprise/__init__.py +7 -0
- largestack/_enterprise/audit.py +217 -0
- largestack/_enterprise/billing.py +198 -0
- largestack/_enterprise/canary.py +223 -0
- largestack/_enterprise/payment.py +347 -0
- largestack/_enterprise/rbac.py +430 -0
- largestack/_enterprise/session_store.py +281 -0
- largestack/_enterprise/sso.py +362 -0
- largestack/_enterprise/tenant.py +139 -0
- largestack/_enterprise/white_label.py +27 -0
- largestack/_eval/__init__.py +0 -0
- largestack/_eval/alerts.py +350 -0
- largestack/_eval/extensions_v130.py +229 -0
- largestack/_eval/pr_diff.py +324 -0
- largestack/_eval/runner.py +343 -0
- largestack/_evals/__init__.py +1 -0
- largestack/_evals/adapters.py +80 -0
- largestack/_evals/runner.py +55 -0
- largestack/_guard/__init__.py +11 -0
- largestack/_guard/agent_identity.py +73 -0
- largestack/_guard/config.py +83 -0
- largestack/_guard/hallucination.py +238 -0
- largestack/_guard/injection.py +308 -0
- largestack/_guard/inter_agent_auth.py +53 -0
- largestack/_guard/kill_switch.py +42 -0
- largestack/_guard/memory_integrity.py +67 -0
- largestack/_guard/nli_hallucination.py +98 -0
- largestack/_guard/pii.py +182 -0
- largestack/_guard/pii_ml.py +102 -0
- largestack/_guard/pipeline.py +89 -0
- largestack/_guard/policy.py +163 -0
- largestack/_guard/prompt_guard.py +66 -0
- largestack/_guard/provider_policy.py +52 -0
- largestack/_guard/redis_kill_switch.py +55 -0
- largestack/_guard/tool_access.py +86 -0
- largestack/_guard/tool_policy.py +126 -0
- largestack/_guard/topic.py +172 -0
- largestack/_guard/toxicity.py +186 -0
- largestack/_indic/__init__.py +341 -0
- largestack/_integrations/__init__.py +121 -0
- largestack/_integrations/cohere_embed.py +128 -0
- largestack/_integrations/embeddings_v09.py +456 -0
- largestack/_integrations/github.py +178 -0
- largestack/_integrations/hf_embed.py +143 -0
- largestack/_integrations/indian_toolkits.py +795 -0
- largestack/_integrations/jina_embed.py +137 -0
- largestack/_integrations/jira.py +129 -0
- largestack/_integrations/langchain_compat.py +227 -0
- largestack/_integrations/langfuse_adapter.py +347 -0
- largestack/_integrations/linear.py +137 -0
- largestack/_integrations/litellm_bridge.py +300 -0
- largestack/_integrations/mcp_adapter.py +116 -0
- largestack/_integrations/notion.py +144 -0
- largestack/_integrations/openai_embeddings.py +76 -0
- largestack/_integrations/openapi_toolkit.py +334 -0
- largestack/_integrations/pandas_toolkit.py +158 -0
- largestack/_integrations/phoenix_adapter.py +318 -0
- largestack/_integrations/postgres.py +96 -0
- largestack/_integrations/razorpay_toolkit.py +365 -0
- largestack/_integrations/registry.py +176 -0
- largestack/_integrations/sheets.py +178 -0
- largestack/_integrations/slack.py +90 -0
- largestack/_integrations/sql_toolkit.py +219 -0
- largestack/_integrations/stripe_toolkit.py +277 -0
- largestack/_integrations/toolkits_v09.py +466 -0
- largestack/_integrations/voyage_embed.py +135 -0
- largestack/_loaders/__init__.py +970 -0
- largestack/_loaders/llamaparse.py +246 -0
- largestack/_loaders/loaders_v09.py +696 -0
- largestack/_loaders/office.py +234 -0
- largestack/_loaders/semantic_chunking.py +251 -0
- largestack/_memory/__init__.py +8 -0
- largestack/_memory/buffer.py +117 -0
- largestack/_memory/compression.py +77 -0
- largestack/_memory/episodic.py +50 -0
- largestack/_memory/external_adapters.py +104 -0
- largestack/_memory/graph.py +289 -0
- largestack/_memory/long_term.py +853 -0
- largestack/_memory/observational.py +90 -0
- largestack/_memory/postgres_store.py +355 -0
- largestack/_memory/procedural.py +152 -0
- largestack/_memory/semantic.py +151 -0
- largestack/_memory/shared.py +38 -0
- largestack/_memory/tools.py +365 -0
- largestack/_memory/vector_store.py +423 -0
- largestack/_observability/__init__.py +0 -0
- largestack/_observability/otel.py +218 -0
- largestack/_observe/__init__.py +5 -0
- largestack/_observe/anomaly.py +65 -0
- largestack/_observe/auto_trace.py +55 -0
- largestack/_observe/cost_dashboard.py +34 -0
- largestack/_observe/event_replay.py +57 -0
- largestack/_observe/gen_ai_instrumentor.py +87 -0
- largestack/_observe/log_redaction.py +83 -0
- largestack/_observe/metrics.py +107 -0
- largestack/_observe/otel_export.py +235 -0
- largestack/_observe/otel_helpers.py +181 -0
- largestack/_observe/sqlite_exporter.py +260 -0
- largestack/_observe/tracer.py +71 -0
- largestack/_observe/traces_db.py +130 -0
- largestack/_orchestrate/__init__.py +10 -0
- largestack/_orchestrate/dag.py +137 -0
- largestack/_orchestrate/debate.py +136 -0
- largestack/_orchestrate/flows.py +45 -0
- largestack/_orchestrate/map_reduce.py +80 -0
- largestack/_orchestrate/parallel.py +156 -0
- largestack/_orchestrate/router.py +95 -0
- largestack/_orchestrate/sequential.py +145 -0
- largestack/_orchestrate/state_machine.py +67 -0
- largestack/_orchestrate/supervisor.py +54 -0
- largestack/_orchestrate/swarm.py +127 -0
- largestack/_rag/__init__.py +8 -0
- largestack/_rag/chunker.py +100 -0
- largestack/_rag/crag.py +39 -0
- largestack/_rag/embedder.py +297 -0
- largestack/_rag/eval.py +251 -0
- largestack/_rag/graph_rag.py +73 -0
- largestack/_rag/pipeline.py +59 -0
- largestack/_rag/query_engines.py +228 -0
- largestack/_rag/reranker.py +263 -0
- largestack/_rag/retriever.py +141 -0
- largestack/_rag/summary_index.py +251 -0
- largestack/_rag/vector_store.py +148 -0
- largestack/_ratelimit/__init__.py +363 -0
- largestack/_rerankers/__init__.py +435 -0
- largestack/_retrievers/__init__.py +817 -0
- largestack/_security/__init__.py +6 -0
- largestack/_security/code_sandbox.py +190 -0
- largestack/_security/e2b_bridge.py +391 -0
- largestack/_security/encryption.py +210 -0
- largestack/_security/mtls.py +378 -0
- largestack/_security/network.py +234 -0
- largestack/_security/permissions.py +226 -0
- largestack/_security/sandbox.py +41 -0
- largestack/_security/sbom.py +194 -0
- largestack/_security/vault.py +299 -0
- largestack/_state/__init__.py +2 -0
- largestack/_state/checkpoint.py +38 -0
- largestack/_state/durable.py +73 -0
- largestack/_state/postgres_checkpointer.py +215 -0
- largestack/_studio/__init__.py +1081 -0
- largestack/_studio/compare.py +409 -0
- largestack/_studio/pyodide_eval.py +354 -0
- largestack/_templates/__init__.py +1 -0
- largestack/_templates/code_generator.py +39 -0
- largestack/_templates/content_factory.py +26 -0
- largestack/_templates/customer_support.py +33 -0
- largestack/_templates/data_pipeline.py +32 -0
- largestack/_templates/research_pipeline.py +45 -0
- largestack/_test/__init__.py +6 -0
- largestack/_test/assertions.py +56 -0
- largestack/_test/benchmark.py +237 -0
- largestack/_test/ci_gates.py +49 -0
- largestack/_test/eval_metrics.py +81 -0
- largestack/_test/llm_judge.py +151 -0
- largestack/_test/recorder.py +30 -0
- largestack/_test/regression.py +81 -0
- largestack/_test/replayer.py +34 -0
- largestack/_test/synthetic.py +66 -0
- largestack/_vectorstores/__init__.py +2000 -0
- largestack/_workflow/__init__.py +15 -0
- largestack/_workflow/checkpoint.py +240 -0
- largestack/_workflow/graph.py +316 -0
- largestack/_workflow/interrupt.py +233 -0
- largestack/_workflow/sub_graph.py +246 -0
- largestack/agent.py +425 -0
- largestack/autonomous_builder.py +676 -0
- largestack/decorators.py +444 -0
- largestack/errors.py +92 -0
- largestack/guardrails.py +22 -0
- largestack/memory.py +23 -0
- largestack/migrations/__init__.py +14 -0
- largestack/migrations/config_v1_to_v1_1.py +49 -0
- largestack/migrations/memory_v1_to_v1_1.py +46 -0
- largestack/migrations/project.py +40 -0
- largestack/migrations/trace_db_v1_to_v1_1.py +53 -0
- largestack/observability.py +165 -0
- largestack/orchestrator.py +369 -0
- largestack/provider_matrix.py +81 -0
- largestack/py.typed +0 -0
- largestack/rag.py +11 -0
- largestack/serve.py +345 -0
- largestack/team.py +135 -0
- largestack/testing.py +354 -0
- largestack/types.py +80 -0
- largestack/workflow.py +190 -0
- largestack-1.0.0.dist-info/METADATA +436 -0
- largestack-1.0.0.dist-info/RECORD +312 -0
- largestack-1.0.0.dist-info/WHEEL +5 -0
- largestack-1.0.0.dist-info/entry_points.txt +2 -0
- largestack-1.0.0.dist-info/licenses/LICENSE +17 -0
- largestack-1.0.0.dist-info/top_level.txt +1 -0
largestack/__init__.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Largestack AI — Universal Multi-Agent AI Framework.
|
|
2
|
+
|
|
3
|
+
from largestack import Agent, Team, Workflow, tool
|
|
4
|
+
|
|
5
|
+
@tool
|
|
6
|
+
async def search(query: str) -> str:
|
|
7
|
+
return f"Results: {query}"
|
|
8
|
+
|
|
9
|
+
agent = Agent(name="r", tools=[search], llm="deepseek/deepseek-chat")
|
|
10
|
+
result = await agent.run("Analyze trends")
|
|
11
|
+
|
|
12
|
+
# Structured output
|
|
13
|
+
result = await agent.run("Analyze", response_model=MySchema)
|
|
14
|
+
|
|
15
|
+
# Multi-agent with error recovery
|
|
16
|
+
team = Team(agents=[a1, a2, a3], on_error="skip", retries_per_agent=2)
|
|
17
|
+
"""
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
|
|
20
|
+
# v0.3.7: auto-install logging redaction filter in production. Strips
|
|
21
|
+
# API keys / Bearer tokens / JWTs from log records before they're emitted.
|
|
22
|
+
# Disable via LARGESTACK_DISABLE_LOG_REDACTION=1 (not recommended).
|
|
23
|
+
import os as _os
|
|
24
|
+
if _os.environ.get("LARGESTACK_DISABLE_LOG_REDACTION", "").lower() not in ("1", "true", "yes"):
|
|
25
|
+
try:
|
|
26
|
+
from largestack._observe.log_redaction import install_redaction_filter
|
|
27
|
+
install_redaction_filter()
|
|
28
|
+
except Exception:
|
|
29
|
+
pass # Never let log-filter install break package import
|
|
30
|
+
del _os
|
|
31
|
+
|
|
32
|
+
_BENCHMARK_SUBPROCESS = __import__("os").environ.get("LARGESTACK_BENCHMARK_SUBPROCESS", "").lower() in ("1", "true", "yes")
|
|
33
|
+
if _BENCHMARK_SUBPROCESS:
|
|
34
|
+
# Keep benchmark subprocesses lightweight and independent of optional ML imports.
|
|
35
|
+
from largestack.agent import Agent
|
|
36
|
+
from largestack.testing import TestModel
|
|
37
|
+
__all__ = ["Agent", "TestModel"]
|
|
38
|
+
else:
|
|
39
|
+
from largestack.agent import Agent
|
|
40
|
+
from largestack.team import Team
|
|
41
|
+
from largestack.workflow import Workflow
|
|
42
|
+
from largestack.orchestrator import Orchestrator, OrchestratorResult
|
|
43
|
+
from largestack._core.tools import tool
|
|
44
|
+
from largestack._core.streaming import StreamHandler
|
|
45
|
+
from largestack._core.context import AgentContext
|
|
46
|
+
from largestack._core.session import SessionStore
|
|
47
|
+
from largestack._core.hitl import HumanInTheLoop
|
|
48
|
+
from largestack._core.registry import AgentRegistry
|
|
49
|
+
from largestack._guard.tool_access import ToolAccessPolicy
|
|
50
|
+
from largestack._guard.agent_identity import AgentIdentityManager
|
|
51
|
+
from largestack._guard.memory_integrity import MemoryIntegrityChecker
|
|
52
|
+
from largestack._guard.inter_agent_auth import InterAgentAuth
|
|
53
|
+
from largestack._core.ag_ui import AGUIServer
|
|
54
|
+
|
|
55
|
+
from largestack._core.steering import (
|
|
56
|
+
steer_before_tool, steer_after_model,
|
|
57
|
+
proceed, guide, interrupt, accept, discard,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
from largestack._guard.pipeline import GuardrailPipeline as Guardrails
|
|
61
|
+
from largestack.guardrails import create_guardrails
|
|
62
|
+
|
|
63
|
+
from largestack._memory.buffer import ConversationMemory
|
|
64
|
+
from largestack._memory.episodic import EpisodicMemory
|
|
65
|
+
from largestack._memory.observational import ObservationalMemory
|
|
66
|
+
from largestack._memory.procedural import ProceduralMemory
|
|
67
|
+
from largestack._memory.semantic import SemanticMemory
|
|
68
|
+
from largestack._memory.graph import GraphMemory
|
|
69
|
+
from largestack._memory.shared import SharedMemorySpace
|
|
70
|
+
from largestack.memory import create_memory
|
|
71
|
+
from largestack.rag import create_rag
|
|
72
|
+
from largestack.observability import Monitor, FeedbackRecord
|
|
73
|
+
from largestack.provider_matrix import provider_support_matrix, get_provider_capabilities, tool_capable_providers
|
|
74
|
+
from largestack.autonomous_builder import (
|
|
75
|
+
AutonomousProjectBuilder,
|
|
76
|
+
BuilderBudget,
|
|
77
|
+
BuildReport,
|
|
78
|
+
GeneratedFile,
|
|
79
|
+
NoOpMemory,
|
|
80
|
+
PatchSet,
|
|
81
|
+
ProjectBuildPlan,
|
|
82
|
+
ProjectSpec,
|
|
83
|
+
RepairAttempt,
|
|
84
|
+
ValidationResult,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Decorator API (PydanticAI-style) — v0.1.1.3
|
|
88
|
+
from largestack.decorators import (
|
|
89
|
+
Agent as TypedAgent,
|
|
90
|
+
RunContext,
|
|
91
|
+
ModelRetry,
|
|
92
|
+
AgentRunResult,
|
|
93
|
+
ToolDefinition,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Testing utilities
|
|
97
|
+
from largestack.testing import (
|
|
98
|
+
TestModel, FunctionModel, capture_run_messages,
|
|
99
|
+
disable_model_requests, enable_model_requests, block_model_requests,
|
|
100
|
+
ALLOW_MODEL_REQUESTS,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
from largestack.types import AgentResult, ToolCall, ToolResult, LLMResponse, CostEstimate
|
|
104
|
+
from largestack.errors import (
|
|
105
|
+
LargestackError, BudgetExceededError, LoopDetectedError,
|
|
106
|
+
ProviderError, GuardrailBlockedError, KillSwitchActivatedError,
|
|
107
|
+
ToolExecutionError, ToolPermissionError, ModelRequestsBlockedError,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"Agent", "Team", "Workflow", "Orchestrator", "OrchestratorResult", "tool", "StreamHandler",
|
|
112
|
+
"TypedAgent", "RunContext", "ModelRetry", "AgentRunResult", "ToolDefinition",
|
|
113
|
+
"TestModel", "FunctionModel", "capture_run_messages",
|
|
114
|
+
"disable_model_requests", "enable_model_requests", "block_model_requests",
|
|
115
|
+
"ALLOW_MODEL_REQUESTS",
|
|
116
|
+
"AgentContext", "SessionStore", "HumanInTheLoop", "AgentRegistry", "AGUIServer", "ToolAccessPolicy", "AgentIdentityManager", "MemoryIntegrityChecker", "InterAgentAuth",
|
|
117
|
+
"steer_before_tool", "steer_after_model", "proceed", "guide", "interrupt", "accept", "discard",
|
|
118
|
+
"Guardrails", "create_guardrails",
|
|
119
|
+
"ConversationMemory", "EpisodicMemory", "ObservationalMemory",
|
|
120
|
+
"ProceduralMemory", "SemanticMemory", "GraphMemory", "SharedMemorySpace",
|
|
121
|
+
"create_memory", "create_rag", "Monitor", "FeedbackRecord", "provider_support_matrix", "get_provider_capabilities", "tool_capable_providers",
|
|
122
|
+
"AutonomousProjectBuilder", "BuilderBudget", "BuildReport", "GeneratedFile", "NoOpMemory",
|
|
123
|
+
"PatchSet", "ProjectBuildPlan", "ProjectSpec", "RepairAttempt", "ValidationResult",
|
|
124
|
+
"AgentResult", "ToolCall", "ToolResult", "LLMResponse", "CostEstimate",
|
|
125
|
+
"LargestackError", "BudgetExceededError", "LoopDetectedError",
|
|
126
|
+
"ProviderError", "GuardrailBlockedError", "KillSwitchActivatedError",
|
|
127
|
+
"ToolExecutionError", "ToolPermissionError", "ModelRequestsBlockedError",
|
|
128
|
+
]
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""A2A (Agent2Agent) Protocol adapter (v0.12.0).
|
|
2
|
+
|
|
3
|
+
Closes the Google ADK / cross-framework interop gap. A2A v1.0 was
|
|
4
|
+
donated to the Linux Foundation and is in production at 150+ orgs
|
|
5
|
+
including SAP, ServiceNow, Salesforce, Workday.
|
|
6
|
+
|
|
7
|
+
A2A complements MCP:
|
|
8
|
+
- MCP — connects agents to **tools and data**
|
|
9
|
+
- A2A — connects agents to **other agents**
|
|
10
|
+
|
|
11
|
+
This module implements:
|
|
12
|
+
|
|
13
|
+
1. ``AgentCard`` — the discovery manifest. Lists what an agent can do.
|
|
14
|
+
2. ``A2AServer`` — exposes a LARGESTACK agent at an HTTP endpoint conforming
|
|
15
|
+
to the A2A Task interface.
|
|
16
|
+
3. ``A2AClient`` — invokes a remote A2A agent via its AgentCard.
|
|
17
|
+
4. ``A2ATask`` — task lifecycle types (submitted → working → completed/failed).
|
|
18
|
+
|
|
19
|
+
Spec reference: https://a2a-protocol.org/
|
|
20
|
+
|
|
21
|
+
This is a **lightweight reference implementation** of the protocol.
|
|
22
|
+
For full v0.3+ features (gRPC, security card signing) production users
|
|
23
|
+
can install the official SDK and use LARGESTACK agents as task handlers.
|
|
24
|
+
|
|
25
|
+
Zero external deps for the core types + client. Server uses ``aiohttp``
|
|
26
|
+
if available; falls back to a stdlib-only test server.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
import asyncio
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import time
|
|
33
|
+
import uuid
|
|
34
|
+
from dataclasses import asdict, dataclass, field
|
|
35
|
+
from typing import Any, Awaitable, Callable, Literal
|
|
36
|
+
|
|
37
|
+
log = logging.getLogger("largestack.a2a")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# -------------------- Domain types --------------------
|
|
41
|
+
|
|
42
|
+
TaskState = Literal[
|
|
43
|
+
"submitted", "working", "input-required", "completed",
|
|
44
|
+
"failed", "canceled",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require_http_url(url: str) -> str:
|
|
50
|
+
"""Allow only absolute HTTP/HTTPS URLs before network requests."""
|
|
51
|
+
from urllib.parse import urlparse
|
|
52
|
+
|
|
53
|
+
parsed = urlparse(url)
|
|
54
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
55
|
+
raise ValueError("URL must be absolute and use http or https")
|
|
56
|
+
return url
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class AgentSkill:
|
|
61
|
+
"""A single capability advertised by an agent."""
|
|
62
|
+
id: str
|
|
63
|
+
name: str
|
|
64
|
+
description: str = ""
|
|
65
|
+
tags: list[str] = field(default_factory=list)
|
|
66
|
+
examples: list[str] = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class AgentCapabilities:
|
|
71
|
+
"""What protocol features the agent supports."""
|
|
72
|
+
streaming: bool = False
|
|
73
|
+
push_notifications: bool = False
|
|
74
|
+
state_transition_history: bool = True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class AgentCard:
|
|
79
|
+
"""A2A agent discovery manifest. Served at ``/.well-known/agent.json``.
|
|
80
|
+
|
|
81
|
+
Spec: https://a2a-protocol.org/latest/specification/agent-card/
|
|
82
|
+
"""
|
|
83
|
+
name: str
|
|
84
|
+
description: str
|
|
85
|
+
url: str # base URL where the agent is hosted
|
|
86
|
+
version: str = "1.0.0"
|
|
87
|
+
protocol_version: str = "0.3.0"
|
|
88
|
+
capabilities: AgentCapabilities = field(default_factory=AgentCapabilities)
|
|
89
|
+
skills: list[AgentSkill] = field(default_factory=list)
|
|
90
|
+
default_input_modes: list[str] = field(
|
|
91
|
+
default_factory=lambda: ["text/plain"],
|
|
92
|
+
)
|
|
93
|
+
default_output_modes: list[str] = field(
|
|
94
|
+
default_factory=lambda: ["text/plain"],
|
|
95
|
+
)
|
|
96
|
+
# Provider info (org publishing this agent)
|
|
97
|
+
provider_name: str = ""
|
|
98
|
+
provider_url: str = ""
|
|
99
|
+
# Authentication required to invoke (none / api-key / oauth2)
|
|
100
|
+
authentication: dict[str, Any] = field(default_factory=dict)
|
|
101
|
+
|
|
102
|
+
def to_dict(self) -> dict[str, Any]:
|
|
103
|
+
return asdict(self)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_dict(cls, d: dict[str, Any]) -> "AgentCard":
|
|
107
|
+
# Tolerant: drop unknown keys
|
|
108
|
+
valid = {f.name for f in cls.__dataclass_fields__.values()}
|
|
109
|
+
clean = {k: v for k, v in d.items() if k in valid}
|
|
110
|
+
|
|
111
|
+
# Re-hydrate nested types
|
|
112
|
+
if "capabilities" in clean and isinstance(clean["capabilities"], dict):
|
|
113
|
+
cap_valid = {
|
|
114
|
+
f.name for f in AgentCapabilities.__dataclass_fields__.values()
|
|
115
|
+
}
|
|
116
|
+
clean["capabilities"] = AgentCapabilities(**{
|
|
117
|
+
k: v for k, v in clean["capabilities"].items()
|
|
118
|
+
if k in cap_valid
|
|
119
|
+
})
|
|
120
|
+
if "skills" in clean and isinstance(clean["skills"], list):
|
|
121
|
+
skill_valid = {
|
|
122
|
+
f.name for f in AgentSkill.__dataclass_fields__.values()
|
|
123
|
+
}
|
|
124
|
+
clean["skills"] = [
|
|
125
|
+
AgentSkill(**{k: v for k, v in s.items() if k in skill_valid})
|
|
126
|
+
if isinstance(s, dict) else s
|
|
127
|
+
for s in clean["skills"]
|
|
128
|
+
]
|
|
129
|
+
return cls(**clean)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class A2AMessage:
|
|
134
|
+
"""A single message in a task conversation."""
|
|
135
|
+
role: Literal["user", "agent"]
|
|
136
|
+
parts: list[dict[str, Any]] = field(default_factory=list)
|
|
137
|
+
timestamp: float = field(default_factory=lambda: time.time())
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def text(cls, role: Literal["user", "agent"], text: str) -> "A2AMessage":
|
|
141
|
+
return cls(role=role, parts=[{"type": "text", "text": text}])
|
|
142
|
+
|
|
143
|
+
def get_text(self) -> str:
|
|
144
|
+
"""Concatenate all text parts."""
|
|
145
|
+
return "\n".join(
|
|
146
|
+
p.get("text", "") for p in self.parts
|
|
147
|
+
if p.get("type") == "text"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class A2ATask:
|
|
153
|
+
"""A2A task lifecycle object."""
|
|
154
|
+
id: str
|
|
155
|
+
state: TaskState = "submitted"
|
|
156
|
+
messages: list[A2AMessage] = field(default_factory=list)
|
|
157
|
+
artifacts: list[dict[str, Any]] = field(default_factory=list)
|
|
158
|
+
created_at: float = field(default_factory=lambda: time.time())
|
|
159
|
+
updated_at: float = field(default_factory=lambda: time.time())
|
|
160
|
+
error: str = ""
|
|
161
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
162
|
+
|
|
163
|
+
def add_message(self, msg: A2AMessage) -> None:
|
|
164
|
+
self.messages.append(msg)
|
|
165
|
+
self.updated_at = time.time()
|
|
166
|
+
|
|
167
|
+
def add_artifact(self, artifact: dict[str, Any]) -> None:
|
|
168
|
+
self.artifacts.append(artifact)
|
|
169
|
+
self.updated_at = time.time()
|
|
170
|
+
|
|
171
|
+
def transition(self, state: TaskState, error: str = "") -> None:
|
|
172
|
+
self.state = state
|
|
173
|
+
self.updated_at = time.time()
|
|
174
|
+
if error:
|
|
175
|
+
self.error = error
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> dict[str, Any]:
|
|
178
|
+
return {
|
|
179
|
+
"id": self.id,
|
|
180
|
+
"state": self.state,
|
|
181
|
+
"messages": [
|
|
182
|
+
{
|
|
183
|
+
"role": m.role, "parts": m.parts,
|
|
184
|
+
"timestamp": m.timestamp,
|
|
185
|
+
}
|
|
186
|
+
for m in self.messages
|
|
187
|
+
],
|
|
188
|
+
"artifacts": self.artifacts,
|
|
189
|
+
"created_at": self.created_at,
|
|
190
|
+
"updated_at": self.updated_at,
|
|
191
|
+
"error": self.error,
|
|
192
|
+
"metadata": self.metadata,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# -------------------- Server --------------------
|
|
197
|
+
|
|
198
|
+
# Type signature for the agent handler callable that the server wraps
|
|
199
|
+
AgentHandler = Callable[[str, A2ATask], Awaitable[str]]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class A2AServer:
|
|
203
|
+
"""Exposes a LARGESTACK agent as an A2A-compliant HTTP endpoint.
|
|
204
|
+
|
|
205
|
+
Provides:
|
|
206
|
+
- ``GET /.well-known/agent.json`` → AgentCard
|
|
207
|
+
- ``POST /tasks/send`` → submit a new task (sync)
|
|
208
|
+
- ``GET /tasks/{id}`` → query task status
|
|
209
|
+
- ``POST /tasks/{id}/cancel`` → cancel a task
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
card: the ``AgentCard`` describing this agent
|
|
213
|
+
handler: async function ``(input_text, task) -> output_text``
|
|
214
|
+
task_ttl_seconds: how long completed tasks are retained (default 1hr)
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
*,
|
|
220
|
+
card: AgentCard,
|
|
221
|
+
handler: AgentHandler,
|
|
222
|
+
task_ttl_seconds: float = 3600.0,
|
|
223
|
+
):
|
|
224
|
+
self.card = card
|
|
225
|
+
self.handler = handler
|
|
226
|
+
self.task_ttl_seconds = task_ttl_seconds
|
|
227
|
+
self._tasks: dict[str, A2ATask] = {}
|
|
228
|
+
self._lock = asyncio.Lock()
|
|
229
|
+
|
|
230
|
+
async def submit_task(
|
|
231
|
+
self, input_text: str,
|
|
232
|
+
*,
|
|
233
|
+
task_id: str | None = None,
|
|
234
|
+
metadata: dict[str, Any] | None = None,
|
|
235
|
+
) -> A2ATask:
|
|
236
|
+
"""Submit a new task. Returns the completed (or failed) task."""
|
|
237
|
+
task = A2ATask(
|
|
238
|
+
id=task_id or str(uuid.uuid4()),
|
|
239
|
+
metadata=metadata or {},
|
|
240
|
+
)
|
|
241
|
+
task.add_message(A2AMessage.text("user", input_text))
|
|
242
|
+
|
|
243
|
+
async with self._lock:
|
|
244
|
+
self._tasks[task.id] = task
|
|
245
|
+
|
|
246
|
+
task.transition("working")
|
|
247
|
+
try:
|
|
248
|
+
output = await self.handler(input_text, task)
|
|
249
|
+
task.add_message(A2AMessage.text("agent", str(output)))
|
|
250
|
+
task.transition("completed")
|
|
251
|
+
except asyncio.CancelledError:
|
|
252
|
+
task.transition("canceled")
|
|
253
|
+
raise
|
|
254
|
+
except Exception as e:
|
|
255
|
+
task.transition("failed", error=str(e))
|
|
256
|
+
log.exception(f"task {task.id} failed")
|
|
257
|
+
|
|
258
|
+
return task
|
|
259
|
+
|
|
260
|
+
async def get_task(self, task_id: str) -> A2ATask | None:
|
|
261
|
+
async with self._lock:
|
|
262
|
+
return self._tasks.get(task_id)
|
|
263
|
+
|
|
264
|
+
async def cancel_task(self, task_id: str) -> bool:
|
|
265
|
+
async with self._lock:
|
|
266
|
+
task = self._tasks.get(task_id)
|
|
267
|
+
if not task:
|
|
268
|
+
return False
|
|
269
|
+
if task.state in ("completed", "failed", "canceled"):
|
|
270
|
+
return False
|
|
271
|
+
task.transition("canceled")
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
async def purge_expired_tasks(self) -> int:
|
|
275
|
+
"""Remove tasks older than ``task_ttl_seconds``."""
|
|
276
|
+
async with self._lock:
|
|
277
|
+
now = time.time()
|
|
278
|
+
to_delete = [
|
|
279
|
+
tid for tid, t in self._tasks.items()
|
|
280
|
+
if t.state in ("completed", "failed", "canceled")
|
|
281
|
+
and (now - t.updated_at) > self.task_ttl_seconds
|
|
282
|
+
]
|
|
283
|
+
for tid in to_delete:
|
|
284
|
+
del self._tasks[tid]
|
|
285
|
+
return len(to_delete)
|
|
286
|
+
|
|
287
|
+
# -------------------- HTTP request handlers --------------------
|
|
288
|
+
|
|
289
|
+
async def handle_request(
|
|
290
|
+
self, method: str, path: str,
|
|
291
|
+
body: dict[str, Any] | None = None,
|
|
292
|
+
) -> tuple[int, dict[str, Any]]:
|
|
293
|
+
"""Generic request dispatcher. Returns (status_code, body_dict).
|
|
294
|
+
|
|
295
|
+
Implementers can wire this into aiohttp / FastAPI / starlette.
|
|
296
|
+
"""
|
|
297
|
+
# Discovery
|
|
298
|
+
if method == "GET" and path == "/.well-known/agent.json":
|
|
299
|
+
return 200, self.card.to_dict()
|
|
300
|
+
|
|
301
|
+
# Submit task
|
|
302
|
+
if method == "POST" and path == "/tasks/send":
|
|
303
|
+
body = body or {}
|
|
304
|
+
input_text = body.get("input", "")
|
|
305
|
+
if not input_text:
|
|
306
|
+
# Fall back to first user message text
|
|
307
|
+
msg = body.get("message", {})
|
|
308
|
+
if isinstance(msg, dict):
|
|
309
|
+
parts = msg.get("parts", [])
|
|
310
|
+
input_text = "\n".join(
|
|
311
|
+
p.get("text", "") for p in parts
|
|
312
|
+
if isinstance(p, dict) and p.get("type") == "text"
|
|
313
|
+
)
|
|
314
|
+
if not input_text:
|
|
315
|
+
return 400, {"error": "input is required"}
|
|
316
|
+
task = await self.submit_task(
|
|
317
|
+
input_text,
|
|
318
|
+
task_id=body.get("id"),
|
|
319
|
+
metadata=body.get("metadata") or {},
|
|
320
|
+
)
|
|
321
|
+
return 200, task.to_dict()
|
|
322
|
+
|
|
323
|
+
# Query task
|
|
324
|
+
if method == "GET" and path.startswith("/tasks/"):
|
|
325
|
+
task_id = path[len("/tasks/"):].split("/")[0]
|
|
326
|
+
task = await self.get_task(task_id)
|
|
327
|
+
if not task:
|
|
328
|
+
return 404, {"error": f"task {task_id} not found"}
|
|
329
|
+
return 200, task.to_dict()
|
|
330
|
+
|
|
331
|
+
# Cancel task
|
|
332
|
+
if method == "POST" and path.startswith("/tasks/") \
|
|
333
|
+
and path.endswith("/cancel"):
|
|
334
|
+
task_id = path[len("/tasks/"):-len("/cancel")]
|
|
335
|
+
ok = await self.cancel_task(task_id)
|
|
336
|
+
if not ok:
|
|
337
|
+
return 400, {"error": "cannot cancel task"}
|
|
338
|
+
return 200, {"id": task_id, "state": "canceled"}
|
|
339
|
+
|
|
340
|
+
return 404, {"error": "unknown endpoint"}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# -------------------- Client --------------------
|
|
344
|
+
|
|
345
|
+
class A2AClient:
|
|
346
|
+
"""Client for invoking a remote A2A agent.
|
|
347
|
+
|
|
348
|
+
Uses ``aiohttp`` if available, falls back to ``urllib`` (sync).
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
base_url: base URL of the remote agent (e.g. ``https://agent.example.com``)
|
|
352
|
+
api_key: optional bearer token
|
|
353
|
+
timeout: per-request timeout in seconds
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
def __init__(
|
|
357
|
+
self,
|
|
358
|
+
*,
|
|
359
|
+
base_url: str,
|
|
360
|
+
api_key: str = "",
|
|
361
|
+
timeout: float = 30.0,
|
|
362
|
+
):
|
|
363
|
+
self.base_url = base_url.rstrip("/")
|
|
364
|
+
self.api_key = api_key
|
|
365
|
+
self.timeout = timeout
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def _headers(self) -> dict[str, str]:
|
|
369
|
+
h = {"Content-Type": "application/json"}
|
|
370
|
+
if self.api_key:
|
|
371
|
+
h["Authorization"] = f"Bearer {self.api_key}"
|
|
372
|
+
return h
|
|
373
|
+
|
|
374
|
+
async def _post_json(
|
|
375
|
+
self, path: str, body: dict[str, Any],
|
|
376
|
+
) -> tuple[int, dict[str, Any]]:
|
|
377
|
+
url = self.base_url + path
|
|
378
|
+
try:
|
|
379
|
+
import aiohttp
|
|
380
|
+
except ImportError:
|
|
381
|
+
return await self._post_json_urllib(url, body)
|
|
382
|
+
|
|
383
|
+
async with aiohttp.ClientSession(headers=self._headers) as s:
|
|
384
|
+
async with s.post(
|
|
385
|
+
url, json=body, timeout=aiohttp.ClientTimeout(
|
|
386
|
+
total=self.timeout,
|
|
387
|
+
),
|
|
388
|
+
) as resp:
|
|
389
|
+
return resp.status, await resp.json()
|
|
390
|
+
|
|
391
|
+
async def _get_json(
|
|
392
|
+
self, path: str,
|
|
393
|
+
) -> tuple[int, dict[str, Any]]:
|
|
394
|
+
url = self.base_url + path
|
|
395
|
+
try:
|
|
396
|
+
import aiohttp
|
|
397
|
+
except ImportError:
|
|
398
|
+
return await self._get_json_urllib(url)
|
|
399
|
+
|
|
400
|
+
async with aiohttp.ClientSession(headers=self._headers) as s:
|
|
401
|
+
async with s.get(
|
|
402
|
+
url, timeout=aiohttp.ClientTimeout(total=self.timeout),
|
|
403
|
+
) as resp:
|
|
404
|
+
return resp.status, await resp.json()
|
|
405
|
+
|
|
406
|
+
async def _post_json_urllib(
|
|
407
|
+
self, url: str, body: dict[str, Any],
|
|
408
|
+
) -> tuple[int, dict[str, Any]]:
|
|
409
|
+
"""stdlib fallback. Synchronous, run in thread."""
|
|
410
|
+
import urllib.error
|
|
411
|
+
import urllib.request
|
|
412
|
+
|
|
413
|
+
def _do():
|
|
414
|
+
req = urllib.request.Request(
|
|
415
|
+
url,
|
|
416
|
+
data=json.dumps(body).encode(),
|
|
417
|
+
headers=self._headers,
|
|
418
|
+
method="POST",
|
|
419
|
+
)
|
|
420
|
+
try:
|
|
421
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as r: # nosec B310
|
|
422
|
+
return r.status, json.loads(r.read().decode())
|
|
423
|
+
except urllib.error.HTTPError as e:
|
|
424
|
+
try:
|
|
425
|
+
return e.code, json.loads(e.read().decode())
|
|
426
|
+
except Exception:
|
|
427
|
+
return e.code, {"error": str(e)}
|
|
428
|
+
|
|
429
|
+
return await asyncio.to_thread(_do)
|
|
430
|
+
|
|
431
|
+
async def _get_json_urllib(
|
|
432
|
+
self, url: str,
|
|
433
|
+
) -> tuple[int, dict[str, Any]]:
|
|
434
|
+
import urllib.error
|
|
435
|
+
import urllib.request
|
|
436
|
+
|
|
437
|
+
def _do():
|
|
438
|
+
req = urllib.request.Request(
|
|
439
|
+
url, headers=self._headers, method="GET",
|
|
440
|
+
)
|
|
441
|
+
try:
|
|
442
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as r: # nosec B310
|
|
443
|
+
return r.status, json.loads(r.read().decode())
|
|
444
|
+
except urllib.error.HTTPError as e:
|
|
445
|
+
try:
|
|
446
|
+
return e.code, json.loads(e.read().decode())
|
|
447
|
+
except Exception:
|
|
448
|
+
return e.code, {"error": str(e)}
|
|
449
|
+
|
|
450
|
+
return await asyncio.to_thread(_do)
|
|
451
|
+
|
|
452
|
+
# -------------------- Public API --------------------
|
|
453
|
+
|
|
454
|
+
async def discover(self) -> AgentCard:
|
|
455
|
+
"""Fetch the agent's AgentCard."""
|
|
456
|
+
status, body = await self._get_json("/.well-known/agent.json")
|
|
457
|
+
if status != 200:
|
|
458
|
+
raise RuntimeError(
|
|
459
|
+
f"discover failed: HTTP {status} - {body.get('error', '')}"
|
|
460
|
+
)
|
|
461
|
+
return AgentCard.from_dict(body)
|
|
462
|
+
|
|
463
|
+
async def send_task(
|
|
464
|
+
self, input_text: str, *,
|
|
465
|
+
task_id: str | None = None,
|
|
466
|
+
metadata: dict[str, Any] | None = None,
|
|
467
|
+
) -> A2ATask:
|
|
468
|
+
"""Submit a task and wait for completion."""
|
|
469
|
+
body: dict[str, Any] = {"input": input_text}
|
|
470
|
+
if task_id:
|
|
471
|
+
body["id"] = task_id
|
|
472
|
+
if metadata:
|
|
473
|
+
body["metadata"] = metadata
|
|
474
|
+
status, resp = await self._post_json("/tasks/send", body)
|
|
475
|
+
if status != 200:
|
|
476
|
+
raise RuntimeError(
|
|
477
|
+
f"send_task failed: HTTP {status} - {resp.get('error', '')}"
|
|
478
|
+
)
|
|
479
|
+
return _task_from_dict(resp)
|
|
480
|
+
|
|
481
|
+
async def get_task(self, task_id: str) -> A2ATask | None:
|
|
482
|
+
status, resp = await self._get_json(f"/tasks/{task_id}")
|
|
483
|
+
if status == 404:
|
|
484
|
+
return None
|
|
485
|
+
if status != 200:
|
|
486
|
+
raise RuntimeError(
|
|
487
|
+
f"get_task failed: HTTP {status} - {resp.get('error', '')}"
|
|
488
|
+
)
|
|
489
|
+
return _task_from_dict(resp)
|
|
490
|
+
|
|
491
|
+
async def cancel_task(self, task_id: str) -> bool:
|
|
492
|
+
status, _ = await self._post_json(f"/tasks/{task_id}/cancel", {})
|
|
493
|
+
return status == 200
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _task_from_dict(d: dict[str, Any]) -> A2ATask:
|
|
497
|
+
"""Hydrate an A2ATask from its serialized form."""
|
|
498
|
+
msgs = []
|
|
499
|
+
for m in d.get("messages", []):
|
|
500
|
+
if isinstance(m, dict):
|
|
501
|
+
msgs.append(A2AMessage(
|
|
502
|
+
role=m.get("role", "user"),
|
|
503
|
+
parts=m.get("parts", []),
|
|
504
|
+
timestamp=m.get("timestamp", time.time()),
|
|
505
|
+
))
|
|
506
|
+
return A2ATask(
|
|
507
|
+
id=d.get("id", ""),
|
|
508
|
+
state=d.get("state", "submitted"),
|
|
509
|
+
messages=msgs,
|
|
510
|
+
artifacts=d.get("artifacts", []),
|
|
511
|
+
created_at=d.get("created_at", time.time()),
|
|
512
|
+
updated_at=d.get("updated_at", time.time()),
|
|
513
|
+
error=d.get("error", ""),
|
|
514
|
+
metadata=d.get("metadata", {}),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# -------------------- LARGESTACK-side helpers --------------------
|
|
519
|
+
|
|
520
|
+
def expose_largestack_agent(
|
|
521
|
+
largestack_agent,
|
|
522
|
+
*,
|
|
523
|
+
name: str,
|
|
524
|
+
description: str,
|
|
525
|
+
url: str,
|
|
526
|
+
skills: list[AgentSkill] | None = None,
|
|
527
|
+
provider_name: str = "RivaiLabs",
|
|
528
|
+
provider_url: str = "https://rivailabs.com",
|
|
529
|
+
) -> A2AServer:
|
|
530
|
+
"""Convenience: wrap a LARGESTACK Agent as an A2A server.
|
|
531
|
+
|
|
532
|
+
The agent must have an async ``.run(input)`` method that returns an
|
|
533
|
+
object with a ``.content`` attribute (LARGESTACK Agent contract).
|
|
534
|
+
"""
|
|
535
|
+
card = AgentCard(
|
|
536
|
+
name=name,
|
|
537
|
+
description=description,
|
|
538
|
+
url=url,
|
|
539
|
+
skills=skills or [],
|
|
540
|
+
provider_name=provider_name,
|
|
541
|
+
provider_url=provider_url,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
async def handler(input_text: str, task: A2ATask) -> str:
|
|
545
|
+
resp = await largestack_agent.run(input_text)
|
|
546
|
+
return getattr(resp, "content", str(resp))
|
|
547
|
+
|
|
548
|
+
return A2AServer(card=card, handler=handler)
|