acra-cli 0.1.1__py3-none-any.whl
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.
- acra/__init__.py +7 -0
- acra/__main__.py +4 -0
- acra/agents/__init__.py +0 -0
- acra/agents/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/agents/__pycache__/llm.cpython-312.pyc +0 -0
- acra/agents/__pycache__/llm_utils.cpython-312.pyc +0 -0
- acra/agents/coder/__init__.py +0 -0
- acra/agents/coder/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/agents/coder/__pycache__/coder_agent.cpython-312.pyc +0 -0
- acra/agents/coder/__pycache__/coder_chain.cpython-312.pyc +0 -0
- acra/agents/coder/coder_agent.py +235 -0
- acra/agents/coder/coder_chain.py +204 -0
- acra/agents/critic/__init__.py +0 -0
- acra/agents/critic/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/agents/critic/__pycache__/critic_agent.cpython-312.pyc +0 -0
- acra/agents/critic/__pycache__/critic_chain.cpython-312.pyc +0 -0
- acra/agents/critic/critic_agent.py +199 -0
- acra/agents/critic/critic_chain.py +268 -0
- acra/agents/executor/__init__.py +0 -0
- acra/agents/executor/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/agents/executor/__pycache__/docker_runner.cpython-312.pyc +0 -0
- acra/agents/executor/__pycache__/executor_agent.cpython-312.pyc +0 -0
- acra/agents/executor/__pycache__/sandbox_runner.cpython-312.pyc +0 -0
- acra/agents/executor/docker_runner.py +114 -0
- acra/agents/executor/executor_agent.py +142 -0
- acra/agents/executor/sandbox_runner.py +144 -0
- acra/agents/human/__init__.py +0 -0
- acra/agents/human/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/agents/human/__pycache__/human_node.cpython-312.pyc +0 -0
- acra/agents/human/human_node.py +37 -0
- acra/agents/llm.py +263 -0
- acra/agents/llm_utils.py +161 -0
- acra/agents/memory/__init__.py +0 -0
- acra/agents/memory/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/agents/memory/__pycache__/memory_agent.cpython-312.pyc +0 -0
- acra/agents/memory/__pycache__/memory_manager.cpython-312.pyc +0 -0
- acra/agents/memory/__pycache__/retrieval.cpython-312.pyc +0 -0
- acra/agents/memory/memory_agent.py +239 -0
- acra/agents/memory/memory_manager.py +273 -0
- acra/agents/memory/retrieval.py +355 -0
- acra/agents/planner/__init__.py +0 -0
- acra/agents/planner/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/agents/planner/__pycache__/planner_agent.cpython-312.pyc +0 -0
- acra/agents/planner/__pycache__/planner_chain.cpython-312.pyc +0 -0
- acra/agents/planner/planner_agent.py +97 -0
- acra/agents/planner/planner_chain.py +160 -0
- acra/agents/researcher/__init__.py +1 -0
- acra/agents/researcher/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/agents/researcher/__pycache__/researcher_agent.cpython-312.pyc +0 -0
- acra/agents/researcher/__pycache__/researcher_chain.cpython-312.pyc +0 -0
- acra/agents/researcher/researcher_agent.py +231 -0
- acra/agents/researcher/researcher_chain.py +193 -0
- acra/brain/__init__.py +6 -0
- acra/brain/model_registry.py +19 -0
- acra/brain/provider_manager.py +21 -0
- acra/cli.py +52 -0
- acra/commands/__init__.py +29 -0
- acra/commands/ask.py +49 -0
- acra/commands/brain.py +58 -0
- acra/commands/config.py +25 -0
- acra/commands/context.py +30 -0
- acra/commands/graph.py +18 -0
- acra/commands/keys.py +40 -0
- acra/commands/logs.py +13 -0
- acra/commands/memory.py +27 -0
- acra/commands/plugin.py +18 -0
- acra/commands/research.py +65 -0
- acra/commands/serve.py +13 -0
- acra/commands/session.py +20 -0
- acra/commands/utils.py +12 -0
- acra/config/__init__.py +36 -0
- acra/config/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/config/__pycache__/defaults.cpython-312.pyc +0 -0
- acra/config/__pycache__/profile_manager.cpython-312.pyc +0 -0
- acra/config/defaults.py +88 -0
- acra/config/profile_manager.py +87 -0
- acra/execution/__init__.py +1 -0
- acra/execution/logs/__init__.py +1 -0
- acra/execution/logs/error_logs/__init__.py +1 -0
- acra/execution/logs/execution_logs/__init__.py +1 -0
- acra/execution/sandbox/__init__.py +1 -0
- acra/execution/sandbox/docker_executor.py +209 -0
- acra/execution/sandbox/isolated_runner.py +148 -0
- acra/execution/sandbox/sandbox_manager.py +178 -0
- acra/graph/__init__.py +0 -0
- acra/graph/checkpoint.py +85 -0
- acra/graph/conditional_edges.py +103 -0
- acra/graph/edges.py +85 -0
- acra/graph/graph_visualizer.py +175 -0
- acra/graph/nodes.py +68 -0
- acra/graph/router.py +69 -0
- acra/graph/state.py +93 -0
- acra/graph/workflow.py +117 -0
- acra/memory/__init__.py +1 -0
- acra/memory/checkpoints/__init__.py +1 -0
- acra/memory/checkpoints/data/__init__.py +1 -0
- acra/memory/checkpoints/postgres_checkpoint.py +247 -0
- acra/memory/checkpoints/sqlite_checkpoint.py +309 -0
- acra/memory/chroma_db/__init__.py +1 -0
- acra/memory/conversation_memory/__init__.py +1 -0
- acra/memory/conversation_memory/long_term.py +229 -0
- acra/memory/conversation_memory/short_term.py +158 -0
- acra/memory/storage/__init__.py +1 -0
- acra/memory/vector_store/__init__.py +0 -0
- acra/memory/vector_store/chroma_store.py +260 -0
- acra/memory/vector_store/embeddings.py +43 -0
- acra/observability/__init__.py +1 -0
- acra/observability/langsmith_tracing.py +91 -0
- acra/observability/metrics.py +189 -0
- acra/observability/monitoring.py +162 -0
- acra/observability/token_tracking.py +131 -0
- acra/prompts/__init__.py +1 -0
- acra/schemas/__init__.py +0 -0
- acra/schemas/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/schemas/__pycache__/coder_schema.cpython-312.pyc +0 -0
- acra/schemas/__pycache__/critic_schema.cpython-312.pyc +0 -0
- acra/schemas/__pycache__/execution_schema.cpython-312.pyc +0 -0
- acra/schemas/__pycache__/planner_schema.cpython-312.pyc +0 -0
- acra/schemas/__pycache__/researcher_schema.cpython-312.pyc +0 -0
- acra/schemas/__pycache__/shared_schema.cpython-312.pyc +0 -0
- acra/schemas/coder_schema.py +257 -0
- acra/schemas/critic_schema.py +177 -0
- acra/schemas/execution_schema.py +74 -0
- acra/schemas/planner_schema.py +129 -0
- acra/schemas/researcher_schema.py +220 -0
- acra/schemas/shared_schema.py +329 -0
- acra/tools/__init__.py +1 -0
- acra/tools/code_tools/__init__.py +1 -0
- acra/tools/code_tools/file_reader.py +156 -0
- acra/tools/code_tools/file_writer.py +108 -0
- acra/tools/code_tools/python_repl.py +72 -0
- acra/tools/code_tools/terminal_runner.py +98 -0
- acra/tools/github_tools/__init__.py +1 -0
- acra/tools/github_tools/commit_generator.py +91 -0
- acra/tools/github_tools/github_search.py +113 -0
- acra/tools/github_tools/repo_analyzer.py +138 -0
- acra/tools/integration_test_helper.py +370 -0
- acra/tools/validation_tools/__init__.py +1 -0
- acra/tools/validation_tools/dependency_checker.py +101 -0
- acra/tools/validation_tools/environment_config_validator.py +353 -0
- acra/tools/validation_tools/http_validator.py +336 -0
- acra/tools/validation_tools/response_validator.py +407 -0
- acra/tools/validation_tools/security_checker.py +93 -0
- acra/tools/validation_tools/syntax_checker.py +75 -0
- acra/tools/validation_tools/web_framework_validator.py +382 -0
- acra/tools/web_tools/__init__.py +1 -0
- acra/tools/web_tools/arxiv_search.py +110 -0
- acra/tools/web_tools/serpapi_search.py +106 -0
- acra/tools/web_tools/tavily_search.py +96 -0
- acra/ui/__init__.py +8 -0
- acra/ui/__pycache__/__init__.cpython-312.pyc +0 -0
- acra/ui/__pycache__/banner.cpython-312.pyc +0 -0
- acra/ui/__pycache__/components.cpython-312.pyc +0 -0
- acra/ui/__pycache__/shell.cpython-312.pyc +0 -0
- acra/ui/__pycache__/theme.cpython-312.pyc +0 -0
- acra/ui/banner.py +40 -0
- acra/ui/components.py +32 -0
- acra/ui/shell.py +91 -0
- acra/ui/theme.py +28 -0
- acra/utils/__init__.py +21 -0
- acra/utils/context_manager.py +20 -0
- acra/utils/keyring_manager.py +83 -0
- acra/utils/output_formatter.py +18 -0
- acra/utils/session_manager.py +59 -0
- acra_cli-0.1.1.dist-info/METADATA +616 -0
- acra_cli-0.1.1.dist-info/RECORD +169 -0
- acra_cli-0.1.1.dist-info/WHEEL +5 -0
- acra_cli-0.1.1.dist-info/licenses/LICENSE +661 -0
- acra_cli-0.1.1.dist-info/top_level.txt +1 -0
|
Binary file
|
|
Binary file
|
acra/ui/banner.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Welcome banner and UI panels for acra."""
|
|
2
|
+
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
MASCOT = r"""
|
|
7
|
+
____ ____ ____
|
|
8
|
+
/ __ \ / __ \/ __ \
|
|
9
|
+
| | | | | | | | | |
|
|
10
|
+
| | | | | | | | | |
|
|
11
|
+
| |__| | |__| | |__| |
|
|
12
|
+
\____/ \____/ \____/
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
WELCOME_TEXT = "acra: agentic CLI for LLM task workflows"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def render_banner(provider: str, model: str, profile: str, workspace: str, theme: dict) -> Panel:
|
|
19
|
+
console = Console()
|
|
20
|
+
lines = [
|
|
21
|
+
f"[bold {theme['accent']}]acra v0.1.0[/bold {theme['accent']}]",
|
|
22
|
+
f"Provider: {provider} · Model: {model}",
|
|
23
|
+
f"Profile: {profile} · Workspace: {workspace}",
|
|
24
|
+
]
|
|
25
|
+
content = "\n".join(lines)
|
|
26
|
+
return Panel(content, title=WELCOME_TEXT, subtitle=MASCOT, style=theme["border"], border_style=theme["border"])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def render_tips(theme: dict) -> Panel:
|
|
30
|
+
tips = (
|
|
31
|
+
"• Run acra init to set up your workspace\n"
|
|
32
|
+
"• Run acra brain models to list available models\n"
|
|
33
|
+
"• Use / to open commands palette\n"
|
|
34
|
+
"• Use @ to pick files for context"
|
|
35
|
+
)
|
|
36
|
+
return Panel(tips, title="Getting Started", style=theme["border"], border_style=theme["accent"])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Compatibility alias for older imports and public package API.
|
|
40
|
+
welcome_banner = render_banner
|
acra/ui/components.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Shared UI render components for acra."""
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def render_panel(content: Any, title: str = "acra") -> None:
|
|
12
|
+
if isinstance(content, list):
|
|
13
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
14
|
+
table.add_column("Name")
|
|
15
|
+
table.add_column("Status")
|
|
16
|
+
table.add_column("Value")
|
|
17
|
+
for item in content:
|
|
18
|
+
if isinstance(item, dict):
|
|
19
|
+
table.add_row(
|
|
20
|
+
str(item.get("name", "")),
|
|
21
|
+
str(item.get("status", "")),
|
|
22
|
+
str(item.get("value", "")),
|
|
23
|
+
)
|
|
24
|
+
else:
|
|
25
|
+
table.add_row(str(item), "", "")
|
|
26
|
+
console.print(Panel(table, title=title))
|
|
27
|
+
else:
|
|
28
|
+
console.print(Panel(str(content), title=title))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_message(message: str) -> None:
|
|
32
|
+
console.print(message)
|
acra/ui/shell.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Interactive shell implementation for acra."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from prompt_toolkit import PromptSession
|
|
9
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
10
|
+
from prompt_toolkit.shortcuts import CompleteStyle
|
|
11
|
+
from acra.ui.banner import render_banner, render_tips
|
|
12
|
+
from acra.ui.theme import get_theme
|
|
13
|
+
from acra.config.profile_manager import ProfileManager
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _slash_completions():
|
|
21
|
+
commands = ["serve", "init", "config show", "keys list", "research", "memory list", "session list", "graph show", "brain models"]
|
|
22
|
+
for command in commands:
|
|
23
|
+
yield Completion(command, start_position=0)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SlashCompleter(Completer):
|
|
27
|
+
def get_completions(self, document, complete_event):
|
|
28
|
+
text = document.text_before_cursor
|
|
29
|
+
if text.startswith("/"):
|
|
30
|
+
for completion in _slash_completions():
|
|
31
|
+
yield completion
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def launch_shell(profile: str = None, workspace: str = None):
|
|
35
|
+
config = ProfileManager().load_profile(profile)
|
|
36
|
+
theme = get_theme(config.get("theme", "dark-orange"))
|
|
37
|
+
banner = render_banner(
|
|
38
|
+
config.get("provider", "gemini"),
|
|
39
|
+
config.get("model", "gemini-2.5-flash"),
|
|
40
|
+
profile or "default",
|
|
41
|
+
workspace or config.get("workspace", "."),
|
|
42
|
+
theme,
|
|
43
|
+
)
|
|
44
|
+
console.print(banner)
|
|
45
|
+
console.print(render_tips(theme))
|
|
46
|
+
session = PromptSession(
|
|
47
|
+
"> "
|
|
48
|
+
)
|
|
49
|
+
while True:
|
|
50
|
+
try:
|
|
51
|
+
user_input = session.prompt(
|
|
52
|
+
"> ",
|
|
53
|
+
completer=SlashCompleter(),
|
|
54
|
+
complete_style=CompleteStyle.COLUMN,
|
|
55
|
+
)
|
|
56
|
+
if user_input in ("exit", "quit", "q", "Ctrl+C"):
|
|
57
|
+
break
|
|
58
|
+
if not user_input.strip():
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
command_text = user_input.strip()
|
|
62
|
+
if command_text.startswith("acra "):
|
|
63
|
+
command_text = command_text[len("acra "):]
|
|
64
|
+
|
|
65
|
+
if command_text in ("exit", "quit", "q"):
|
|
66
|
+
break
|
|
67
|
+
|
|
68
|
+
console.print(f"[dim]→[/dim] {user_input}")
|
|
69
|
+
|
|
70
|
+
cmd = [sys.executable, "-m", "acra"] + shlex.split(command_text)
|
|
71
|
+
completed = subprocess.run(
|
|
72
|
+
cmd,
|
|
73
|
+
cwd=os.getcwd(),
|
|
74
|
+
stdin=sys.stdin,
|
|
75
|
+
stdout=subprocess.PIPE,
|
|
76
|
+
stderr=subprocess.STDOUT,
|
|
77
|
+
text=True,
|
|
78
|
+
)
|
|
79
|
+
if completed.stdout:
|
|
80
|
+
console.print(completed.stdout, soft_wrap=True)
|
|
81
|
+
if completed.returncode != 0:
|
|
82
|
+
if completed.stdout and "Missing command" in completed.stdout:
|
|
83
|
+
console.print("[dim]Tip:[/dim] try a subcommand such as 'brain models' or 'config init'.")
|
|
84
|
+
else:
|
|
85
|
+
console.print(f"[bold red]Command failed with exit code {completed.returncode}.[/bold red]")
|
|
86
|
+
except KeyboardInterrupt:
|
|
87
|
+
break
|
|
88
|
+
except EOFError:
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
console.print(Panel("Goodbye", title="acra"))
|
acra/ui/theme.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""acra theme registry."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
THEMES: Dict[str, Dict[str, str]] = {
|
|
6
|
+
"dark-orange": {
|
|
7
|
+
"accent": "bright_yellow",
|
|
8
|
+
"border": "dark_orange",
|
|
9
|
+
"text": "white",
|
|
10
|
+
"info": "bright_blue",
|
|
11
|
+
},
|
|
12
|
+
"dark-blue": {
|
|
13
|
+
"accent": "bright_cyan",
|
|
14
|
+
"border": "blue",
|
|
15
|
+
"text": "white",
|
|
16
|
+
"info": "bright_white",
|
|
17
|
+
},
|
|
18
|
+
"dark-green": {
|
|
19
|
+
"accent": "green",
|
|
20
|
+
"border": "bright_green",
|
|
21
|
+
"text": "white",
|
|
22
|
+
"info": "bright_white",
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_theme(name: str) -> Dict[str, str]:
|
|
28
|
+
return THEMES.get(name, THEMES["dark-orange"])
|
acra/utils/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Utility helpers for acra."""
|
|
2
|
+
|
|
3
|
+
from .keyring_manager import (
|
|
4
|
+
delete_key,
|
|
5
|
+
get_key,
|
|
6
|
+
get_key_status,
|
|
7
|
+
get_research_key_status,
|
|
8
|
+
list_keys,
|
|
9
|
+
set_key,
|
|
10
|
+
)
|
|
11
|
+
from .output_formatter import format_research_report
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"delete_key",
|
|
15
|
+
"get_key",
|
|
16
|
+
"get_key_status",
|
|
17
|
+
"get_research_key_status",
|
|
18
|
+
"list_keys",
|
|
19
|
+
"set_key",
|
|
20
|
+
"format_research_report",
|
|
21
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Context management utilities for acra."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ContextManager:
|
|
9
|
+
"""Simple context storage for acra CLI commands."""
|
|
10
|
+
|
|
11
|
+
_context_items: List[str] = []
|
|
12
|
+
|
|
13
|
+
def add(self, item: str) -> None:
|
|
14
|
+
self._context_items.append(item)
|
|
15
|
+
|
|
16
|
+
def list(self) -> List[str]:
|
|
17
|
+
return list(self._context_items)
|
|
18
|
+
|
|
19
|
+
def clear(self) -> None:
|
|
20
|
+
self._context_items.clear()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Key management using OS keyring for acra."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import keyring
|
|
8
|
+
except ImportError: # pragma: no cover
|
|
9
|
+
keyring = None
|
|
10
|
+
|
|
11
|
+
SERVICE_MAIN = "acra"
|
|
12
|
+
SERVICE_RESEARCH = "acra-research"
|
|
13
|
+
KNOWN_RESEARCH_SOURCES = ["web", "github", "docs", "arxiv"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _service_and_username(name: str) -> (str, str):
|
|
17
|
+
if name.startswith("research."):
|
|
18
|
+
return SERVICE_RESEARCH, name.split("research.", 1)[1]
|
|
19
|
+
return SERVICE_MAIN, name
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _mask_value(value: Optional[str]) -> str:
|
|
23
|
+
if not value:
|
|
24
|
+
return "not set"
|
|
25
|
+
if len(value) <= 10:
|
|
26
|
+
return value
|
|
27
|
+
return f"{value[:6]}...{value[-4:]}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _env_key(name: str) -> Optional[str]:
|
|
31
|
+
if name.startswith("research."):
|
|
32
|
+
source = name.split("research.", 1)[1].upper()
|
|
33
|
+
return os.getenv(f"ACRA_RESEARCH_{source}_KEY")
|
|
34
|
+
if name == "provider":
|
|
35
|
+
return os.getenv("ACRA_PROVIDER_KEY")
|
|
36
|
+
return os.getenv(name.upper())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def set_key(name: str, value: str) -> None:
|
|
40
|
+
if keyring is None:
|
|
41
|
+
raise RuntimeError("keyring is required for storing secrets")
|
|
42
|
+
service, username = _service_and_username(name)
|
|
43
|
+
keyring.set_password(service, username, value)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_key(name: str) -> Optional[str]:
|
|
47
|
+
if keyring is not None:
|
|
48
|
+
service, username = _service_and_username(name)
|
|
49
|
+
value = keyring.get_password(service, username)
|
|
50
|
+
if value:
|
|
51
|
+
return value
|
|
52
|
+
return _env_key(name)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def list_keys() -> List[Dict[str, str]]:
|
|
56
|
+
statuses = []
|
|
57
|
+
names = ["provider"] + [f"research.{source}" for source in KNOWN_RESEARCH_SOURCES]
|
|
58
|
+
for name in names:
|
|
59
|
+
value = get_key(name)
|
|
60
|
+
status = "configured" if value else "not set"
|
|
61
|
+
statuses.append({"name": name, "status": status, "value": _mask_value(value)})
|
|
62
|
+
return statuses
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def delete_key(name: str) -> None:
|
|
66
|
+
if keyring is None:
|
|
67
|
+
raise RuntimeError("keyring is required for deleting secrets")
|
|
68
|
+
service, username = _service_and_username(name)
|
|
69
|
+
keyring.delete_password(service, username)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_key_status(name: str) -> Dict[str, str]:
|
|
73
|
+
value = get_key(name)
|
|
74
|
+
return {
|
|
75
|
+
"name": name,
|
|
76
|
+
"configured": bool(value),
|
|
77
|
+
"masked": _mask_value(value),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_research_key_status(name: str) -> Dict[str, str]:
|
|
82
|
+
"""Alias for research-specific key status lookup."""
|
|
83
|
+
return get_key_status(name)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Output formatting utilities for acra."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_research_report(result: Dict[str, Any], output_format: str = "citations") -> Dict[str, Any]:
|
|
7
|
+
report = {
|
|
8
|
+
"query": result.get("user_request"),
|
|
9
|
+
"summary": result.get("research_summary", ""),
|
|
10
|
+
"findings": result.get("research_data", []),
|
|
11
|
+
"sources": result.get("research_sources", []),
|
|
12
|
+
"status": result.get("research_status", "unknown"),
|
|
13
|
+
}
|
|
14
|
+
if output_format == "summary":
|
|
15
|
+
report = {"query": report["query"], "summary": report["summary"]}
|
|
16
|
+
elif output_format == "detailed":
|
|
17
|
+
report["details"] = result
|
|
18
|
+
return report
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Session persistence for acra."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
SESSION_DIR = Path.home() / ".acra" / "sessions"
|
|
11
|
+
SESSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _session_path(session_id: str) -> Path:
|
|
15
|
+
return SESSION_DIR / f"{session_id}.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_session(data: Dict[str, Any], name: Optional[str] = None) -> str:
|
|
19
|
+
session_id = uuid.uuid4().hex[:8]
|
|
20
|
+
now = datetime.utcnow().isoformat() + "Z"
|
|
21
|
+
payload = {
|
|
22
|
+
"id": session_id,
|
|
23
|
+
"name": name or f"session-{session_id}",
|
|
24
|
+
"created_at": now,
|
|
25
|
+
"updated_at": now,
|
|
26
|
+
"data": data,
|
|
27
|
+
}
|
|
28
|
+
with open(_session_path(session_id), "w", encoding="utf-8") as f:
|
|
29
|
+
json.dump(payload, f, indent=2)
|
|
30
|
+
return session_id
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def list_sessions() -> List[Dict[str, Any]]:
|
|
34
|
+
sessions = []
|
|
35
|
+
for path in sorted(SESSION_DIR.glob("*.json")):
|
|
36
|
+
try:
|
|
37
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
38
|
+
sessions.append(json.load(f))
|
|
39
|
+
except Exception:
|
|
40
|
+
continue
|
|
41
|
+
return sessions
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_session(session_id: str) -> Optional[Dict[str, Any]]:
|
|
45
|
+
path = _session_path(session_id)
|
|
46
|
+
if not path.exists():
|
|
47
|
+
return None
|
|
48
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
49
|
+
return json.load(f)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def save_session(session_id: str, data: Dict[str, Any]) -> None:
|
|
53
|
+
payload = load_session(session_id)
|
|
54
|
+
if not payload:
|
|
55
|
+
raise FileNotFoundError(f"Session not found: {session_id}")
|
|
56
|
+
payload["data"] = data
|
|
57
|
+
payload["updated_at"] = datetime.utcnow().isoformat() + "Z"
|
|
58
|
+
with open(_session_path(session_id), "w", encoding="utf-8") as f:
|
|
59
|
+
json.dump(payload, f, indent=2)
|