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.
Files changed (93) hide show
  1. {aptdata-0.0.3 → aptdata-0.1.0}/PKG-INFO +3 -3
  2. {aptdata-0.0.3 → aptdata-0.1.0}/README.md +2 -2
  3. {aptdata-0.0.3 → 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.3 → aptdata-0.1.0}/aptdata/cli/app.py +4 -0
  18. {aptdata-0.0.3 → 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.1.0/aptdata/cli/commands/project_cmd.py +108 -0
  21. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/dataset.py +27 -7
  22. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/system.py +88 -1
  23. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/workflow.py +16 -33
  24. aptdata-0.1.0/aptdata/gamification/__init__.py +21 -0
  25. aptdata-0.1.0/aptdata/gamification/streak.py +85 -0
  26. aptdata-0.1.0/aptdata/gamification/xp.py +101 -0
  27. aptdata-0.1.0/aptdata/habits/__init__.py +22 -0
  28. aptdata-0.1.0/aptdata/habits/quickwins.py +146 -0
  29. aptdata-0.1.0/aptdata/integrations/__init__.py +1 -0
  30. aptdata-0.1.0/aptdata/integrations/calendar.py +211 -0
  31. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/mcp/server.py +68 -0
  32. aptdata-0.1.0/aptdata/observability/__init__.py +14 -0
  33. aptdata-0.1.0/aptdata/observability/llm_observer.py +156 -0
  34. {aptdata-0.0.3 → aptdata-0.1.0}/pyproject.toml +1 -1
  35. {aptdata-0.0.3 → aptdata-0.1.0}/LICENSE +0 -0
  36. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/__init__.py +0 -0
  37. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/config_cmd.py +0 -0
  38. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/mesh_cmd.py +0 -0
  39. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/plugin_cmd.py +0 -0
  40. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/system_cmd.py +0 -0
  41. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/commands/telemetry_cmd.py +0 -0
  42. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/completions.py +0 -0
  43. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/interactive.py +0 -0
  44. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/rendering/__init__.py +0 -0
  45. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/rendering/console.py +0 -0
  46. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/rendering/logger.py +0 -0
  47. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/rendering/panels.py +0 -0
  48. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/rendering/tables.py +0 -0
  49. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/cli/scaffold.py +0 -0
  50. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/config/__init__.py +0 -0
  51. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/config/parser.py +0 -0
  52. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/config/schema.py +0 -0
  53. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/config/secrets.py +0 -0
  54. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/__init__.py +0 -0
  55. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/context.py +0 -0
  56. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/decorators.py +0 -0
  57. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/events.py +0 -0
  58. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/lineage.py +0 -0
  59. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/registry.py +0 -0
  60. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/state.py +0 -0
  61. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/core/yaml_builder.py +0 -0
  62. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/mcp/__init__.py +0 -0
  63. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/__init__.py +0 -0
  64. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/ai/__init__.py +0 -0
  65. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/ai/chunking.py +0 -0
  66. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/ai/embeddings.py +0 -0
  67. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/base.py +0 -0
  68. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/dataset.py +0 -0
  69. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/governance/__init__.py +0 -0
  70. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/governance/catalog.py +0 -0
  71. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/governance/classification.py +0 -0
  72. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/governance/lineage_store.py +0 -0
  73. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/governance/rules.py +0 -0
  74. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/local_fs.py +0 -0
  75. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/manager.py +0 -0
  76. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/postgres.py +0 -0
  77. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/quality/__init__.py +0 -0
  78. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/quality/contract.py +0 -0
  79. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/quality/expectations.py +0 -0
  80. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/quality/report.py +0 -0
  81. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/quality/validator.py +0 -0
  82. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/rest.py +0 -0
  83. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/transform/__init__.py +0 -0
  84. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/transform/pandas.py +0 -0
  85. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/transform/spark.py +0 -0
  86. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/vector/__init__.py +0 -0
  87. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/vector/base.py +0 -0
  88. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/plugins/vector/qdrant.py +0 -0
  89. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/telemetry/__init__.py +0 -0
  90. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/telemetry/instrumentation.py +0 -0
  91. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/telemetry/provider.py +0 -0
  92. {aptdata-0.0.3 → aptdata-0.1.0}/aptdata/tui/__init__.py +0 -0
  93. {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
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.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.
49
49
 
50
50
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
51
51
  [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
52
- [![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)
53
53
 
54
54
  ---
55
55
 
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  """aptdata: A framework for smart data pipelines."""
2
2
 
3
- __version__ = "0.0.3"
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
@@ -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
+ )