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.
Files changed (45) hide show
  1. reporesolve-0.1.0/LICENSE +21 -0
  2. reporesolve-0.1.0/PKG-INFO +65 -0
  3. reporesolve-0.1.0/README.md +51 -0
  4. reporesolve-0.1.0/pyproject.toml +26 -0
  5. reporesolve-0.1.0/reporesolve/__init__.py +3 -0
  6. reporesolve-0.1.0/reporesolve/agent/__init__.py +1 -0
  7. reporesolve-0.1.0/reporesolve/agent/memory.py +16 -0
  8. reporesolve-0.1.0/reporesolve/agent/planner.py +77 -0
  9. reporesolve-0.1.0/reporesolve/agent/schema.py +83 -0
  10. reporesolve-0.1.0/reporesolve/cli/__init__.py +5 -0
  11. reporesolve-0.1.0/reporesolve/cli/main.py +132 -0
  12. reporesolve-0.1.0/reporesolve/config/__init__.py +1 -0
  13. reporesolve-0.1.0/reporesolve/config/env.py +30 -0
  14. reporesolve-0.1.0/reporesolve/config/settings.py +68 -0
  15. reporesolve-0.1.0/reporesolve/providers/__init__.py +1 -0
  16. reporesolve-0.1.0/reporesolve/providers/anthropic_provider.py +48 -0
  17. reporesolve-0.1.0/reporesolve/providers/base.py +17 -0
  18. reporesolve-0.1.0/reporesolve/providers/openai_provider.py +44 -0
  19. reporesolve-0.1.0/reporesolve/storage/__init__.py +1 -0
  20. reporesolve-0.1.0/reporesolve/storage/paths.py +27 -0
  21. reporesolve-0.1.0/reporesolve/supervisor/__init__.py +1 -0
  22. reporesolve-0.1.0/reporesolve/supervisor/state.py +17 -0
  23. reporesolve-0.1.0/reporesolve/supervisor/supervisor.py +293 -0
  24. reporesolve-0.1.0/reporesolve/supervisor/workflow.py +38 -0
  25. reporesolve-0.1.0/reporesolve/tools/__init__.py +1 -0
  26. reporesolve-0.1.0/reporesolve/tools/base.py +24 -0
  27. reporesolve-0.1.0/reporesolve/tools/build.py +21 -0
  28. reporesolve-0.1.0/reporesolve/tools/clone.py +88 -0
  29. reporesolve-0.1.0/reporesolve/tools/inspect.py +41 -0
  30. reporesolve-0.1.0/reporesolve/tools/install.py +21 -0
  31. reporesolve-0.1.0/reporesolve/tools/parse.py +131 -0
  32. reporesolve-0.1.0/reporesolve/tools/smoke.py +22 -0
  33. reporesolve-0.1.0/reporesolve/tui/__init__.py +1 -0
  34. reporesolve-0.1.0/reporesolve/tui/flows.py +38 -0
  35. reporesolve-0.1.0/reporesolve/tui/prompts.py +114 -0
  36. reporesolve-0.1.0/reporesolve/tui/render.py +34 -0
  37. reporesolve-0.1.0/reporesolve/utils/__init__.py +1 -0
  38. reporesolve-0.1.0/reporesolve/utils/logging.py +31 -0
  39. reporesolve-0.1.0/reporesolve.egg-info/PKG-INFO +65 -0
  40. reporesolve-0.1.0/reporesolve.egg-info/SOURCES.txt +43 -0
  41. reporesolve-0.1.0/reporesolve.egg-info/dependency_links.txt +1 -0
  42. reporesolve-0.1.0/reporesolve.egg-info/entry_points.txt +2 -0
  43. reporesolve-0.1.0/reporesolve.egg-info/requires.txt +4 -0
  44. reporesolve-0.1.0/reporesolve.egg-info/top_level.txt +1 -0
  45. 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,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
@@ -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,5 @@
1
+ """CLI package for RepoResolve."""
2
+
3
+ from .main import app
4
+
5
+ __all__ = ["app"]
@@ -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,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