reporesolve 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.
- reporesolve-0.1.0/LICENSE +21 -0
- reporesolve-0.1.0/PKG-INFO +65 -0
- reporesolve-0.1.0/README.md +51 -0
- reporesolve-0.1.0/pyproject.toml +26 -0
- reporesolve-0.1.0/reporesolve/__init__.py +3 -0
- reporesolve-0.1.0/reporesolve/agent/__init__.py +1 -0
- reporesolve-0.1.0/reporesolve/agent/memory.py +16 -0
- reporesolve-0.1.0/reporesolve/agent/planner.py +77 -0
- reporesolve-0.1.0/reporesolve/agent/schema.py +83 -0
- reporesolve-0.1.0/reporesolve/cli/__init__.py +5 -0
- reporesolve-0.1.0/reporesolve/cli/main.py +132 -0
- reporesolve-0.1.0/reporesolve/config/__init__.py +1 -0
- reporesolve-0.1.0/reporesolve/config/env.py +30 -0
- reporesolve-0.1.0/reporesolve/config/settings.py +68 -0
- reporesolve-0.1.0/reporesolve/providers/__init__.py +1 -0
- reporesolve-0.1.0/reporesolve/providers/anthropic_provider.py +48 -0
- reporesolve-0.1.0/reporesolve/providers/base.py +17 -0
- reporesolve-0.1.0/reporesolve/providers/openai_provider.py +44 -0
- reporesolve-0.1.0/reporesolve/storage/__init__.py +1 -0
- reporesolve-0.1.0/reporesolve/storage/paths.py +27 -0
- reporesolve-0.1.0/reporesolve/supervisor/__init__.py +1 -0
- reporesolve-0.1.0/reporesolve/supervisor/state.py +17 -0
- reporesolve-0.1.0/reporesolve/supervisor/supervisor.py +293 -0
- reporesolve-0.1.0/reporesolve/supervisor/workflow.py +38 -0
- reporesolve-0.1.0/reporesolve/tools/__init__.py +1 -0
- reporesolve-0.1.0/reporesolve/tools/base.py +24 -0
- reporesolve-0.1.0/reporesolve/tools/build.py +21 -0
- reporesolve-0.1.0/reporesolve/tools/clone.py +88 -0
- reporesolve-0.1.0/reporesolve/tools/inspect.py +41 -0
- reporesolve-0.1.0/reporesolve/tools/install.py +21 -0
- reporesolve-0.1.0/reporesolve/tools/parse.py +131 -0
- reporesolve-0.1.0/reporesolve/tools/smoke.py +22 -0
- reporesolve-0.1.0/reporesolve/tui/__init__.py +1 -0
- reporesolve-0.1.0/reporesolve/tui/flows.py +38 -0
- reporesolve-0.1.0/reporesolve/tui/prompts.py +114 -0
- reporesolve-0.1.0/reporesolve/tui/render.py +34 -0
- reporesolve-0.1.0/reporesolve/utils/__init__.py +1 -0
- reporesolve-0.1.0/reporesolve/utils/logging.py +31 -0
- reporesolve-0.1.0/reporesolve.egg-info/PKG-INFO +65 -0
- reporesolve-0.1.0/reporesolve.egg-info/SOURCES.txt +43 -0
- reporesolve-0.1.0/reporesolve.egg-info/dependency_links.txt +1 -0
- reporesolve-0.1.0/reporesolve.egg-info/entry_points.txt +2 -0
- reporesolve-0.1.0/reporesolve.egg-info/requires.txt +4 -0
- reporesolve-0.1.0/reporesolve.egg-info/top_level.txt +1 -0
- reporesolve-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Applied Intelligence Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reporesolve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agentic supervisor for building and repairing multi-repository environments
|
|
5
|
+
Author: Your Name
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: typer>=0.12.0
|
|
10
|
+
Requires-Dist: questionary>=2.0.1
|
|
11
|
+
Requires-Dist: rich>=13.7.0
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# RepoResolve
|
|
16
|
+
|
|
17
|
+
RepoResolve is an agentic supervisor system that builds and repairs multi-repository environments. It guides you through input collection, analyzes dependency metadata, and iteratively proposes environment changes using a reasoning agent while deterministic tools execute the steps.
|
|
18
|
+
|
|
19
|
+
## What It Solves
|
|
20
|
+
- Bootstrapping a working environment across multiple repos
|
|
21
|
+
- Identifying dependency conflicts and setup failures
|
|
22
|
+
- Iteratively refining environment proposals until smoke tests pass
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e .
|
|
27
|
+
reporesolve
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Example Output (Trimmed)
|
|
31
|
+
```
|
|
32
|
+
RepoResolve - Starting supervisor run...
|
|
33
|
+
Cloning repos... OK
|
|
34
|
+
Inspecting repos... OK
|
|
35
|
+
Parsing dependencies... OK
|
|
36
|
+
Attempt 1/3
|
|
37
|
+
Build... OK
|
|
38
|
+
Install... OK
|
|
39
|
+
Smoke tests... OK
|
|
40
|
+
Success
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## How the Agent Loop Works (High Level)
|
|
44
|
+
1. Tools inspect repositories and parse dependencies.
|
|
45
|
+
2. The agent proposes an environment decision in strict JSON.
|
|
46
|
+
3. Tools build/install/test the environment.
|
|
47
|
+
4. Failures are summarized and sent back to the agent for revision.
|
|
48
|
+
5. The loop stops on success, agent stop, or max attempts.
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
RepoResolve reads API keys from `.env` (if present) or environment variables.
|
|
52
|
+
|
|
53
|
+
Example `.env`:
|
|
54
|
+
```
|
|
55
|
+
OPENAI_API_KEY=your-key
|
|
56
|
+
ANTHROPIC_API_KEY=your-key
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
- `reporesolve` (start guided flow)
|
|
61
|
+
- `reporesolve start`
|
|
62
|
+
- `reporesolve config`
|
|
63
|
+
- `reporesolve resume`
|
|
64
|
+
- `reporesolve doctor`
|
|
65
|
+
- `reporesolve version`
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# RepoResolve
|
|
2
|
+
|
|
3
|
+
RepoResolve is an agentic supervisor system that builds and repairs multi-repository environments. It guides you through input collection, analyzes dependency metadata, and iteratively proposes environment changes using a reasoning agent while deterministic tools execute the steps.
|
|
4
|
+
|
|
5
|
+
## What It Solves
|
|
6
|
+
- Bootstrapping a working environment across multiple repos
|
|
7
|
+
- Identifying dependency conflicts and setup failures
|
|
8
|
+
- Iteratively refining environment proposals until smoke tests pass
|
|
9
|
+
|
|
10
|
+
## Quickstart
|
|
11
|
+
```bash
|
|
12
|
+
pip install -e .
|
|
13
|
+
reporesolve
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Example Output (Trimmed)
|
|
17
|
+
```
|
|
18
|
+
RepoResolve - Starting supervisor run...
|
|
19
|
+
Cloning repos... OK
|
|
20
|
+
Inspecting repos... OK
|
|
21
|
+
Parsing dependencies... OK
|
|
22
|
+
Attempt 1/3
|
|
23
|
+
Build... OK
|
|
24
|
+
Install... OK
|
|
25
|
+
Smoke tests... OK
|
|
26
|
+
Success
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How the Agent Loop Works (High Level)
|
|
30
|
+
1. Tools inspect repositories and parse dependencies.
|
|
31
|
+
2. The agent proposes an environment decision in strict JSON.
|
|
32
|
+
3. Tools build/install/test the environment.
|
|
33
|
+
4. Failures are summarized and sent back to the agent for revision.
|
|
34
|
+
5. The loop stops on success, agent stop, or max attempts.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
RepoResolve reads API keys from `.env` (if present) or environment variables.
|
|
38
|
+
|
|
39
|
+
Example `.env`:
|
|
40
|
+
```
|
|
41
|
+
OPENAI_API_KEY=your-key
|
|
42
|
+
ANTHROPIC_API_KEY=your-key
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
- `reporesolve` (start guided flow)
|
|
47
|
+
- `reporesolve start`
|
|
48
|
+
- `reporesolve config`
|
|
49
|
+
- `reporesolve resume`
|
|
50
|
+
- `reporesolve doctor`
|
|
51
|
+
- `reporesolve version`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "reporesolve"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Agentic supervisor for building and repairing multi-repository environments"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer>=0.12.0",
|
|
13
|
+
"questionary>=2.0.1",
|
|
14
|
+
"rich>=13.7.0",
|
|
15
|
+
"python-dotenv>=1.0.1",
|
|
16
|
+
]
|
|
17
|
+
authors = [
|
|
18
|
+
{ name = "Your Name" }
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
reporesolve = "reporesolve.cli.main:app"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["."]
|
|
26
|
+
include = ["reporesolve*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Agent memory placeholder for future decision history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from .schema import AgentDecision
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class AgentMemory:
|
|
13
|
+
decisions: List[AgentDecision] = field(default_factory=list)
|
|
14
|
+
|
|
15
|
+
def record(self, decision: AgentDecision) -> None:
|
|
16
|
+
self.decisions.append(decision)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Agent planner for structured decision making."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
from .schema import AgentDecision, DecisionError, fallback_decision
|
|
9
|
+
from ..providers.base import BaseProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_PROMPT_HEADER = (
|
|
13
|
+
"You are RepoResolve Agent. You must output STRICT JSON only. "
|
|
14
|
+
"No markdown, no commentary."
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_PROMPT_SCHEMA = {
|
|
18
|
+
"action": "revise_environment | retry | stop | explain",
|
|
19
|
+
"reason": "string",
|
|
20
|
+
"changes": "list",
|
|
21
|
+
"retry": "bool",
|
|
22
|
+
"confidence": "float between 0 and 1",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentPlanner:
|
|
27
|
+
def __init__(self, provider: BaseProvider) -> None:
|
|
28
|
+
self._provider = provider
|
|
29
|
+
|
|
30
|
+
def plan_initial_environment(self, dependencies: Dict[str, Any]) -> AgentDecision:
|
|
31
|
+
prompt = self._build_prompt(
|
|
32
|
+
"Generate an initial environment proposal based on dependencies.",
|
|
33
|
+
{"dependencies": dependencies},
|
|
34
|
+
)
|
|
35
|
+
return self._request_decision(prompt, {"stage": "initial", "dependencies": dependencies})
|
|
36
|
+
|
|
37
|
+
def revise_environment(
|
|
38
|
+
self, previous_attempt: Dict[str, Any], failure: Dict[str, Any]
|
|
39
|
+
) -> AgentDecision:
|
|
40
|
+
prompt = self._build_prompt(
|
|
41
|
+
"Revise the environment based on the failure summary.",
|
|
42
|
+
{"previous_attempt": previous_attempt, "failure": failure},
|
|
43
|
+
)
|
|
44
|
+
return self._request_decision(
|
|
45
|
+
prompt,
|
|
46
|
+
{"stage": "revise", "previous_attempt": previous_attempt, "failure": failure},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def decide_next_action(self, state: Dict[str, Any]) -> AgentDecision:
|
|
50
|
+
prompt = self._build_prompt(
|
|
51
|
+
"Decide the next action based on current session state.",
|
|
52
|
+
{"state": state},
|
|
53
|
+
)
|
|
54
|
+
return self._request_decision(prompt, {"stage": "decide", "state": state})
|
|
55
|
+
|
|
56
|
+
def _build_prompt(self, instruction: str, payload: Dict[str, Any]) -> str:
|
|
57
|
+
body = json.dumps(payload, indent=2)
|
|
58
|
+
schema = json.dumps(_PROMPT_SCHEMA, indent=2)
|
|
59
|
+
return "\n\n".join(
|
|
60
|
+
[
|
|
61
|
+
_PROMPT_HEADER,
|
|
62
|
+
f"Instruction: {instruction}",
|
|
63
|
+
"Return JSON matching this schema:",
|
|
64
|
+
schema,
|
|
65
|
+
"Context:",
|
|
66
|
+
body,
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _request_decision(self, prompt: str, context: Dict[str, Any]) -> AgentDecision:
|
|
71
|
+
try:
|
|
72
|
+
raw = self._provider.generate_decision(prompt, context)
|
|
73
|
+
return AgentDecision.from_json(raw)
|
|
74
|
+
except DecisionError as exc:
|
|
75
|
+
return fallback_decision(f"Invalid agent output: {exc}")
|
|
76
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
77
|
+
return fallback_decision(f"Provider error: {exc}")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Agent decision schema and validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
ALLOWED_ACTIONS = {"revise_environment", "retry", "stop", "explain"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DecisionError(ValueError):
|
|
13
|
+
"""Raised when an agent decision fails validation."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AgentDecision:
|
|
18
|
+
action: str
|
|
19
|
+
reason: str
|
|
20
|
+
changes: List[Dict[str, Any]] = field(default_factory=list)
|
|
21
|
+
retry: bool = False
|
|
22
|
+
confidence: float = 0.0
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_dict(cls, payload: Dict[str, Any]) -> "AgentDecision":
|
|
26
|
+
if not isinstance(payload, dict):
|
|
27
|
+
raise DecisionError("Decision payload must be an object.")
|
|
28
|
+
|
|
29
|
+
action = payload.get("action")
|
|
30
|
+
reason = payload.get("reason")
|
|
31
|
+
changes = payload.get("changes", [])
|
|
32
|
+
retry = payload.get("retry", False)
|
|
33
|
+
confidence = payload.get("confidence", 0.0)
|
|
34
|
+
|
|
35
|
+
if action not in ALLOWED_ACTIONS:
|
|
36
|
+
raise DecisionError(f"Invalid action: {action}")
|
|
37
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
38
|
+
raise DecisionError("Decision must include a non-empty reason.")
|
|
39
|
+
if not isinstance(changes, list):
|
|
40
|
+
raise DecisionError("Changes must be a list.")
|
|
41
|
+
if not isinstance(retry, bool):
|
|
42
|
+
raise DecisionError("Retry must be a boolean.")
|
|
43
|
+
if not isinstance(confidence, (int, float)):
|
|
44
|
+
raise DecisionError("Confidence must be a number.")
|
|
45
|
+
|
|
46
|
+
confidence_value = float(confidence)
|
|
47
|
+
if confidence_value < 0.0 or confidence_value > 1.0:
|
|
48
|
+
raise DecisionError("Confidence must be between 0 and 1.")
|
|
49
|
+
|
|
50
|
+
return cls(
|
|
51
|
+
action=action,
|
|
52
|
+
reason=reason.strip(),
|
|
53
|
+
changes=changes,
|
|
54
|
+
retry=retry,
|
|
55
|
+
confidence=confidence_value,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_json(cls, raw: str) -> "AgentDecision":
|
|
60
|
+
try:
|
|
61
|
+
payload = json.loads(raw)
|
|
62
|
+
except json.JSONDecodeError as exc:
|
|
63
|
+
raise DecisionError(f"Invalid JSON output: {exc}") from exc
|
|
64
|
+
return cls.from_dict(payload)
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
67
|
+
return {
|
|
68
|
+
"action": self.action,
|
|
69
|
+
"reason": self.reason,
|
|
70
|
+
"changes": self.changes,
|
|
71
|
+
"retry": self.retry,
|
|
72
|
+
"confidence": self.confidence,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fallback_decision(message: str) -> AgentDecision:
|
|
77
|
+
return AgentDecision(
|
|
78
|
+
action="explain",
|
|
79
|
+
reason=message,
|
|
80
|
+
changes=[],
|
|
81
|
+
retry=False,
|
|
82
|
+
confidence=0.0,
|
|
83
|
+
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from .. import __version__
|
|
10
|
+
from ..config.settings import load_settings, settings_summary
|
|
11
|
+
from ..supervisor.supervisor import run_supervisor
|
|
12
|
+
from ..tui.flows import run_guided_flow
|
|
13
|
+
from ..utils.logging import setup_logging
|
|
14
|
+
from ..storage.paths import report_path
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(add_completion=False, help="RepoResolve - agentic supervisor system")
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _print_settings() -> None:
|
|
21
|
+
settings = load_settings()
|
|
22
|
+
summary = settings_summary(settings)
|
|
23
|
+
print("Current configuration:")
|
|
24
|
+
for key, value in summary.items():
|
|
25
|
+
print(f"- {key}: {value}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _not_implemented(feature: str) -> int:
|
|
29
|
+
print(f"{feature} is not implemented yet (Phase 1 skeleton).")
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _handle_start() -> int:
|
|
34
|
+
try:
|
|
35
|
+
state = run_guided_flow()
|
|
36
|
+
if state is None:
|
|
37
|
+
return 0
|
|
38
|
+
result = run_supervisor(state)
|
|
39
|
+
console.print(Panel(json.dumps(result, indent=2), title="Final Result", expand=False))
|
|
40
|
+
return 0
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
console.print(Panel(f"Unexpected error: {exc}", title="Error", style="red"))
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _handle_resume() -> int:
|
|
47
|
+
path = report_path()
|
|
48
|
+
if not path.exists():
|
|
49
|
+
console.print(Panel("No previous report found.", title="Resume"))
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
54
|
+
except json.JSONDecodeError:
|
|
55
|
+
console.print(Panel("Report file is invalid JSON.", title="Resume"))
|
|
56
|
+
return 1
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
console.print(Panel(f"Failed to read report: {exc}", title="Resume", style="red"))
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
history = data.get("history", [])
|
|
62
|
+
result = data.get("result", {})
|
|
63
|
+
|
|
64
|
+
console.print(Panel(f"Attempts: {len(history)}", title="Previous Session"))
|
|
65
|
+
if history:
|
|
66
|
+
last = history[-1]
|
|
67
|
+
decision = last.get("decision", {})
|
|
68
|
+
console.print(
|
|
69
|
+
Panel(
|
|
70
|
+
json.dumps(decision, indent=2),
|
|
71
|
+
title="Last Decision",
|
|
72
|
+
expand=False,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
console.print(Panel(json.dumps(result, indent=2), title="Last Result", expand=False))
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _handle_doctor() -> int:
|
|
80
|
+
return _not_implemented("Doctor checks")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _handle_config() -> int:
|
|
84
|
+
_print_settings()
|
|
85
|
+
print("Config wizard is not implemented yet (Phase 1 skeleton).")
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _handle_version() -> int:
|
|
90
|
+
print(__version__)
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@app.callback(invoke_without_command=True)
|
|
95
|
+
def _main(ctx: typer.Context) -> None:
|
|
96
|
+
setup_logging()
|
|
97
|
+
if ctx.invoked_subcommand is None:
|
|
98
|
+
raise typer.Exit(code=_handle_start())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command()
|
|
102
|
+
def start() -> None:
|
|
103
|
+
"""Start guided flow."""
|
|
104
|
+
raise typer.Exit(code=_handle_start())
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command()
|
|
108
|
+
def config() -> None:
|
|
109
|
+
"""Configure provider and model."""
|
|
110
|
+
raise typer.Exit(code=_handle_config())
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command()
|
|
114
|
+
def resume() -> None:
|
|
115
|
+
"""Resume the last session."""
|
|
116
|
+
raise typer.Exit(code=_handle_resume())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command()
|
|
120
|
+
def doctor() -> None:
|
|
121
|
+
"""Run system checks."""
|
|
122
|
+
raise typer.Exit(code=_handle_doctor())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command()
|
|
126
|
+
def version() -> None:
|
|
127
|
+
"""Show version."""
|
|
128
|
+
raise typer.Exit(code=_handle_version())
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_dotenv_if_available(path: Path) -> None:
|
|
8
|
+
try:
|
|
9
|
+
from dotenv import load_dotenv # type: ignore
|
|
10
|
+
except Exception:
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
if path.exists():
|
|
14
|
+
load_dotenv(path, override=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_env_file(path: Path) -> Dict[str, str]:
|
|
18
|
+
if not path.exists():
|
|
19
|
+
return {}
|
|
20
|
+
|
|
21
|
+
values: Dict[str, str] = {}
|
|
22
|
+
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
23
|
+
line = raw_line.strip()
|
|
24
|
+
if not line or line.startswith("#"):
|
|
25
|
+
continue
|
|
26
|
+
if "=" not in line:
|
|
27
|
+
continue
|
|
28
|
+
key, value = line.split("=", 1)
|
|
29
|
+
values[key.strip()] = value.strip().strip('"').strip("'")
|
|
30
|
+
return values
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
|
|
9
|
+
from .env import load_dotenv_if_available, load_env_file
|
|
10
|
+
from ..storage.paths import config_file_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Settings:
|
|
15
|
+
provider: Optional[str] = None
|
|
16
|
+
model: Optional[str] = None
|
|
17
|
+
openai_api_key: Optional[str] = None
|
|
18
|
+
anthropic_api_key: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _read_config_file(path: Path) -> Dict[str, str]:
|
|
22
|
+
if not path.exists():
|
|
23
|
+
return {}
|
|
24
|
+
try:
|
|
25
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
26
|
+
except json.JSONDecodeError:
|
|
27
|
+
return {}
|
|
28
|
+
if not isinstance(data, dict):
|
|
29
|
+
return {}
|
|
30
|
+
return {str(k): str(v) for k, v in data.items()}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_settings() -> Settings:
|
|
34
|
+
env_file = Path.cwd() / ".env"
|
|
35
|
+
load_dotenv_if_available(env_file)
|
|
36
|
+
env_values = load_env_file(env_file)
|
|
37
|
+
|
|
38
|
+
config_values = _read_config_file(config_file_path())
|
|
39
|
+
|
|
40
|
+
merged: Dict[str, str] = {}
|
|
41
|
+
merged.update(env_values)
|
|
42
|
+
merged.update(config_values)
|
|
43
|
+
merged.update(os.environ)
|
|
44
|
+
|
|
45
|
+
return Settings(
|
|
46
|
+
provider=merged.get("REPORESOLVE_PROVIDER") or merged.get("provider"),
|
|
47
|
+
model=merged.get("REPORESOLVE_MODEL") or merged.get("model"),
|
|
48
|
+
openai_api_key=merged.get("OPENAI_API_KEY") or merged.get("openai_api_key"),
|
|
49
|
+
anthropic_api_key=merged.get("ANTHROPIC_API_KEY")
|
|
50
|
+
or merged.get("anthropic_api_key"),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _mask_secret(value: Optional[str]) -> str:
|
|
55
|
+
if not value:
|
|
56
|
+
return "(not set)"
|
|
57
|
+
if len(value) <= 4:
|
|
58
|
+
return "****"
|
|
59
|
+
return "*" * (len(value) - 4) + value[-4:]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def settings_summary(settings: Settings) -> Dict[str, str]:
|
|
63
|
+
return {
|
|
64
|
+
"provider": settings.provider or "(not set)",
|
|
65
|
+
"model": settings.model or "(not set)",
|
|
66
|
+
"openai_api_key": _mask_secret(settings.openai_api_key),
|
|
67
|
+
"anthropic_api_key": _mask_secret(settings.anthropic_api_key),
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Anthropic provider implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
from .base import BaseProvider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _mock_decision(reason: str) -> str:
|
|
12
|
+
return json.dumps(
|
|
13
|
+
{
|
|
14
|
+
"action": "explain",
|
|
15
|
+
"reason": reason,
|
|
16
|
+
"changes": [],
|
|
17
|
+
"retry": False,
|
|
18
|
+
"confidence": 0.0,
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AnthropicProvider(BaseProvider):
|
|
24
|
+
def generate_decision(self, prompt: str, context: Dict[str, Any]) -> str:
|
|
25
|
+
if not self.api_key:
|
|
26
|
+
return _mock_decision("Anthropic API key not configured.")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import anthropic # type: ignore
|
|
30
|
+
except Exception:
|
|
31
|
+
return _mock_decision("Anthropic SDK not installed; returning mock decision.")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
client = anthropic.Anthropic(api_key=self.api_key)
|
|
35
|
+
model = self.model or "claude-sonnet-4-6"
|
|
36
|
+
message = client.messages.create(
|
|
37
|
+
model=model,
|
|
38
|
+
max_tokens=512,
|
|
39
|
+
messages=[{"role": "user", "content": prompt}],
|
|
40
|
+
)
|
|
41
|
+
text = None
|
|
42
|
+
if hasattr(message, "content") and message.content:
|
|
43
|
+
text = message.content[0].text
|
|
44
|
+
if not text:
|
|
45
|
+
return _mock_decision("Anthropic response missing text output.")
|
|
46
|
+
return text
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
return _mock_decision(f"Anthropic call failed: {exc}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Provider interface for agent decisions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseProvider(ABC):
|
|
10
|
+
def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None) -> None:
|
|
11
|
+
self.api_key = api_key
|
|
12
|
+
self.model = model
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def generate_decision(self, prompt: str, context: Dict[str, Any]) -> str:
|
|
16
|
+
"""Return a JSON string matching the AgentDecision schema."""
|
|
17
|
+
raise NotImplementedError
|