aptdata 0.0.2__tar.gz → 0.1.0__tar.gz

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 (98) hide show
  1. {aptdata-0.0.2 → aptdata-0.1.0}/PKG-INFO +23 -4
  2. {aptdata-0.0.2 → aptdata-0.1.0}/README.md +20 -2
  3. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/__init__.py +1 -1
  4. aptdata-0.1.0/aptdata/agents/__init__.py +49 -0
  5. aptdata-0.1.0/aptdata/agents/base.py +158 -0
  6. aptdata-0.1.0/aptdata/agents/cli_agents.py +99 -0
  7. aptdata-0.1.0/aptdata/agents/openclaw.py +95 -0
  8. aptdata-0.1.0/aptdata/agents/project.py +180 -0
  9. aptdata-0.1.0/aptdata/agents/registry.py +123 -0
  10. aptdata-0.1.0/aptdata/agents/router.py +201 -0
  11. aptdata-0.1.0/aptdata/analytics/__init__.py +18 -0
  12. aptdata-0.1.0/aptdata/analytics/clusters.py +49 -0
  13. aptdata-0.1.0/aptdata/analytics/dedup.py +44 -0
  14. aptdata-0.1.0/aptdata/analytics/indicators.py +598 -0
  15. aptdata-0.1.0/aptdata/analytics/stats.py +65 -0
  16. aptdata-0.1.0/aptdata/analytics/tfidf.py +42 -0
  17. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/app.py +4 -0
  18. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/commands/__init__.py +11 -1
  19. aptdata-0.1.0/aptdata/cli/commands/agents_cmd.py +180 -0
  20. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/commands/mesh_cmd.py +21 -27
  21. aptdata-0.1.0/aptdata/cli/commands/project_cmd.py +108 -0
  22. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/core/__init__.py +10 -2
  23. aptdata-0.1.0/aptdata/core/context.py +95 -0
  24. aptdata-0.1.0/aptdata/core/dataset.py +141 -0
  25. aptdata-0.1.0/aptdata/core/decorators.py +140 -0
  26. aptdata-0.1.0/aptdata/core/events.py +104 -0
  27. aptdata-0.1.0/aptdata/core/registry.py +31 -0
  28. aptdata-0.1.0/aptdata/core/system.py +655 -0
  29. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/core/workflow.py +16 -33
  30. aptdata-0.1.0/aptdata/core/yaml_builder.py +126 -0
  31. aptdata-0.1.0/aptdata/gamification/__init__.py +21 -0
  32. aptdata-0.1.0/aptdata/gamification/streak.py +85 -0
  33. aptdata-0.1.0/aptdata/gamification/xp.py +101 -0
  34. aptdata-0.1.0/aptdata/habits/__init__.py +22 -0
  35. aptdata-0.1.0/aptdata/habits/quickwins.py +146 -0
  36. aptdata-0.1.0/aptdata/integrations/__init__.py +1 -0
  37. aptdata-0.1.0/aptdata/integrations/calendar.py +211 -0
  38. aptdata-0.1.0/aptdata/mcp/server.py +380 -0
  39. aptdata-0.1.0/aptdata/observability/__init__.py +14 -0
  40. aptdata-0.1.0/aptdata/observability/llm_observer.py +156 -0
  41. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/dataset.py +10 -4
  42. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/transform/pandas.py +57 -1
  43. aptdata-0.1.0/aptdata/telemetry/__init__.py +6 -0
  44. aptdata-0.1.0/aptdata/telemetry/provider.py +28 -0
  45. {aptdata-0.0.2 → aptdata-0.1.0}/pyproject.toml +10 -4
  46. aptdata-0.0.2/aptdata/core/context.py +0 -31
  47. aptdata-0.0.2/aptdata/core/dataset.py +0 -39
  48. aptdata-0.0.2/aptdata/core/system.py +0 -317
  49. aptdata-0.0.2/aptdata/mcp/server.py +0 -198
  50. aptdata-0.0.2/aptdata/telemetry/__init__.py +0 -5
  51. {aptdata-0.0.2 → aptdata-0.1.0}/LICENSE +0 -0
  52. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/__init__.py +0 -0
  53. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/commands/config_cmd.py +0 -0
  54. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/commands/plugin_cmd.py +0 -0
  55. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/commands/system_cmd.py +0 -0
  56. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/commands/telemetry_cmd.py +0 -0
  57. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/completions.py +0 -0
  58. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/interactive.py +0 -0
  59. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/rendering/__init__.py +0 -0
  60. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/rendering/console.py +0 -0
  61. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/rendering/logger.py +0 -0
  62. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/rendering/panels.py +0 -0
  63. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/rendering/tables.py +0 -0
  64. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/cli/scaffold.py +0 -0
  65. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/config/__init__.py +0 -0
  66. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/config/parser.py +0 -0
  67. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/config/schema.py +0 -0
  68. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/config/secrets.py +0 -0
  69. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/core/lineage.py +0 -0
  70. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/core/state.py +0 -0
  71. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/mcp/__init__.py +0 -0
  72. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/__init__.py +0 -0
  73. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/ai/__init__.py +0 -0
  74. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/ai/chunking.py +0 -0
  75. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/ai/embeddings.py +0 -0
  76. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/base.py +0 -0
  77. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/governance/__init__.py +0 -0
  78. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/governance/catalog.py +0 -0
  79. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/governance/classification.py +0 -0
  80. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/governance/lineage_store.py +0 -0
  81. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/governance/rules.py +0 -0
  82. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/local_fs.py +0 -0
  83. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/manager.py +0 -0
  84. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/postgres.py +0 -0
  85. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/quality/__init__.py +0 -0
  86. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/quality/contract.py +0 -0
  87. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/quality/expectations.py +0 -0
  88. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/quality/report.py +0 -0
  89. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/quality/validator.py +0 -0
  90. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/rest.py +0 -0
  91. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/transform/__init__.py +0 -0
  92. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/transform/spark.py +0 -0
  93. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/vector/__init__.py +0 -0
  94. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/vector/base.py +0 -0
  95. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/plugins/vector/qdrant.py +0 -0
  96. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/telemetry/instrumentation.py +0 -0
  97. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/tui/__init__.py +0 -0
  98. {aptdata-0.0.2 → aptdata-0.1.0}/aptdata/tui/monitor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aptdata
3
- Version: 0.0.2
3
+ Version: 0.1.0
4
4
  Summary: A declarative, extensible framework for building smart data pipelines in Python
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -17,12 +17,13 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
20
+ Provides-Extra: ai
20
21
  Provides-Extra: all
21
22
  Provides-Extra: pandas
22
23
  Provides-Extra: plugins
23
24
  Provides-Extra: spark
24
25
  Requires-Dist: httpx (>=0.27,<0.28) ; extra == "plugins" or extra == "all"
25
- Requires-Dist: mcp (>=1.26.0,<2.0.0)
26
+ Requires-Dist: mcp (>=1.26.0,<2.0.0) ; extra == "ai" or extra == "all"
26
27
  Requires-Dist: opentelemetry-api (>=1.40.0,<2.0.0)
27
28
  Requires-Dist: opentelemetry-sdk (>=1.40.0,<2.0.0)
28
29
  Requires-Dist: pandas (>=2.2,<3.0) ; extra == "pandas" or extra == "all"
@@ -44,11 +45,11 @@ Description-Content-Type: text/markdown
44
45
 
45
46
  # aptdata
46
47
 
47
- > **v0.0.2** · A declarative, extensible framework for building smart data pipelines in Python.
48
+ > **v0.0.3** · A declarative, extensible framework for building smart data pipelines in Python.
48
49
 
49
50
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
50
51
  [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
51
- [![Version](https://img.shields.io/badge/version-0.0.2-orange)](CHANGELOG.md)
52
+ [![Version](https://img.shields.io/badge/version-0.0.3-orange)](CHANGELOG.md)
52
53
 
53
54
  ---
54
55
 
@@ -95,6 +96,7 @@ pip install aptdata
95
96
  pip install aptdata[pandas] # pandas support
96
97
  pip install aptdata[spark] # PySpark support
97
98
  pip install aptdata[plugins] # REST, PostgreSQL, Parquet I/O
99
+ pip install aptdata[ai] # MCP server for AI agents
98
100
  pip install aptdata[all] # everything
99
101
  ```
100
102
 
@@ -273,6 +275,23 @@ See [Governance docs](docs/governance.md) for the full API.
273
275
 
274
276
  ---
275
277
 
278
+ ## AI Agents & MCP Server
279
+
280
+ aptdata ships with a built-in [Model Context Protocol](https://modelcontextprotocol.io/) server (`mcp-start`). This transforms AI assistants (like Claude, Copilot, or Devin) into autonomous data engineers with direct access to:
281
+
282
+ - **Pipeline Execution:** Trigger and monitor data flows (`run_flow`).
283
+ - **Data Quality:** Audit the latest quality test results (`quality://reports/...`).
284
+ - **Data Governance:** Read business rules to prevent violations (`governance://rules`).
285
+ - **Lineage:** Trace upstream dependencies and column-level provenance (`get_pipeline_lineage`).
286
+
287
+ ```bash
288
+ aptdata mcp-start --transport stdio
289
+ ```
290
+
291
+ See the [MCP Documentation](docs/mcp.md) for setup instructions.
292
+
293
+ ---
294
+
276
295
  ## Release process
277
296
 
278
297
  Releases are automated via the [Release workflow](.github/workflows/release.yml).
@@ -1,10 +1,10 @@
1
1
  # aptdata
2
2
 
3
- > **v0.0.2** · A declarative, extensible framework for building smart data pipelines in Python.
3
+ > **v0.0.3** · A declarative, extensible framework for building smart data pipelines in Python.
4
4
 
5
5
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
- [![Version](https://img.shields.io/badge/version-0.0.2-orange)](CHANGELOG.md)
7
+ [![Version](https://img.shields.io/badge/version-0.0.3-orange)](CHANGELOG.md)
8
8
 
9
9
  ---
10
10
 
@@ -51,6 +51,7 @@ pip install aptdata
51
51
  pip install aptdata[pandas] # pandas support
52
52
  pip install aptdata[spark] # PySpark support
53
53
  pip install aptdata[plugins] # REST, PostgreSQL, Parquet I/O
54
+ pip install aptdata[ai] # MCP server for AI agents
54
55
  pip install aptdata[all] # everything
55
56
  ```
56
57
 
@@ -229,6 +230,23 @@ See [Governance docs](docs/governance.md) for the full API.
229
230
 
230
231
  ---
231
232
 
233
+ ## AI Agents & MCP Server
234
+
235
+ aptdata ships with a built-in [Model Context Protocol](https://modelcontextprotocol.io/) server (`mcp-start`). This transforms AI assistants (like Claude, Copilot, or Devin) into autonomous data engineers with direct access to:
236
+
237
+ - **Pipeline Execution:** Trigger and monitor data flows (`run_flow`).
238
+ - **Data Quality:** Audit the latest quality test results (`quality://reports/...`).
239
+ - **Data Governance:** Read business rules to prevent violations (`governance://rules`).
240
+ - **Lineage:** Trace upstream dependencies and column-level provenance (`get_pipeline_lineage`).
241
+
242
+ ```bash
243
+ aptdata mcp-start --transport stdio
244
+ ```
245
+
246
+ See the [MCP Documentation](docs/mcp.md) for setup instructions.
247
+
248
+ ---
249
+
232
250
  ## Release process
233
251
 
234
252
  Releases are automated via the [Release workflow](.github/workflows/release.yml).
@@ -1,3 +1,3 @@
1
1
  """aptdata: A framework for smart data pipelines."""
2
2
 
3
- __version__ = "0.0.2"
3
+ __version__ = "0.1.0"
@@ -0,0 +1,49 @@
1
+ """aptdata.agents — uniform interface over heterogeneous agent backends.
2
+
3
+ The single source of truth for the multi-agent ecosystem: one declarative
4
+ registry (``agents.yaml``), one :class:`IAgent` contract, one adapter per
5
+ backend kind.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from aptdata.agents.base import (
11
+ AgentHealth,
12
+ AgentResponse,
13
+ AgentSpec,
14
+ BaseAgent,
15
+ IAgent,
16
+ )
17
+ from aptdata.agents.cli_agents import ClaudeCodeAgent, CLIAgent, OpenCodeAgent
18
+ from aptdata.agents.openclaw import OpenClawAgent
19
+ from aptdata.agents.project import (
20
+ Project,
21
+ ProjectRunner,
22
+ Task,
23
+ TaskResult,
24
+ scaffold_project,
25
+ )
26
+ from aptdata.agents.registry import ADAPTERS, AgentRegistry
27
+ from aptdata.agents.router import RouteDecision, Router, Skill
28
+
29
+ __all__ = [
30
+ "ADAPTERS",
31
+ "AgentHealth",
32
+ "AgentRegistry",
33
+ "AgentResponse",
34
+ "AgentSpec",
35
+ "BaseAgent",
36
+ "CLIAgent",
37
+ "ClaudeCodeAgent",
38
+ "IAgent",
39
+ "OpenClawAgent",
40
+ "OpenCodeAgent",
41
+ "Project",
42
+ "ProjectRunner",
43
+ "RouteDecision",
44
+ "Router",
45
+ "Skill",
46
+ "Task",
47
+ "TaskResult",
48
+ "scaffold_project",
49
+ ]
@@ -0,0 +1,158 @@
1
+ """Agents — a uniform interface over heterogeneous AI/automation backends.
2
+
3
+ This module lets aptdata talk to many kinds of agent backends (OpenClaw
4
+ workers, OpenCode, Claude Code, plain HTTP APIs) through a single, stable
5
+ contract, mirroring the :mod:`aptdata.integrations` pattern.
6
+
7
+ * :class:`AgentSpec` — declarative description of one backend (loaded from
8
+ ``agents.yaml``); the single source of truth that replaces the three
9
+ divergent registries (multiverso.json, plugin schema.yaml, app.py).
10
+ * :class:`IAgent` / :class:`BaseAgent` — the interface every adapter
11
+ implements, following the project's ``IXxx`` -> ``BaseXxx`` convention.
12
+ * :class:`AgentResponse` — a machine-readable result, JSON-friendly like the
13
+ rest of aptdata's outputs.
14
+
15
+ Design goals
16
+ ------------
17
+ * **Single source of truth** — one registry, many consumers.
18
+ * **Adapter per backend** — each backend's quirks are isolated in one class.
19
+ * **No global state** — specs are passed in explicitly at construction.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ from abc import ABC, abstractmethod
26
+ from dataclasses import dataclass, field
27
+ from enum import Enum
28
+ from typing import Any
29
+
30
+ from pydantic import BaseModel, ConfigDict, Field
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Declarative spec — the single source of truth
37
+ # ---------------------------------------------------------------------------
38
+
39
+
40
+ class AgentSpec(BaseModel):
41
+ """Declarative description of a single agent backend.
42
+
43
+ Loaded from ``agents.yaml``. Carries enough metadata for the registry to
44
+ route to it and for an adapter to reach it, without any consumer needing
45
+ to know how the backend works internally.
46
+ """
47
+
48
+ model_config = ConfigDict(extra="forbid")
49
+
50
+ id: str
51
+ name: str
52
+ type: str # adapter kind: openclaw | opencode | claude_code | http
53
+ location: str = "local" # local | vps
54
+ enabled: bool = True
55
+ role: str = "worker"
56
+ handle: str | None = None # Telegram handle, if any
57
+ model: str | None = None
58
+ capabilities: list[str] = Field(default_factory=list)
59
+
60
+ # Transport (optional — only some adapters use these)
61
+ host: str | None = None
62
+ port: int | None = None
63
+ endpoint: str | None = None
64
+ timeout_ms: int = 30000
65
+ weight: int = 5
66
+
67
+ note: str = ""
68
+
69
+ @property
70
+ def base_url(self) -> str | None:
71
+ """HTTP base URL for adapters that speak over the network."""
72
+ if self.host and self.port:
73
+ return f"http://{self.host}:{self.port}"
74
+ return None
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Status + response value objects
79
+ # ---------------------------------------------------------------------------
80
+
81
+
82
+ class AgentHealth(str, Enum):
83
+ """Coarse liveness of a backend."""
84
+
85
+ UP = "up"
86
+ DOWN = "down"
87
+ DISABLED = "disabled"
88
+ UNKNOWN = "unknown"
89
+
90
+
91
+ @dataclass
92
+ class AgentResponse:
93
+ """Result of sending a prompt to an agent.
94
+
95
+ JSON-serialisable so it fits aptdata's machine-readable output contract.
96
+ """
97
+
98
+ ok: bool
99
+ agent_id: str
100
+ text: str = ""
101
+ error: str | None = None
102
+ raw: dict[str, Any] = field(default_factory=dict)
103
+
104
+ def to_dict(self) -> dict[str, Any]:
105
+ return {
106
+ "ok": self.ok,
107
+ "agent_id": self.agent_id,
108
+ "text": self.text,
109
+ "error": self.error,
110
+ }
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Interface + base
115
+ # ---------------------------------------------------------------------------
116
+
117
+
118
+ class IAgent(ABC):
119
+ """Uniform interface every backend adapter implements."""
120
+
121
+ @abstractmethod
122
+ def send(self, prompt: str, **kwargs: Any) -> AgentResponse:
123
+ """Send a prompt/instruction and return the backend's reply."""
124
+ raise NotImplementedError
125
+
126
+ @abstractmethod
127
+ def health(self) -> AgentHealth:
128
+ """Report coarse liveness without side effects on the conversation."""
129
+ raise NotImplementedError
130
+
131
+
132
+ class BaseAgent(IAgent):
133
+ """Common behaviour shared by adapters; holds the declarative spec."""
134
+
135
+ #: adapter kind this class handles; subclasses override.
136
+ type: str = "base"
137
+
138
+ def __init__(self, spec: AgentSpec) -> None:
139
+ self.spec = spec
140
+
141
+ @property
142
+ def id(self) -> str:
143
+ return self.spec.id
144
+
145
+ @property
146
+ def capabilities(self) -> list[str]:
147
+ return self.spec.capabilities
148
+
149
+ def supports(self, capability: str) -> bool:
150
+ return capability in self.spec.capabilities
151
+
152
+ def health(self) -> AgentHealth:
153
+ if not self.spec.enabled:
154
+ return AgentHealth.DISABLED
155
+ return AgentHealth.UNKNOWN
156
+
157
+ def __repr__(self) -> str: # pragma: no cover - cosmetic
158
+ return f"<{self.__class__.__name__} id={self.spec.id!r} type={self.type!r}>"
@@ -0,0 +1,99 @@
1
+ """CLI-backed agent adapters — Claude Code and OpenCode.
2
+
3
+ Unlike OpenClaw (which speaks a gateway protocol), these backends are driven
4
+ through their command-line interface as stateless one-shot dispatches:
5
+
6
+ * :class:`ClaudeCodeAgent` — ``claude -p "<prompt>"`` (print mode).
7
+ * :class:`OpenCodeAgent` — ``opencode run "<prompt>"``.
8
+
9
+ Each invocation spawns a fresh session (no shared conversation), which keeps
10
+ dispatch stateless and safe. The prompt is always passed as an argv element
11
+ (never through a shell) so its content can't inject commands.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import shutil
18
+ import subprocess
19
+ from typing import Any
20
+
21
+ from aptdata.agents.base import AgentHealth, AgentResponse, BaseAgent
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class CLIAgent(BaseAgent):
27
+ """Common machinery for agents driven by a local CLI binary."""
28
+
29
+ #: the executable this adapter shells out to; subclasses set it.
30
+ binary: str = ""
31
+
32
+ def _build_command(self, prompt: str) -> list[str]:
33
+ """Return the argv list to run for *prompt*. Subclasses implement."""
34
+ raise NotImplementedError
35
+
36
+ def send(self, prompt: str, **kwargs: Any) -> AgentResponse:
37
+ if not self.spec.enabled:
38
+ return AgentResponse(ok=False, agent_id=self.id, error="agent disabled")
39
+ if shutil.which(self.binary) is None:
40
+ return AgentResponse(
41
+ ok=False, agent_id=self.id, error=f"'{self.binary}' not found in PATH"
42
+ )
43
+
44
+ cmd = self._build_command(prompt)
45
+ try:
46
+ proc = subprocess.run(
47
+ cmd,
48
+ capture_output=True,
49
+ text=True,
50
+ timeout=self.spec.timeout_ms / 1000,
51
+ check=False,
52
+ )
53
+ except subprocess.TimeoutExpired:
54
+ return AgentResponse(
55
+ ok=False, agent_id=self.id, error="timeout"
56
+ )
57
+ except OSError as exc: # pragma: no cover - defensive
58
+ return AgentResponse(ok=False, agent_id=self.id, error=str(exc))
59
+
60
+ if proc.returncode != 0:
61
+ return AgentResponse(
62
+ ok=False,
63
+ agent_id=self.id,
64
+ error=(proc.stderr or proc.stdout or "non-zero exit").strip()[:500],
65
+ )
66
+ return AgentResponse(
67
+ ok=True, agent_id=self.id, text=proc.stdout.strip()
68
+ )
69
+
70
+ def health(self) -> AgentHealth:
71
+ if not self.spec.enabled:
72
+ return AgentHealth.DISABLED
73
+ return AgentHealth.UP if shutil.which(self.binary) else AgentHealth.DOWN
74
+
75
+
76
+ class ClaudeCodeAgent(CLIAgent):
77
+ """Dispatches to Claude Code via ``claude -p``."""
78
+
79
+ type = "claude_code"
80
+ binary = "claude"
81
+
82
+ def _build_command(self, prompt: str) -> list[str]:
83
+ cmd = ["claude", "-p", prompt]
84
+ if self.spec.model:
85
+ cmd += ["--model", self.spec.model]
86
+ return cmd
87
+
88
+
89
+ class OpenCodeAgent(CLIAgent):
90
+ """Dispatches to OpenCode via ``opencode run``."""
91
+
92
+ type = "opencode"
93
+ binary = "opencode"
94
+
95
+ def _build_command(self, prompt: str) -> list[str]:
96
+ cmd = ["opencode", "run", prompt]
97
+ if self.spec.model:
98
+ cmd += ["-m", self.spec.model]
99
+ return cmd
@@ -0,0 +1,95 @@
1
+ """OpenClaw backend adapter.
2
+
3
+ OpenClaw workers (Zeca, Ondina, Maresia, Hermez, ...) expose an HTTP chat
4
+ endpoint (default ``/api/chat`` on ports 48330-48333). This adapter isolates
5
+ that transport so the rest of aptdata only ever sees :class:`IAgent`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any
12
+
13
+ from aptdata.agents.base import AgentHealth, AgentResponse, BaseAgent
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class OpenClawAgent(BaseAgent):
19
+ """Talks to an OpenClaw worker over HTTP."""
20
+
21
+ type = "openclaw"
22
+
23
+ def _endpoint(self) -> str | None:
24
+ base = self.spec.base_url
25
+ if not base:
26
+ return None
27
+ path = self.spec.endpoint or "/api/chat"
28
+ return f"{base}{path}"
29
+
30
+ def send(self, prompt: str, **kwargs: Any) -> AgentResponse:
31
+ if not self.spec.enabled:
32
+ return AgentResponse(
33
+ ok=False, agent_id=self.id, error="agent disabled"
34
+ )
35
+
36
+ url = self._endpoint()
37
+ if not url:
38
+ return AgentResponse(
39
+ ok=False,
40
+ agent_id=self.id,
41
+ error="no host/port configured for OpenClaw agent",
42
+ )
43
+
44
+ import requests # lazy: keeps core import-light, mirrors integrations/
45
+
46
+ payload = {"message": prompt, **kwargs}
47
+ try:
48
+ resp = requests.post(
49
+ url, json=payload, timeout=self.spec.timeout_ms / 1000
50
+ )
51
+ except requests.RequestException as exc:
52
+ logger.warning("OpenClaw %s unreachable: %s", self.id, exc)
53
+ return AgentResponse(ok=False, agent_id=self.id, error=str(exc))
54
+
55
+ if resp.status_code != 200:
56
+ return AgentResponse(
57
+ ok=False,
58
+ agent_id=self.id,
59
+ error=f"HTTP {resp.status_code}",
60
+ raw={"status": resp.status_code, "body": resp.text[:500]},
61
+ )
62
+
63
+ data = self._parse(resp)
64
+ text = self._extract_text(data)
65
+ return AgentResponse(ok=True, agent_id=self.id, text=text, raw=data)
66
+
67
+ @staticmethod
68
+ def _parse(resp: Any) -> dict[str, Any]:
69
+ try:
70
+ data = resp.json()
71
+ return data if isinstance(data, dict) else {"value": data}
72
+ except ValueError:
73
+ return {"text": resp.text}
74
+
75
+ @staticmethod
76
+ def _extract_text(data: dict[str, Any]) -> str:
77
+ for key in ("reply", "response", "message", "text", "output"):
78
+ value = data.get(key)
79
+ if isinstance(value, str):
80
+ return value
81
+ return ""
82
+
83
+ def health(self) -> AgentHealth:
84
+ if not self.spec.enabled:
85
+ return AgentHealth.DISABLED
86
+ url = self.spec.base_url
87
+ if not url:
88
+ return AgentHealth.UNKNOWN
89
+ import requests
90
+
91
+ try:
92
+ resp = requests.get(url, timeout=3)
93
+ return AgentHealth.UP if resp.status_code < 500 else AgentHealth.DOWN
94
+ except requests.RequestException:
95
+ return AgentHealth.DOWN