aptdata 0.0.3__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.
- {aptdata-0.0.3 → aptdata-0.1.0}/PKG-INFO +3 -3
- {aptdata-0.0.3 → aptdata-0.1.0}/README.md +2 -2
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/__init__.py +1 -1
- aptdata-0.1.0/aptdata/agents/__init__.py +49 -0
- aptdata-0.1.0/aptdata/agents/base.py +158 -0
- aptdata-0.1.0/aptdata/agents/cli_agents.py +99 -0
- aptdata-0.1.0/aptdata/agents/openclaw.py +95 -0
- aptdata-0.1.0/aptdata/agents/project.py +180 -0
- aptdata-0.1.0/aptdata/agents/registry.py +123 -0
- aptdata-0.1.0/aptdata/agents/router.py +201 -0
- aptdata-0.1.0/aptdata/analytics/__init__.py +18 -0
- aptdata-0.1.0/aptdata/analytics/clusters.py +49 -0
- aptdata-0.1.0/aptdata/analytics/dedup.py +44 -0
- aptdata-0.1.0/aptdata/analytics/indicators.py +598 -0
- aptdata-0.1.0/aptdata/analytics/stats.py +65 -0
- aptdata-0.1.0/aptdata/analytics/tfidf.py +42 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/app.py +4 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/__init__.py +11 -1
- aptdata-0.1.0/aptdata/cli/commands/agents_cmd.py +180 -0
- aptdata-0.1.0/aptdata/cli/commands/project_cmd.py +108 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/dataset.py +27 -7
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/system.py +88 -1
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/workflow.py +16 -33
- aptdata-0.1.0/aptdata/gamification/__init__.py +21 -0
- aptdata-0.1.0/aptdata/gamification/streak.py +85 -0
- aptdata-0.1.0/aptdata/gamification/xp.py +101 -0
- aptdata-0.1.0/aptdata/habits/__init__.py +22 -0
- aptdata-0.1.0/aptdata/habits/quickwins.py +146 -0
- aptdata-0.1.0/aptdata/integrations/__init__.py +1 -0
- aptdata-0.1.0/aptdata/integrations/calendar.py +211 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/mcp/server.py +68 -0
- aptdata-0.1.0/aptdata/observability/__init__.py +14 -0
- aptdata-0.1.0/aptdata/observability/llm_observer.py +156 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/pyproject.toml +1 -1
- {aptdata-0.0.3 → aptdata-0.1.0}/LICENSE +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/config_cmd.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/mesh_cmd.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/plugin_cmd.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/system_cmd.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/telemetry_cmd.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/completions.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/interactive.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/rendering/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/rendering/console.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/rendering/logger.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/rendering/panels.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/rendering/tables.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/scaffold.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/config/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/config/parser.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/config/schema.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/config/secrets.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/context.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/decorators.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/events.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/lineage.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/registry.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/state.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/yaml_builder.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/mcp/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/ai/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/ai/chunking.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/ai/embeddings.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/base.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/dataset.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/governance/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/governance/catalog.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/governance/classification.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/governance/lineage_store.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/governance/rules.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/local_fs.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/manager.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/postgres.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/quality/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/quality/contract.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/quality/expectations.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/quality/report.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/quality/validator.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/rest.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/transform/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/transform/pandas.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/transform/spark.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/vector/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/vector/base.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/vector/qdrant.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/telemetry/__init__.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/telemetry/instrumentation.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/telemetry/provider.py +0 -0
- {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/tui/__init__.py +0 -0
- {aptdata-0.0.3 → 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
|
|
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
|
|
@@ -45,11 +45,11 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
|
|
46
46
|
# aptdata
|
|
47
47
|
|
|
48
|
-
> **v0.0.
|
|
48
|
+
> **v0.0.3** · A declarative, extensible framework for building smart data pipelines in Python.
|
|
49
49
|
|
|
50
50
|
[](https://www.python.org/)
|
|
51
51
|
[](LICENSE)
|
|
52
|
-
[](CHANGELOG.md)
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# aptdata
|
|
2
2
|
|
|
3
|
-
> **v0.0.
|
|
3
|
+
> **v0.0.3** · A declarative, extensible framework for building smart data pipelines in Python.
|
|
4
4
|
|
|
5
5
|
[](https://www.python.org/)
|
|
6
6
|
[](LICENSE)
|
|
7
|
-
[](CHANGELOG.md)
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -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
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Projects — a project is a Flow of agent tasks.
|
|
2
|
+
|
|
3
|
+
This is the bridge between aptdata's orchestration model and the multi-agent
|
|
4
|
+
ecosystem: a :class:`Project` is a declarative plan (a list of :class:`Task`),
|
|
5
|
+
and :class:`ProjectRunner` executes it by routing each task to the right agent
|
|
6
|
+
via the :class:`~aptdata.agents.router.Router`.
|
|
7
|
+
|
|
8
|
+
A project is persisted as ``<name>.project.yaml`` — editable by hand, runnable
|
|
9
|
+
by ``aptdata project run``. Each task can pin an explicit ``agent``, ask for a
|
|
10
|
+
``capability`` (routed to the best agent), or fall back to skill routing on its
|
|
11
|
+
``prompt``. ``depends_on`` lets a task be skipped when an upstream task fails.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
from aptdata.agents.router import Router
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Task(BaseModel):
|
|
30
|
+
"""One step of a project, dispatched to a single agent."""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
prompt: str
|
|
34
|
+
capability: str | None = None # route by capability
|
|
35
|
+
agent: str | None = None # pin an explicit agent (wins over capability)
|
|
36
|
+
depends_on: list[str] = Field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Project(BaseModel):
|
|
40
|
+
"""A declarative plan: an ordered list of agent tasks."""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
description: str = ""
|
|
44
|
+
tasks: list[Task] = Field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_yaml(cls, path: str | Path) -> Project:
|
|
48
|
+
raw = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {}
|
|
49
|
+
return cls(**raw)
|
|
50
|
+
|
|
51
|
+
def to_yaml(self, path: str | Path) -> None:
|
|
52
|
+
data = self.model_dump(exclude_defaults=False)
|
|
53
|
+
Path(path).write_text(
|
|
54
|
+
yaml.safe_dump(data, allow_unicode=True, sort_keys=False),
|
|
55
|
+
encoding="utf-8",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class TaskResult:
|
|
61
|
+
"""Outcome of running a single task."""
|
|
62
|
+
|
|
63
|
+
task_id: str
|
|
64
|
+
agent_id: str | None
|
|
65
|
+
mode: str
|
|
66
|
+
ok: bool
|
|
67
|
+
text: str = ""
|
|
68
|
+
error: str | None = None
|
|
69
|
+
skipped: bool = False
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"task_id": self.task_id,
|
|
74
|
+
"agent_id": self.agent_id,
|
|
75
|
+
"mode": self.mode,
|
|
76
|
+
"ok": self.ok,
|
|
77
|
+
"skipped": self.skipped,
|
|
78
|
+
"error": self.error,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ProjectRunner:
|
|
83
|
+
"""Executes a :class:`Project` by routing every task through the Router."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, project: Project, router: Router) -> None:
|
|
86
|
+
self.project = project
|
|
87
|
+
self.router = router
|
|
88
|
+
|
|
89
|
+
def _route_task(self, task: Task):
|
|
90
|
+
"""Decide which agent handles a task (explicit > capability > skill)."""
|
|
91
|
+
from aptdata.agents.router import RouteDecision # noqa: PLC0415
|
|
92
|
+
|
|
93
|
+
if task.agent:
|
|
94
|
+
if task.agent in self.router.registry:
|
|
95
|
+
return RouteDecision(
|
|
96
|
+
agent_id=task.agent, mode="explicit", text=task.prompt,
|
|
97
|
+
confidence=1.0,
|
|
98
|
+
)
|
|
99
|
+
return RouteDecision(agent_id=None, mode="none", text=task.prompt)
|
|
100
|
+
if task.capability:
|
|
101
|
+
agent = self.router.registry.resolve(task.capability)
|
|
102
|
+
if agent:
|
|
103
|
+
return RouteDecision(
|
|
104
|
+
agent_id=agent.id, mode="capability", text=task.prompt,
|
|
105
|
+
confidence=0.9,
|
|
106
|
+
)
|
|
107
|
+
return self.router.route(task.prompt)
|
|
108
|
+
|
|
109
|
+
def plan(self) -> list[TaskResult]:
|
|
110
|
+
"""Dry-run: resolve routing for every task without sending anything."""
|
|
111
|
+
results: list[TaskResult] = []
|
|
112
|
+
for task in self.project.tasks:
|
|
113
|
+
decision = self._route_task(task)
|
|
114
|
+
results.append(
|
|
115
|
+
TaskResult(
|
|
116
|
+
task_id=task.id,
|
|
117
|
+
agent_id=decision.agent_id,
|
|
118
|
+
mode=decision.mode,
|
|
119
|
+
ok=decision.agent_id is not None,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
return results
|
|
123
|
+
|
|
124
|
+
def run(self) -> list[TaskResult]:
|
|
125
|
+
"""Execute tasks in order, skipping those whose dependencies failed."""
|
|
126
|
+
results: dict[str, TaskResult] = {}
|
|
127
|
+
for task in self.project.tasks:
|
|
128
|
+
failed_dep = next(
|
|
129
|
+
(
|
|
130
|
+
d
|
|
131
|
+
for d in task.depends_on
|
|
132
|
+
if d not in results or not results[d].ok
|
|
133
|
+
),
|
|
134
|
+
None,
|
|
135
|
+
)
|
|
136
|
+
if failed_dep is not None:
|
|
137
|
+
results[task.id] = TaskResult(
|
|
138
|
+
task_id=task.id, agent_id=None, mode="skipped", ok=False,
|
|
139
|
+
skipped=True, error=f"dependency '{failed_dep}' unmet",
|
|
140
|
+
)
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
decision = self._route_task(task)
|
|
144
|
+
if decision.agent_id is None:
|
|
145
|
+
results[task.id] = TaskResult(
|
|
146
|
+
task_id=task.id, agent_id=None, mode=decision.mode, ok=False,
|
|
147
|
+
error="no agent could be routed",
|
|
148
|
+
)
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
response = self.router.registry.get(decision.agent_id).send(
|
|
152
|
+
decision.text
|
|
153
|
+
)
|
|
154
|
+
results[task.id] = TaskResult(
|
|
155
|
+
task_id=task.id,
|
|
156
|
+
agent_id=decision.agent_id,
|
|
157
|
+
mode=decision.mode,
|
|
158
|
+
ok=response.ok,
|
|
159
|
+
text=response.text,
|
|
160
|
+
error=response.error,
|
|
161
|
+
)
|
|
162
|
+
return [results[t.id] for t in self.project.tasks]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def scaffold_project(name: str) -> Project:
|
|
166
|
+
"""Return a starter project with example tasks across capabilities."""
|
|
167
|
+
return Project(
|
|
168
|
+
name=name,
|
|
169
|
+
description=f"Projeto {name} — orquestrado pelo aptdata.",
|
|
170
|
+
tasks=[
|
|
171
|
+
Task(id="planejar", prompt=f"Faz o plano técnico do {name}.",
|
|
172
|
+
capability="planning"),
|
|
173
|
+
Task(id="backend", prompt="Implementa o backend / API.",
|
|
174
|
+
capability="backend", depends_on=["planejar"]),
|
|
175
|
+
Task(id="frontend", prompt="Implementa a interface.",
|
|
176
|
+
capability="frontend", depends_on=["planejar"]),
|
|
177
|
+
Task(id="deploy", prompt="Sobe em produção.",
|
|
178
|
+
capability="deploy", depends_on=["backend", "frontend"]),
|
|
179
|
+
],
|
|
180
|
+
)
|