probiotic 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.
- probiotic-0.1.0/.gitignore +12 -0
- probiotic-0.1.0/PKG-INFO +78 -0
- probiotic-0.1.0/README.md +54 -0
- probiotic-0.1.0/hive/__init__.py +4 -0
- probiotic-0.1.0/hive/analyzer.py +78 -0
- probiotic-0.1.0/hive/config.py +25 -0
- probiotic-0.1.0/hive/distributor.py +14 -0
- probiotic-0.1.0/hive/models.py +229 -0
- probiotic-0.1.0/hive/registry.py +15 -0
- probiotic-0.1.0/hive/server.py +86 -0
- probiotic-0.1.0/pyproject.toml +46 -0
- probiotic-0.1.0/src/probiotic/__init__.py +6 -0
- probiotic-0.1.0/src/probiotic/cli.py +390 -0
- probiotic-0.1.0/src/probiotic/dna/__init__.py +4 -0
- probiotic-0.1.0/src/probiotic/dna/binary_finder.py +146 -0
- probiotic-0.1.0/src/probiotic/dna/config.py +113 -0
- probiotic-0.1.0/src/probiotic/dna/hive_client.py +119 -0
- probiotic-0.1.0/src/probiotic/dna/journal.py +177 -0
- probiotic-0.1.0/src/probiotic/dna/lock.py +54 -0
- probiotic-0.1.0/src/probiotic/dna/prompts.py +167 -0
- probiotic-0.1.0/src/probiotic/dna/runtime.py +266 -0
- probiotic-0.1.0/src/probiotic/dna/scheduler.py +162 -0
- probiotic-0.1.0/src/probiotic/dna/tickets.py +72 -0
- probiotic-0.1.0/src/probiotic/dna/tools/__init__.py +2 -0
- probiotic-0.1.0/src/probiotic/dna/tools/usage.py +215 -0
- probiotic-0.1.0/src/probiotic/templates/__init__.py +2 -0
- probiotic-0.1.0/src/probiotic/templates/_base/SOUL.md.j2 +7 -0
- probiotic-0.1.0/src/probiotic/templates/_base/agent.md.j2 +25 -0
- probiotic-0.1.0/src/probiotic/templates/_base/cell.toml.default +13 -0
- probiotic-0.1.0/src/probiotic/templates/commit/SOUL.md +4 -0
- probiotic-0.1.0/src/probiotic/templates/commit/agent.md.j2 +22 -0
- probiotic-0.1.0/src/probiotic/templates/commit/cell.toml.default +13 -0
- probiotic-0.1.0/src/probiotic/templates/documentation/SOUL.md +4 -0
- probiotic-0.1.0/src/probiotic/templates/documentation/agent.md.j2 +23 -0
- probiotic-0.1.0/src/probiotic/templates/documentation/cell.toml.default +13 -0
- probiotic-0.1.0/src/probiotic/templates/janitor/SOUL.md +7 -0
- probiotic-0.1.0/src/probiotic/templates/janitor/agent.md.j2 +35 -0
- probiotic-0.1.0/src/probiotic/templates/janitor/cell.toml.default +13 -0
- probiotic-0.1.0/src/probiotic/templates/manager/SOUL.md +8 -0
- probiotic-0.1.0/src/probiotic/templates/manager/agent.md.j2 +35 -0
- probiotic-0.1.0/src/probiotic/templates/manager/cell.toml.default +13 -0
- probiotic-0.1.0/src/probiotic/templates/progress_tracker/SOUL.md +4 -0
- probiotic-0.1.0/src/probiotic/templates/progress_tracker/agent.md.j2 +29 -0
- probiotic-0.1.0/src/probiotic/templates/progress_tracker/cell.toml.default +13 -0
- probiotic-0.1.0/src/probiotic/templates/researcher/SOUL.md +4 -0
- probiotic-0.1.0/src/probiotic/templates/researcher/agent.md.j2 +42 -0
- probiotic-0.1.0/src/probiotic/templates/researcher/cell.toml.default +13 -0
- probiotic-0.1.0/tests/__init__.py +0 -0
- probiotic-0.1.0/tests/test_probiotic_cli.py +63 -0
- probiotic-0.1.0/tests/test_probiotic_hive_client.py +60 -0
- probiotic-0.1.0/tests/test_probiotic_hive_server.py +50 -0
- probiotic-0.1.0/tests/test_probiotic_journal.py +58 -0
- probiotic-0.1.0/tests/test_probiotic_prompts.py +53 -0
- probiotic-0.1.0/tests/test_probiotic_runtime.py +91 -0
- probiotic-0.1.0/tests/test_probiotic_scheduler.py +41 -0
- probiotic-0.1.0/tests/test_probiotic_tickets.py +30 -0
probiotic-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: probiotic
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Autonomous self-improving agent framework for code repositories
|
|
5
|
+
Project-URL: Homepage, https://github.com/penthoy/probiotic
|
|
6
|
+
Project-URL: Repository, https://github.com/penthoy/probiotic
|
|
7
|
+
Project-URL: Issues, https://github.com/penthoy/probiotic/issues
|
|
8
|
+
Keywords: agents,automation,cli,symbiont
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Provides-Extra: hive-server
|
|
21
|
+
Requires-Dist: fastapi>=0.110; extra == 'hive-server'
|
|
22
|
+
Requires-Dist: uvicorn>=0.30; extra == 'hive-server'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Probiotic
|
|
26
|
+
|
|
27
|
+
Probiotic is a lightweight runtime for repository-native autonomous agents.
|
|
28
|
+
|
|
29
|
+
It provides:
|
|
30
|
+
|
|
31
|
+
- a DNA runtime that installs as a Python package
|
|
32
|
+
- repo-local cells under `symbiont/cells/`
|
|
33
|
+
- a nightly scheduler
|
|
34
|
+
- optional hive reporting and prompt improvement distribution
|
|
35
|
+
|
|
36
|
+
## Status
|
|
37
|
+
|
|
38
|
+
This project is currently alpha.
|
|
39
|
+
|
|
40
|
+
The v2 loop implemented in this repo includes:
|
|
41
|
+
|
|
42
|
+
- prompt hashing based on shareable prompt text
|
|
43
|
+
- passive approval metrics from local run history
|
|
44
|
+
- hive prompt snapshot storage
|
|
45
|
+
- analyzer-generated prompt patches
|
|
46
|
+
- nightly `apply_updates()` before cell execution
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
Until the package is published to PyPI, install it from GitHub or from a built wheel:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install git+https://github.com/penthoy/probiotic.git
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
or
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install probiotic-0.1.0-py3-none-any.whl
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## CLI
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
probiotic init
|
|
66
|
+
probiotic spawn janitor --template janitor
|
|
67
|
+
probiotic run-nightly
|
|
68
|
+
probiotic connect-hive http://<host>:8000 --api-key <key>
|
|
69
|
+
probiotic update-check
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv sync --extra dev --extra hive-server
|
|
76
|
+
uv run pytest
|
|
77
|
+
uv build
|
|
78
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Probiotic
|
|
2
|
+
|
|
3
|
+
Probiotic is a lightweight runtime for repository-native autonomous agents.
|
|
4
|
+
|
|
5
|
+
It provides:
|
|
6
|
+
|
|
7
|
+
- a DNA runtime that installs as a Python package
|
|
8
|
+
- repo-local cells under `symbiont/cells/`
|
|
9
|
+
- a nightly scheduler
|
|
10
|
+
- optional hive reporting and prompt improvement distribution
|
|
11
|
+
|
|
12
|
+
## Status
|
|
13
|
+
|
|
14
|
+
This project is currently alpha.
|
|
15
|
+
|
|
16
|
+
The v2 loop implemented in this repo includes:
|
|
17
|
+
|
|
18
|
+
- prompt hashing based on shareable prompt text
|
|
19
|
+
- passive approval metrics from local run history
|
|
20
|
+
- hive prompt snapshot storage
|
|
21
|
+
- analyzer-generated prompt patches
|
|
22
|
+
- nightly `apply_updates()` before cell execution
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
Until the package is published to PyPI, install it from GitHub or from a built wheel:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install git+https://github.com/penthoy/probiotic.git
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
or
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install probiotic-0.1.0-py3-none-any.whl
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## CLI
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
probiotic init
|
|
42
|
+
probiotic spawn janitor --template janitor
|
|
43
|
+
probiotic run-nightly
|
|
44
|
+
probiotic connect-hive http://<host>:8000 --api-key <key>
|
|
45
|
+
probiotic update-check
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv sync --extra dev --extra hive-server
|
|
52
|
+
uv run pytest
|
|
53
|
+
uv build
|
|
54
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
from .models import HiveStore, RunRecord, VerifiedImprovement
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _confidence(delta: float, repo_count: int) -> float:
|
|
10
|
+
return min(0.99, max(0.0, 0.65 + delta + repo_count * 0.05))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_verified_improvements(
|
|
14
|
+
records: list[RunRecord],
|
|
15
|
+
*,
|
|
16
|
+
prompt_lookup: Callable[[str, str], str | None] | None = None,
|
|
17
|
+
min_repos: int = 3,
|
|
18
|
+
min_delta: float = 0.10,
|
|
19
|
+
min_confidence: float = 0.80,
|
|
20
|
+
) -> list[VerifiedImprovement]:
|
|
21
|
+
by_hash: dict[str, list[RunRecord]] = defaultdict(list)
|
|
22
|
+
for record in records:
|
|
23
|
+
if record.approval_rate_30d is not None:
|
|
24
|
+
by_hash[record.prompt_hash].append(record)
|
|
25
|
+
|
|
26
|
+
improvements: list[VerifiedImprovement] = []
|
|
27
|
+
for prompt_hash, grouped in by_hash.items():
|
|
28
|
+
repos = {record.instance_id for record in grouped}
|
|
29
|
+
if len(repos) < min_repos:
|
|
30
|
+
continue
|
|
31
|
+
previous_rates = [
|
|
32
|
+
float(record.metrics["previous_approval_rate_30d"])
|
|
33
|
+
for record in grouped
|
|
34
|
+
if "previous_approval_rate_30d" in record.metrics
|
|
35
|
+
]
|
|
36
|
+
if not previous_rates:
|
|
37
|
+
continue
|
|
38
|
+
previous = sum(previous_rates) / len(previous_rates)
|
|
39
|
+
current = sum(float(record.approval_rate_30d or 0.0) for record in grouped) / len(grouped)
|
|
40
|
+
delta = current - previous
|
|
41
|
+
confidence = _confidence(delta, len(repos))
|
|
42
|
+
if delta >= min_delta and confidence >= min_confidence:
|
|
43
|
+
sample = grouped[0]
|
|
44
|
+
patch = prompt_lookup(prompt_hash, sample.agent) if prompt_lookup is not None else ""
|
|
45
|
+
if prompt_lookup is not None and not patch:
|
|
46
|
+
continue
|
|
47
|
+
improvements.append(
|
|
48
|
+
VerifiedImprovement(
|
|
49
|
+
agent_template=sample.agent,
|
|
50
|
+
prompt_hash=prompt_hash,
|
|
51
|
+
change_type="prompt_patch",
|
|
52
|
+
description=f"{sample.agent} prompt correlated with {delta:.0%} approval-rate lift across {len(repos)} repos",
|
|
53
|
+
patch=patch,
|
|
54
|
+
confidence=confidence,
|
|
55
|
+
verified_repos=len(repos),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
return improvements
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def analyze_and_store(
|
|
62
|
+
store: HiveStore,
|
|
63
|
+
*,
|
|
64
|
+
min_repos: int = 3,
|
|
65
|
+
min_delta: float = 0.10,
|
|
66
|
+
min_confidence: float = 0.80,
|
|
67
|
+
) -> list[VerifiedImprovement]:
|
|
68
|
+
improvements = find_verified_improvements(
|
|
69
|
+
store.reports(),
|
|
70
|
+
prompt_lookup=lambda prompt_hash, agent: store.get_prompt_by_hash(prompt_hash, agent=agent),
|
|
71
|
+
min_repos=min_repos,
|
|
72
|
+
min_delta=min_delta,
|
|
73
|
+
min_confidence=min_confidence,
|
|
74
|
+
)
|
|
75
|
+
for improvement in improvements:
|
|
76
|
+
store.save_improvement(improvement)
|
|
77
|
+
return improvements
|
|
78
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class HiveServerConfig:
|
|
10
|
+
database_path: Path = Path("hive.sqlite3")
|
|
11
|
+
api_key: str = ""
|
|
12
|
+
min_repos: int = 2
|
|
13
|
+
min_delta: float = 0.10
|
|
14
|
+
min_confidence: float = 0.80
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_env(cls) -> "HiveServerConfig":
|
|
18
|
+
return cls(
|
|
19
|
+
database_path=Path(os.environ.get("PROBIOTIC_HIVE_DB", "hive.sqlite3")),
|
|
20
|
+
api_key=os.environ.get("PROBIOTIC_HIVE_API_KEY", ""),
|
|
21
|
+
min_repos=int(os.environ.get("PROBIOTIC_HIVE_MIN_REPOS", "2")),
|
|
22
|
+
min_delta=float(os.environ.get("PROBIOTIC_HIVE_MIN_DELTA", "0.10")),
|
|
23
|
+
min_confidence=float(os.environ.get("PROBIOTIC_HIVE_MIN_CONFIDENCE", "0.80")),
|
|
24
|
+
)
|
|
25
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
|
|
5
|
+
from .models import HiveStore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def updates_for_instance(store: HiveStore, *, instance_id: str) -> dict:
|
|
9
|
+
return {
|
|
10
|
+
"improvements": [asdict(improvement) for improvement in store.improvements()],
|
|
11
|
+
"new_templates": [],
|
|
12
|
+
"instance_id": instance_id,
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Instance:
|
|
13
|
+
instance_id: str
|
|
14
|
+
repo_name: str
|
|
15
|
+
last_seen: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class RunRecord:
|
|
20
|
+
instance_id: str
|
|
21
|
+
repo_name: str
|
|
22
|
+
agent: str
|
|
23
|
+
run_date: str
|
|
24
|
+
outcome: str
|
|
25
|
+
prompt_hash: str
|
|
26
|
+
approval_rate_30d: float | None = None
|
|
27
|
+
metrics: dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class VerifiedImprovement:
|
|
32
|
+
agent_template: str
|
|
33
|
+
prompt_hash: str
|
|
34
|
+
change_type: str
|
|
35
|
+
description: str
|
|
36
|
+
patch: str
|
|
37
|
+
confidence: float
|
|
38
|
+
verified_repos: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class PromptSnapshot:
|
|
43
|
+
prompt_hash: str
|
|
44
|
+
agent: str
|
|
45
|
+
content: str
|
|
46
|
+
first_seen: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HiveStore:
|
|
50
|
+
def __init__(self, path: Path | str = "hive.sqlite3") -> None:
|
|
51
|
+
self.path = Path(path)
|
|
52
|
+
self.init_db()
|
|
53
|
+
|
|
54
|
+
def connect(self) -> sqlite3.Connection:
|
|
55
|
+
con = sqlite3.connect(str(self.path))
|
|
56
|
+
con.row_factory = sqlite3.Row
|
|
57
|
+
return con
|
|
58
|
+
|
|
59
|
+
def init_db(self) -> None:
|
|
60
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
with self.connect() as con:
|
|
62
|
+
con.execute(
|
|
63
|
+
"""
|
|
64
|
+
CREATE TABLE IF NOT EXISTS instances (
|
|
65
|
+
instance_id TEXT PRIMARY KEY,
|
|
66
|
+
repo_name TEXT NOT NULL,
|
|
67
|
+
last_seen TEXT NOT NULL
|
|
68
|
+
)
|
|
69
|
+
"""
|
|
70
|
+
)
|
|
71
|
+
con.execute(
|
|
72
|
+
"""
|
|
73
|
+
CREATE TABLE IF NOT EXISTS run_records (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
instance_id TEXT NOT NULL,
|
|
76
|
+
repo_name TEXT NOT NULL,
|
|
77
|
+
agent TEXT NOT NULL,
|
|
78
|
+
run_date TEXT NOT NULL,
|
|
79
|
+
outcome TEXT NOT NULL,
|
|
80
|
+
prompt_hash TEXT NOT NULL,
|
|
81
|
+
approval_rate_30d REAL,
|
|
82
|
+
metrics_json TEXT NOT NULL
|
|
83
|
+
)
|
|
84
|
+
"""
|
|
85
|
+
)
|
|
86
|
+
con.execute(
|
|
87
|
+
"""
|
|
88
|
+
CREATE TABLE IF NOT EXISTS verified_improvements (
|
|
89
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
90
|
+
agent_template TEXT NOT NULL,
|
|
91
|
+
prompt_hash TEXT NOT NULL UNIQUE,
|
|
92
|
+
change_type TEXT NOT NULL,
|
|
93
|
+
description TEXT NOT NULL,
|
|
94
|
+
patch TEXT NOT NULL,
|
|
95
|
+
confidence REAL NOT NULL,
|
|
96
|
+
verified_repos INTEGER NOT NULL
|
|
97
|
+
)
|
|
98
|
+
"""
|
|
99
|
+
)
|
|
100
|
+
con.execute(
|
|
101
|
+
"""
|
|
102
|
+
CREATE TABLE IF NOT EXISTS prompt_snapshots (
|
|
103
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
|
+
prompt_hash TEXT NOT NULL,
|
|
105
|
+
agent TEXT NOT NULL,
|
|
106
|
+
content TEXT NOT NULL,
|
|
107
|
+
first_seen TEXT NOT NULL,
|
|
108
|
+
UNIQUE(prompt_hash, agent)
|
|
109
|
+
)
|
|
110
|
+
"""
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def save_report(self, report: RunRecord) -> None:
|
|
114
|
+
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
|
115
|
+
with self.connect() as con:
|
|
116
|
+
con.execute(
|
|
117
|
+
"""
|
|
118
|
+
INSERT INTO instances(instance_id, repo_name, last_seen)
|
|
119
|
+
VALUES (?, ?, ?)
|
|
120
|
+
ON CONFLICT(instance_id) DO UPDATE SET repo_name=excluded.repo_name, last_seen=excluded.last_seen
|
|
121
|
+
""",
|
|
122
|
+
(report.instance_id, report.repo_name, now),
|
|
123
|
+
)
|
|
124
|
+
con.execute(
|
|
125
|
+
"""
|
|
126
|
+
INSERT INTO run_records(
|
|
127
|
+
instance_id, repo_name, agent, run_date, outcome,
|
|
128
|
+
prompt_hash, approval_rate_30d, metrics_json
|
|
129
|
+
)
|
|
130
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
131
|
+
""",
|
|
132
|
+
(
|
|
133
|
+
report.instance_id,
|
|
134
|
+
report.repo_name,
|
|
135
|
+
report.agent,
|
|
136
|
+
report.run_date,
|
|
137
|
+
report.outcome,
|
|
138
|
+
report.prompt_hash,
|
|
139
|
+
report.approval_rate_30d,
|
|
140
|
+
json.dumps(report.metrics),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def reports(self) -> list[RunRecord]:
|
|
145
|
+
with self.connect() as con:
|
|
146
|
+
rows = con.execute("SELECT * FROM run_records ORDER BY id").fetchall()
|
|
147
|
+
return [
|
|
148
|
+
RunRecord(
|
|
149
|
+
instance_id=row["instance_id"],
|
|
150
|
+
repo_name=row["repo_name"],
|
|
151
|
+
agent=row["agent"],
|
|
152
|
+
run_date=row["run_date"],
|
|
153
|
+
outcome=row["outcome"],
|
|
154
|
+
prompt_hash=row["prompt_hash"],
|
|
155
|
+
approval_rate_30d=row["approval_rate_30d"],
|
|
156
|
+
metrics=json.loads(row["metrics_json"]),
|
|
157
|
+
)
|
|
158
|
+
for row in rows
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
def save_prompt_snapshot(self, *, prompt_hash: str, agent: str, content: str) -> None:
|
|
162
|
+
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
|
163
|
+
with self.connect() as con:
|
|
164
|
+
con.execute(
|
|
165
|
+
"""
|
|
166
|
+
INSERT INTO prompt_snapshots(prompt_hash, agent, content, first_seen)
|
|
167
|
+
VALUES (?, ?, ?, ?)
|
|
168
|
+
ON CONFLICT(prompt_hash, agent) DO NOTHING
|
|
169
|
+
""",
|
|
170
|
+
(prompt_hash, agent, content, now),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def get_prompt_by_hash(self, prompt_hash: str, *, agent: str | None = None) -> str | None:
|
|
174
|
+
query = "SELECT content FROM prompt_snapshots WHERE prompt_hash = ?"
|
|
175
|
+
params: list[Any] = [prompt_hash]
|
|
176
|
+
if agent is not None:
|
|
177
|
+
query += " AND agent = ?"
|
|
178
|
+
params.append(agent)
|
|
179
|
+
query += " ORDER BY first_seen LIMIT 1"
|
|
180
|
+
with self.connect() as con:
|
|
181
|
+
row = con.execute(query, params).fetchone()
|
|
182
|
+
if row is None:
|
|
183
|
+
return None
|
|
184
|
+
return str(row["content"])
|
|
185
|
+
|
|
186
|
+
def save_improvement(self, improvement: VerifiedImprovement) -> None:
|
|
187
|
+
with self.connect() as con:
|
|
188
|
+
con.execute(
|
|
189
|
+
"""
|
|
190
|
+
INSERT INTO verified_improvements(
|
|
191
|
+
agent_template, prompt_hash, change_type, description,
|
|
192
|
+
patch, confidence, verified_repos
|
|
193
|
+
)
|
|
194
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
195
|
+
ON CONFLICT(prompt_hash) DO UPDATE SET
|
|
196
|
+
agent_template=excluded.agent_template,
|
|
197
|
+
change_type=excluded.change_type,
|
|
198
|
+
description=excluded.description,
|
|
199
|
+
patch=excluded.patch,
|
|
200
|
+
confidence=excluded.confidence,
|
|
201
|
+
verified_repos=excluded.verified_repos
|
|
202
|
+
""",
|
|
203
|
+
(
|
|
204
|
+
improvement.agent_template,
|
|
205
|
+
improvement.prompt_hash,
|
|
206
|
+
improvement.change_type,
|
|
207
|
+
improvement.description,
|
|
208
|
+
improvement.patch,
|
|
209
|
+
improvement.confidence,
|
|
210
|
+
improvement.verified_repos,
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def improvements(self) -> list[VerifiedImprovement]:
|
|
215
|
+
with self.connect() as con:
|
|
216
|
+
rows = con.execute("SELECT * FROM verified_improvements ORDER BY id").fetchall()
|
|
217
|
+
return [
|
|
218
|
+
VerifiedImprovement(
|
|
219
|
+
agent_template=row["agent_template"],
|
|
220
|
+
prompt_hash=row["prompt_hash"],
|
|
221
|
+
change_type=row["change_type"],
|
|
222
|
+
description=row["description"],
|
|
223
|
+
patch=row["patch"],
|
|
224
|
+
confidence=row["confidence"],
|
|
225
|
+
verified_repos=row["verified_repos"],
|
|
226
|
+
)
|
|
227
|
+
for row in rows
|
|
228
|
+
]
|
|
229
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .models import HiveStore, RunRecord
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Registry:
|
|
7
|
+
def __init__(self, store: HiveStore) -> None:
|
|
8
|
+
self.store = store
|
|
9
|
+
|
|
10
|
+
def record_report(self, report: RunRecord, *, prompt_text: str | None = None) -> dict[str, str]:
|
|
11
|
+
self.store.save_report(report)
|
|
12
|
+
if prompt_text and prompt_text.strip():
|
|
13
|
+
self.store.save_prompt_snapshot(prompt_hash=report.prompt_hash, agent=report.agent, content=prompt_text)
|
|
14
|
+
return {"status": "ok"}
|
|
15
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .analyzer import analyze_and_store
|
|
7
|
+
from .config import HiveServerConfig
|
|
8
|
+
from .distributor import updates_for_instance
|
|
9
|
+
from .models import HiveStore, RunRecord
|
|
10
|
+
from .registry import Registry
|
|
11
|
+
|
|
12
|
+
try: # pragma: no cover - exercised only when optional dependency is installed
|
|
13
|
+
from fastapi import Depends, FastAPI, Header, HTTPException
|
|
14
|
+
except ImportError: # pragma: no cover
|
|
15
|
+
Depends = FastAPI = Header = HTTPException = None # type: ignore[assignment]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
config = HiveServerConfig.from_env()
|
|
19
|
+
store = HiveStore(config.database_path)
|
|
20
|
+
registry = Registry(store)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _require_fastapi() -> None:
|
|
24
|
+
if FastAPI is None:
|
|
25
|
+
raise RuntimeError("Install probiotic[hive-server] to run the hive server")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _auth(authorization: str | None = None) -> None:
|
|
29
|
+
if not config.api_key:
|
|
30
|
+
return
|
|
31
|
+
expected = f"Bearer {config.api_key}"
|
|
32
|
+
if authorization != expected:
|
|
33
|
+
raise HTTPException(status_code=401, detail="invalid hive API key")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_app():
|
|
37
|
+
_require_fastapi()
|
|
38
|
+
app = FastAPI(title="Probiotic Hive")
|
|
39
|
+
|
|
40
|
+
@app.get("/health")
|
|
41
|
+
def health() -> dict[str, str]:
|
|
42
|
+
return {"status": "ok"}
|
|
43
|
+
|
|
44
|
+
@app.post("/report")
|
|
45
|
+
def report(payload: dict[str, Any], authorization: str | None = Header(default=None)) -> dict[str, str]:
|
|
46
|
+
_auth(authorization)
|
|
47
|
+
record = RunRecord(
|
|
48
|
+
instance_id=str(payload["instance_id"]),
|
|
49
|
+
repo_name=str(payload["repo_name"]),
|
|
50
|
+
agent=str(payload["agent"]),
|
|
51
|
+
run_date=str(payload["run_date"]),
|
|
52
|
+
outcome=str(payload["outcome"]),
|
|
53
|
+
prompt_hash=str(payload["prompt_hash"]),
|
|
54
|
+
approval_rate_30d=payload.get("approval_rate_30d"),
|
|
55
|
+
metrics=dict(payload.get("metrics", {})),
|
|
56
|
+
)
|
|
57
|
+
result = registry.record_report(record, prompt_text=payload.get("prompt_text"))
|
|
58
|
+
analyze_and_store(
|
|
59
|
+
store,
|
|
60
|
+
min_repos=config.min_repos,
|
|
61
|
+
min_delta=config.min_delta,
|
|
62
|
+
min_confidence=config.min_confidence,
|
|
63
|
+
)
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
@app.get("/updates")
|
|
67
|
+
def updates(instance_id: str, since: str | None = None, authorization: str | None = Header(default=None)) -> dict:
|
|
68
|
+
_auth(authorization)
|
|
69
|
+
return updates_for_instance(store, instance_id=instance_id)
|
|
70
|
+
|
|
71
|
+
@app.post("/analyze")
|
|
72
|
+
def analyze(authorization: str | None = Header(default=None)) -> dict:
|
|
73
|
+
_auth(authorization)
|
|
74
|
+
improvements = analyze_and_store(
|
|
75
|
+
store,
|
|
76
|
+
min_repos=config.min_repos,
|
|
77
|
+
min_delta=config.min_delta,
|
|
78
|
+
min_confidence=config.min_confidence,
|
|
79
|
+
)
|
|
80
|
+
return {"improvements": [asdict(item) for item in improvements]}
|
|
81
|
+
|
|
82
|
+
return app
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
app = create_app() if FastAPI is not None else None
|
|
86
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "probiotic"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Autonomous self-improving agent framework for code repositories"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
keywords = ["agents", "automation", "cli", "symbiont"]
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 3 - Alpha",
|
|
10
|
+
"Environment :: Console",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Topic :: Software Development :: Libraries",
|
|
16
|
+
]
|
|
17
|
+
dependencies = []
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/penthoy/probiotic"
|
|
21
|
+
Repository = "https://github.com/penthoy/probiotic"
|
|
22
|
+
Issues = "https://github.com/penthoy/probiotic/issues"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.0",
|
|
27
|
+
"pytest-asyncio>=0.24",
|
|
28
|
+
]
|
|
29
|
+
hive-server = [
|
|
30
|
+
"fastapi>=0.110",
|
|
31
|
+
"uvicorn>=0.30",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
probiotic = "probiotic.cli:main"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["hatchling"]
|
|
39
|
+
build-backend = "hatchling.build"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/probiotic"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
asyncio_mode = "auto"
|
|
46
|
+
testpaths = ["tests"]
|