From 041f3e65b60acaab10d71234f810f7ee54c46217 Mon Sep 17 00:00:00 2001 From: a2a-platform Date: Thu, 28 May 2026 12:53:13 +0000 Subject: [PATCH] deploy --- README.md | 10 + a2a.yaml | 12 + agent.py | 439 ++++++++++++++++++++++++++++ openapi.json | 741 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 7 + 5 files changed, 1209 insertions(+) create mode 100644 README.md create mode 100644 a2a.yaml create mode 100644 agent.py create mode 100644 openapi.json create mode 100644 requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..acb095d --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# tasks-api + +Generated A2APack agent for Tasks API. + +- Source OpenAPI: https://tasks.a2acloud.io/openapi.json +- Generated operations: 6 +- Main skill: `auto` + +The generated code is intentionally editable. Tune prompts, operation +grouping, auth names, and safety policy before publishing serious agents. diff --git a/a2a.yaml b/a2a.yaml new file mode 100644 index 0000000..5722b8b --- /dev/null +++ b/a2a.yaml @@ -0,0 +1,12 @@ +name: tasks-api +version: 0.1.0 +entrypoint: agent:TasksApi +description: API-only service for tracking tasks in Postgres. +runtime: + resources: + cpu: 200m + memory: 512Mi + egress: + allow_hosts: + - tasks.a2acloud.io + deny_internet_by_default: true diff --git a/agent.py b/agent.py new file mode 100644 index 0000000..2e2bfec --- /dev/null +++ b/agent.py @@ -0,0 +1,439 @@ +from __future__ import annotations + +import base64 +import json +import re +from typing import Any + +import httpx +from deepagents import create_deep_agent +from langchain_core.messages import BaseMessage +from langchain_core.tools import StructuredTool +from langchain_openai import ChatOpenAI +from pydantic import BaseModel, Field + +from a2a_pack import ( + A2AAgent, + ConsumerSetup, + ConsumerSetupField, + ConsumerSetupMissing, + EgressPolicy, + LLMProvisioning, + Pricing, + Resources, + RunContext, + skill, +) + + +DEFAULT_BASE_URL = "https://tasks.a2acloud.io" +OPERATIONS = json.loads("{\n \"create_task\": {\n \"description\": \"Create Task\",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"create_task\",\n \"parameters\": [],\n \"path\": \"/api/tasks\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/TaskCreate\"\n },\n \"security\": [\n {\n \"bearerAuth\": []\n },\n {\n \"apiKeyAuth\": []\n }\n ],\n \"skill_name\": \"create_task\",\n \"summary\": \"Create Task\",\n \"tags\": [\n \"Tasks\"\n ]\n },\n \"delete_task\": {\n \"description\": \"Delete Task\",\n \"destructive\": true,\n \"method\": \"DELETE\",\n \"operation_id\": \"delete_task\",\n \"parameters\": [\n {\n \"description\": \"\",\n \"in\": \"path\",\n \"name\": \"task_id\",\n \"required\": true,\n \"schema\": {\n \"title\": \"Task Id\",\n \"type\": \"string\"\n }\n }\n ],\n \"path\": \"/api/tasks/{task_id}\",\n \"request_body\": null,\n \"security\": [\n {\n \"bearerAuth\": []\n },\n {\n \"apiKeyAuth\": []\n }\n ],\n \"skill_name\": \"delete_task\",\n \"summary\": \"Delete Task\",\n \"tags\": [\n \"Tasks\"\n ]\n },\n \"get_task\": {\n \"description\": \"Get Task\",\n \"destructive\": false,\n \"method\": \"GET\",\n \"operation_id\": \"get_task\",\n \"parameters\": [\n {\n \"description\": \"\",\n \"in\": \"path\",\n \"name\": \"task_id\",\n \"required\": true,\n \"schema\": {\n \"title\": \"Task Id\",\n \"type\": \"string\"\n }\n }\n ],\n \"path\": \"/api/tasks/{task_id}\",\n \"request_body\": null,\n \"security\": [\n {\n \"bearerAuth\": []\n },\n {\n \"apiKeyAuth\": []\n }\n ],\n \"skill_name\": \"get_task\",\n \"summary\": \"Get Task\",\n \"tags\": [\n \"Tasks\"\n ]\n },\n \"get_tasks_health\": {\n \"description\": \"Healthz\",\n \"destructive\": false,\n \"method\": \"GET\",\n \"operation_id\": \"get_tasks_health\",\n \"parameters\": [],\n \"path\": \"/healthz\",\n \"request_body\": null,\n \"security\": null,\n \"skill_name\": \"get_tasks_health\",\n \"summary\": \"Healthz\",\n \"tags\": [\n \"Health\"\n ]\n },\n \"list_tasks\": {\n \"description\": \"List Tasks\",\n \"destructive\": false,\n \"method\": \"GET\",\n \"operation_id\": \"list_tasks\",\n \"parameters\": [\n {\n \"description\": \"\",\n \"in\": \"query\",\n \"name\": \"status\",\n \"required\": false,\n \"schema\": {\n \"anyOf\": [\n {\n \"enum\": [\n \"todo\",\n \"in_progress\",\n \"done\",\n \"canceled\"\n ],\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"title\": \"Status\"\n }\n },\n {\n \"description\": \"\",\n \"in\": \"query\",\n \"name\": \"q\",\n \"required\": false,\n \"schema\": {\n \"anyOf\": [\n {\n \"maxLength\": 120,\n \"minLength\": 1,\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"title\": \"Q\"\n }\n },\n {\n \"description\": \"\",\n \"in\": \"query\",\n \"name\": \"due_before\",\n \"required\": false,\n \"schema\": {\n \"anyOf\": [\n {\n \"format\": \"date-time\",\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"title\": \"Due Before\"\n }\n },\n {\n \"description\": \"\",\n \"in\": \"query\",\n \"name\": \"limit\",\n \"required\": false,\n \"schema\": {\n \"default\": 50,\n \"maximum\": 200,\n \"minimum\": 1,\n \"title\": \"Limit\",\n \"type\": \"integer\"\n }\n },\n {\n \"description\": \"\",\n \"in\": \"query\",\n \"name\": \"offset\",\n \"required\": false,\n \"schema\": {\n \"default\": 0,\n \"minimum\": 0,\n \"title\": \"Offset\",\n \"type\": \"integer\"\n }\n }\n ],\n \"path\": \"/api/tasks\",\n \"request_body\": null,\n \"security\": [\n {\n \"bearerAuth\": []\n },\n {\n \"apiKeyAuth\": []\n }\n ],\n \"skill_name\": \"list_tasks\",\n \"summary\": \"List Tasks\",\n \"tags\": [\n \"Tasks\"\n ]\n },\n \"update_task\": {\n \"description\": \"Update Task\",\n \"destructive\": true,\n \"method\": \"PATCH\",\n \"operation_id\": \"update_task\",\n \"parameters\": [\n {\n \"description\": \"\",\n \"in\": \"path\",\n \"name\": \"task_id\",\n \"required\": true,\n \"schema\": {\n \"title\": \"Task Id\",\n \"type\": \"string\"\n }\n }\n ],\n \"path\": \"/api/tasks/{task_id}\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/TaskUpdate\"\n },\n \"security\": [\n {\n \"bearerAuth\": []\n },\n {\n \"apiKeyAuth\": []\n }\n ],\n \"skill_name\": \"update_task\",\n \"summary\": \"Update Task\",\n \"tags\": [\n \"Tasks\"\n ]\n }\n}") +ROOT_SECURITY = json.loads("[]") +SECURITY_SCHEMES = json.loads("{\n \"apiKeyAuth\": {\n \"description\": \"Use the configured `TASKS_API_KEY` value.\",\n \"in\": \"header\",\n \"name\": \"x-api-key\",\n \"type\": \"apiKey\"\n },\n \"bearerAuth\": {\n \"description\": \"Use `Authorization: Bearer $TASKS_API_KEY`.\",\n \"scheme\": \"bearer\",\n \"type\": \"http\"\n }\n}") +SECURITY_FIELDS = json.loads("{\n \"apiKeyAuth\": {\n \"field\": \"X_API_KEY\",\n \"kind\": \"apiKey\",\n \"location\": \"header\",\n \"name\": \"x-api-key\"\n },\n \"bearerAuth\": {\n \"field\": \"BEARERAUTH_TOKEN\",\n \"kind\": \"http\",\n \"scheme\": \"bearer\"\n }\n}") +PATH_PARAMETER_RE = re.compile(r"{([^}/]+)}") + + +class OperationInput(BaseModel): + parameters: dict[str, Any] = Field( + default_factory=dict, + description="Path, query, header, and cookie parameters keyed by OpenAPI parameter name.", + ) + body: Any | None = Field(default=None, description="JSON request body, when the operation accepts one.") + + +class TasksApi(A2AAgent): + name = "tasks-api" + description = "API-only service for tracking tasks in Postgres." + version = "0.1.0" + consumer_setup = ConsumerSetup.from_fields( + ConsumerSetupField.config("OPENAPI_BASE_URL", label="API base URL", description="Override the default API server (https://tasks.a2acloud.io).", required=False, input_type="url"), + ConsumerSetupField.secret("BEARERAUTH_TOKEN", label="bearerAuth bearer credential", description="Use `Authorization: Bearer $TASKS_API_KEY`.", required=True), + ConsumerSetupField.secret("X_API_KEY", label="apiKeyAuth API key", description="Use the configured `TASKS_API_KEY` value.", required=True), + ) + llm_provisioning = LLMProvisioning.PLATFORM + pricing = Pricing( + price_per_call_usd=0.0, + caller_pays_llm=False, + notes="Uses a scoped platform LLM grant through ctx.llm.", + ) + resources = Resources(cpu="200m", memory="512Mi") + egress = EgressPolicy( + allow_hosts=('tasks.a2acloud.io',), + deny_internet_by_default=True, + ) + tools_used = ("openapi", "deepagents") + capabilities = { + "openapi_auto_agent": { + "operation_count": len(OPERATIONS), + "default_base_url": DEFAULT_BASE_URL, + "security_schemes": list(SECURITY_SCHEMES), + } + } + + @skill( + name="auto", + description="Use the OpenAPI service to complete a natural-language goal.", + tags=("openapi", "auto"), + timeout_seconds=180, + ) + async def auto(self, ctx: RunContext, goal: str) -> dict[str, Any]: + creds = ctx.llm + if not creds.api_key: + return { + "error": "llm_credentials_missing", + "final": ( + "LLM credentials were not available for this run. Hosted " + "generated OpenAPI agents should receive a platform LLM grant." + ), + "messages": [], + } + model_kwargs: dict[str, Any] = { + "model": creds.model, + "base_url": creds.base_url, + "api_key": creds.api_key, + } + if creds.temperature_mode != "omit" and creds.temperature is not None: + model_kwargs["temperature"] = creds.temperature + if creds.extra_body: + model_kwargs["extra_body"] = dict(creds.extra_body) + model = ChatOpenAI(**model_kwargs) + graph = create_deep_agent( + model=model, + tools=self._operation_tools(ctx), + system_prompt=self._system_prompt(), + ) + result = await graph.ainvoke({"messages": [{"role": "user", "content": goal}]}) + messages = result.get("messages", []) if isinstance(result, dict) else [] + final = _message_text(messages[-1]) if messages else result + return { + "final": final, + "messages": [_message_to_dict(message) for message in messages[-8:]], + } + + @skill( + name="get_tasks_health", + description="GET /healthz - Healthz", + tags=('Health',), + timeout_seconds=60, + ) + async def get_tasks_health( + self, + ctx: RunContext, + parameters: dict[str, Any] | None = None, + body: Any | None = None, + ) -> dict[str, Any]: + return await self._request( + ctx, + "get_tasks_health", + parameters=parameters or {}, + body=body, + ) + + @skill( + name="list_tasks", + description="GET /api/tasks - List Tasks", + tags=('Tasks',), + timeout_seconds=60, + ) + async def list_tasks( + self, + ctx: RunContext, + parameters: dict[str, Any] | None = None, + body: Any | None = None, + ) -> dict[str, Any]: + return await self._request( + ctx, + "list_tasks", + parameters=parameters or {}, + body=body, + ) + + @skill( + name="create_task", + description="POST /api/tasks - Create Task", + tags=('Tasks',), + timeout_seconds=120, + ) + async def create_task( + self, + ctx: RunContext, + parameters: dict[str, Any] | None = None, + body: Any | None = None, + ) -> dict[str, Any]: + return await self._request( + ctx, + "create_task", + parameters=parameters or {}, + body=body, + ) + + @skill( + name="get_task", + description="GET /api/tasks/{task_id} - Get Task", + tags=('Tasks',), + timeout_seconds=60, + ) + async def get_task( + self, + ctx: RunContext, + parameters: dict[str, Any] | None = None, + body: Any | None = None, + ) -> dict[str, Any]: + return await self._request( + ctx, + "get_task", + parameters=parameters or {}, + body=body, + ) + + @skill( + name="update_task", + description="PATCH /api/tasks/{task_id} - Update Task", + tags=('Tasks',), + timeout_seconds=120, + ) + async def update_task( + self, + ctx: RunContext, + parameters: dict[str, Any] | None = None, + body: Any | None = None, + ) -> dict[str, Any]: + return await self._request( + ctx, + "update_task", + parameters=parameters or {}, + body=body, + ) + + @skill( + name="delete_task", + description="DELETE /api/tasks/{task_id} - Delete Task", + tags=('Tasks',), + timeout_seconds=120, + ) + async def delete_task( + self, + ctx: RunContext, + parameters: dict[str, Any] | None = None, + body: Any | None = None, + ) -> dict[str, Any]: + return await self._request( + ctx, + "delete_task", + parameters=parameters or {}, + body=body, + ) + + def _operation_tools(self, ctx: RunContext) -> list[StructuredTool]: + tools: list[StructuredTool] = [] + for operation_id, operation in OPERATIONS.items(): + async def call( + parameters: dict[str, Any] | None = None, + body: Any | None = None, + *, + _operation_id: str = operation_id, + ) -> dict[str, Any]: + return await self._request( + ctx, + _operation_id, + parameters=parameters or {}, + body=body, + ) + + tools.append( + StructuredTool.from_function( + coroutine=call, + name=operation["skill_name"], + description=self._tool_description(operation), + args_schema=OperationInput, + ) + ) + return tools + + def _system_prompt(self) -> str: + lines = [ + "You operate an API through generated OpenAPI tools.", + "Call tools to get real results. Do not invent API responses.", + "For write, update, or delete operations, explain the intended action before calling the tool.", + "If an operation reports missing consumer setup, tell the user which setup field is required.", + "", + "Available operations:", + ] + for operation in OPERATIONS.values(): + marker = "WRITE" if operation.get("destructive") else "READ" + lines.append( + f"- {operation['skill_name']}: {marker} {operation['method']} {operation['path']} — {operation['summary']}" + ) + return "\n".join(lines) + + def _tool_description(self, operation: dict[str, Any]) -> str: + parts = [ + f"{operation['method']} {operation['path']}", + str(operation.get("description") or operation.get("summary") or ""), + ] + if operation.get("parameters"): + parts.append( + "Parameters: " + + ", ".join( + f"{p['name']} in {p['in']}{' required' if p.get('required') else ''}" + for p in operation["parameters"] + ) + ) + if operation.get("request_body") is not None: + parts.append("Accepts a JSON request body.") + return "\n".join(part for part in parts if part) + + async def _request( + self, + ctx: RunContext, + operation_id: str, + *, + parameters: dict[str, Any], + body: Any | None, + ) -> dict[str, Any]: + operation = OPERATIONS[operation_id] + base_url = str(ctx.consumer_config("OPENAPI_BASE_URL", DEFAULT_BASE_URL) or DEFAULT_BASE_URL).rstrip("/") + url, query, headers = self._request_parts(ctx, operation, parameters) + request_kwargs: dict[str, Any] = { + "params": query, + "headers": headers, + } + if body is not None: + request_kwargs["json"] = body + async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client: + response = await client.request( + operation["method"], + f"{base_url}{url}", + **request_kwargs, + ) + content_type = response.headers.get("content-type", "") + try: + payload: Any = response.json() if "json" in content_type else response.text + except ValueError: + payload = response.text + if response.status_code >= 400: + return { + "ok": False, + "status_code": response.status_code, + "operation_id": operation_id, + "error": payload, + } + return { + "ok": True, + "status_code": response.status_code, + "operation_id": operation_id, + "result": payload, + } + + def _request_parts( + self, + ctx: RunContext, + operation: dict[str, Any], + parameters: dict[str, Any], + ) -> tuple[str, dict[str, Any], dict[str, str]]: + path = operation["path"] + query: dict[str, Any] = {} + headers: dict[str, str] = {} + cookies: dict[str, Any] = {} + for param in operation.get("parameters") or []: + name = param["name"] + if name not in parameters: + if param.get("required"): + raise ValueError(f"missing required parameter {name!r}") + continue + value = parameters[name] + location = param["in"] + if location == "path": + path = path.replace("{" + name + "}", str(value)) + elif location == "query": + query[name] = value + elif location == "header": + headers[name] = str(value) + elif location == "cookie": + cookies[name] = value + unresolved = PATH_PARAMETER_RE.findall(path) + if unresolved: + missing = ", ".join(sorted(set(unresolved))) + raise ValueError("missing required path parameter(s): " + missing) + if cookies: + headers["cookie"] = "; ".join(f"{key}={value}" for key, value in cookies.items()) + auth_headers, auth_query = self._auth_for_operation(ctx, operation) + headers.update(auth_headers) + query.update(auth_query) + return path, query, headers + + def _auth_for_operation(self, ctx: RunContext, operation: dict[str, Any]) -> tuple[dict[str, str], dict[str, Any]]: + requirements = operation.get("security") + if requirements is None: + requirements = ROOT_SECURITY + if requirements == []: + return {}, {} + missing: list[str] = [] + for requirement in requirements or []: + if not isinstance(requirement, dict) or not requirement: + return {}, {} + headers: dict[str, str] = {} + query: dict[str, Any] = {} + ok = True + for scheme_name in requirement: + mapping = SECURITY_FIELDS.get(scheme_name) or {} + secret_name = mapping.get("field") + value = _consumer_secret_optional(ctx, secret_name) if secret_name else "" + if not value: + ok = False + if secret_name: + missing.append(secret_name) + continue + kind = mapping.get("kind") + if kind == "apiKey": + prefix = mapping.get("prefix") or "" + sent_value = f"{prefix}{value}" + if mapping.get("location") == "query": + query[mapping.get("name") or scheme_name] = sent_value + else: + headers[mapping.get("name") or scheme_name] = sent_value + elif kind in {"http", "oauth2"}: + scheme = mapping.get("scheme") or "Bearer" + if scheme.lower() == "basic": + token = base64.b64encode(value.encode("utf-8")).decode("ascii") + headers["authorization"] = f"Basic {token}" + else: + headers["authorization"] = f"{scheme} {value}" + else: + ok = False + if ok: + return headers, query + if missing: + raise ConsumerSetupMissing( + "operation requires consumer setup secret(s): " + + ", ".join(sorted(set(missing))) + ) + return {}, {} + + +def _consumer_secret_optional(ctx: RunContext, name: str | None) -> str: + if not name: + return "" + try: + return ctx.consumer_secret(name) + except ConsumerSetupMissing: + return "" + + +def _message_text(message: Any) -> Any: + content = getattr(message, "content", message) + return content + + +def _message_to_dict(message: Any) -> dict[str, Any]: + if isinstance(message, BaseMessage): + return message.model_dump(mode="json") + if hasattr(message, "model_dump"): + return message.model_dump(mode="json") + if isinstance(message, dict): + return message + return {"content": str(message)} + + +agent = TasksApi() diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..b68229d --- /dev/null +++ b/openapi.json @@ -0,0 +1,741 @@ +{ + "components": { + "schemas": { + "DeleteOut": { + "properties": { + "ok": { + "const": true, + "default": true, + "title": "Ok", + "type": "boolean" + } + }, + "title": "DeleteOut", + "type": "object" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array" + } + }, + "title": "HTTPValidationError", + "type": "object" + }, + "HealthError": { + "properties": { + "error": { + "title": "Error", + "type": "string" + }, + "ok": { + "const": false, + "default": false, + "title": "Ok", + "type": "boolean" + } + }, + "required": [ + "error" + ], + "title": "HealthError", + "type": "object" + }, + "HealthOk": { + "properties": { + "ok": { + "const": true, + "default": true, + "title": "Ok", + "type": "boolean" + } + }, + "title": "HealthOk", + "type": "object" + }, + "TaskCreate": { + "properties": { + "description": { + "default": "", + "maxLength": 10000, + "title": "Description", + "type": "string" + }, + "due_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Due At" + }, + "labels": { + "items": { + "type": "string" + }, + "maxItems": 20, + "title": "Labels", + "type": "array" + }, + "priority": { + "default": 0, + "maximum": 100.0, + "minimum": -100.0, + "title": "Priority", + "type": "integer" + }, + "status": { + "default": "todo", + "enum": [ + "todo", + "in_progress", + "done", + "canceled" + ], + "title": "Status", + "type": "string" + }, + "title": { + "maxLength": 240, + "minLength": 1, + "title": "Title", + "type": "string" + } + }, + "required": [ + "title" + ], + "title": "TaskCreate", + "type": "object" + }, + "TaskListOut": { + "properties": { + "limit": { + "title": "Limit", + "type": "integer" + }, + "offset": { + "title": "Offset", + "type": "integer" + }, + "tasks": { + "items": { + "$ref": "#/components/schemas/TaskOut" + }, + "title": "Tasks", + "type": "array" + }, + "total": { + "title": "Total", + "type": "integer" + } + }, + "required": [ + "tasks", + "total", + "limit", + "offset" + ], + "title": "TaskListOut", + "type": "object" + }, + "TaskOut": { + "properties": { + "completed_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Completed At" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + }, + "due_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Due At" + }, + "id": { + "title": "Id", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "priority": { + "title": "Priority", + "type": "integer" + }, + "status": { + "enum": [ + "todo", + "in_progress", + "done", + "canceled" + ], + "title": "Status", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "updated_at": { + "format": "date-time", + "title": "Updated At", + "type": "string" + } + }, + "required": [ + "id", + "title", + "description", + "status", + "priority", + "labels", + "due_at", + "completed_at", + "created_at", + "updated_at" + ], + "title": "TaskOut", + "type": "object" + }, + "TaskUpdate": { + "properties": { + "description": { + "anyOf": [ + { + "maxLength": 10000, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "due_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Due At" + }, + "labels": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 20, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Labels" + }, + "priority": { + "anyOf": [ + { + "maximum": 100.0, + "minimum": -100.0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Priority" + }, + "status": { + "anyOf": [ + { + "enum": [ + "todo", + "in_progress", + "done", + "canceled" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + }, + "title": { + "anyOf": [ + { + "maxLength": 240, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + } + }, + "title": "TaskUpdate", + "type": "object" + }, + "ValidationError": { + "properties": { + "ctx": { + "title": "Context", + "type": "object" + }, + "input": { + "title": "Input" + }, + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "title": "Location", + "type": "array" + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + }, + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError", + "type": "object" + } + }, + "securitySchemes": { + "apiKeyAuth": { + "description": "Use the configured `TASKS_API_KEY` value.", + "in": "header", + "name": "x-api-key", + "type": "apiKey" + }, + "bearerAuth": { + "description": "Use `Authorization: Bearer $TASKS_API_KEY`.", + "scheme": "bearer", + "type": "http" + } + } + }, + "info": { + "description": "API-only service for tracking tasks in Postgres.", + "title": "Tasks API", + "version": "0.1.0" + }, + "openapi": "3.1.0", + "paths": { + "/api/tasks": { + "get": { + "operationId": "listTasks", + "parameters": [ + { + "in": "query", + "name": "status", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "todo", + "in_progress", + "done", + "canceled" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "anyOf": [ + { + "maxLength": 120, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Q" + } + }, + { + "in": "query", + "name": "due_before", + "required": false, + "schema": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Due Before" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "maximum": 200, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "title": "Offset", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskListOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "summary": "List Tasks", + "tags": [ + "Tasks" + ] + }, + "post": { + "operationId": "createTask", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "summary": "Create Task", + "tags": [ + "Tasks" + ] + } + }, + "/api/tasks/{task_id}": { + "delete": { + "operationId": "deleteTask", + "parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "summary": "Delete Task", + "tags": [ + "Tasks" + ] + }, + "get": { + "operationId": "getTask", + "parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "summary": "Get Task", + "tags": [ + "Tasks" + ] + }, + "patch": { + "operationId": "updateTask", + "parameters": [ + { + "in": "path", + "name": "task_id", + "required": true, + "schema": { + "title": "Task Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskOut" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "summary": "Update Task", + "tags": [ + "Tasks" + ] + } + }, + "/healthz": { + "get": { + "operationId": "getTasksHealth", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthOk" + } + } + }, + "description": "Successful Response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthError" + } + } + }, + "description": "Service Unavailable" + } + }, + "summary": "Healthz", + "tags": [ + "Health" + ] + } + } + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..086fbda --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +# a2a-pack is installed by the platform base image. +deepagents>=0.5.0 +langchain>=0.3 +langchain-openai>=0.2 +langchain-core>=0.3 +langgraph>=0.6 +httpx>=0.27