Skip to content

Commit 2fd9ae7

Browse files
committed
Learning Claude Code
1 parent 27605e3 commit 2fd9ae7

47 files changed

Lines changed: 1729 additions & 14014 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coverage

-68 KB
Binary file not shown.

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,19 @@ yarn.lock
8989
.DS_Store
9090
Thumbs.db
9191
.tmp/
92+
_pytest_session_tmp/
93+
_pytest_case_tmp/
9294

9395
# Runtime workflow artifacts
9496
memory/moirai_runs/wf_*.json
97+
memory/moirai_runs/opro_*.json
98+
memory/moirai_runs/*.json
99+
100+
# Runtime user config/state (generated locally)
101+
config/users/
102+
103+
# Test coverage artifacts
104+
.coverage
105+
coverage.xml
106+
htmlcov/
95107

README.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Governance: [`GOVERNANCE.md`](GOVERNANCE.md)
3434
Maintainers: [`MAINTAINERS.md`](MAINTAINERS.md)
3535
Roadmap: [`ROADMAP.md`](ROADMAP.md)
3636
Deployment: [`docs/deployment.md`](docs/deployment.md)
37+
User setup (copy-and-run): [`docs/getting-started/real-user-setup.md`](docs/getting-started/real-user-setup.md)
3738

3839
## Why Promethea?
3940

@@ -134,10 +135,10 @@ pip install -r requirements.txt
134135

135136
```bash
136137
# Windows
137-
copy example.env .env
138+
copy env.example .env
138139

139140
# macOS / Linux
140-
cp example.env .env
141+
cp env.example .env
141142
```
142143

143144
Open `.env` and set **at minimum these three fields** (they are coupled — all three must match your provider):
@@ -161,12 +162,7 @@ API__MODEL=your-local-model-id
161162

162163
> ⚠️ `API__BASE_URL` and `API__MODEL` must match. Changing only the key will not work.
163164
164-
Choose a memory backend (start here if you want zero external dependencies):
165-
166-
```bash
167-
MEMORY__ENABLED=true
168-
MEMORY__STORE_BACKEND=sqlite_graph # or flat_memory
169-
```
165+
Non-sensitive runtime options (including memory backend, default `neo4j`) come from `config/default.json`.
170166

171167
### 3. Run
172168

UI/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -540,8 +540,8 @@ <h3 id="settingsSectionSystemTitle">System</h3>
540540
<div class="form-group">
541541
<label for="logLevel">Log Level</label>
542542
<select id="logLevel" name="system.log_level">
543-
<option value="DEBUG">DEBUG</option>
544543
<option value="INFO">INFO</option>
544+
<option value="DEBUG">DEBUG</option>
545545
<option value="WARNING">WARNING</option>
546546
<option value="ERROR">ERROR</option>
547547
</select>
@@ -664,7 +664,7 @@ <h3 id="settingsSectionMemoryTitle">Memory</h3>
664664
<button id="quickAskBtn" class="quick-ask-btn">Follow-up</button>
665665
</div>
666666

667-
<script src="https://d3js.org/d3.v7.min.js"></script>
667+
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
668668
<script src="script.js?v=20260317a" charset="UTF-8"></script>
669669
<script src="voice.js?v=20260309a" charset="UTF-8"></script>
670670
</body>

UI/script.js

Lines changed: 271 additions & 110 deletions
Large diffs are not rendered by default.

agentkit/tools/web/websearch.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010
logger = logging.getLogger(__name__)
1111

1212

13+
def _resolve_ddgs_class():
14+
try:
15+
from ddgs import DDGS
16+
17+
return DDGS
18+
except ImportError:
19+
from duckduckgo_search import DDGS
20+
21+
return DDGS
22+
23+
1324
class WebSearchService:
1425
"""Simple DuckDuckGo search wrapper with sandbox-aware network guard."""
1526

@@ -25,7 +36,12 @@ def _ensure_network_allowed(self) -> Optional[str]:
2536
return f"Sandbox blocked web search: {decision.reason}"
2637
return None
2738

28-
async def search(self, query: str, max_results: Optional[int] = None) -> str:
39+
async def search(
40+
self,
41+
query: str,
42+
max_results: Optional[int] = None,
43+
num_results: Optional[int] = None,
44+
) -> str:
2945
"""Run a general web search."""
3046
blocked = self._ensure_network_allowed()
3147
if blocked:
@@ -34,10 +50,11 @@ async def search(self, query: str, max_results: Optional[int] = None) -> str:
3450
if not query or not query.strip():
3551
return "Error: query cannot be empty"
3652

37-
limit = max_results if max_results and max_results > 0 else self.max_results
53+
requested = max_results if max_results and max_results > 0 else num_results
54+
limit = requested if requested and requested > 0 else self.max_results
3855

3956
try:
40-
from duckduckgo_search import DDGS
57+
DDGS = _resolve_ddgs_class()
4158

4259
logger.info("Search: %s (max=%s)", query, limit)
4360
results = []
@@ -60,7 +77,7 @@ async def search(self, query: str, max_results: Optional[int] = None) -> str:
6077
)
6178
return "\n".join(formatted)
6279
except ImportError:
63-
return "Missing dependency: install duckduckgo-search"
80+
return "Missing dependency: install ddgs"
6481
except Exception as e:
6582
logger.error("Search failed: %s", e)
6683
return f"Error: search failed: {e}"
@@ -71,7 +88,7 @@ async def quick_answer(self, query: str) -> str:
7188
if blocked:
7289
return blocked
7390
try:
74-
from duckduckgo_search import DDGS
91+
DDGS = _resolve_ddgs_class()
7592

7693
with DDGS() as ddgs:
7794
answers = list(ddgs.answers(query))
@@ -90,15 +107,21 @@ async def quick_answer(self, query: str) -> str:
90107
logger.error("Quick answer failed: %s", e)
91108
return await self.search(query, max_results=3)
92109

93-
async def news_search(self, query: str, max_results: Optional[int] = None) -> str:
110+
async def news_search(
111+
self,
112+
query: str,
113+
max_results: Optional[int] = None,
114+
num_results: Optional[int] = None,
115+
) -> str:
94116
"""Search news results."""
95117
blocked = self._ensure_network_allowed()
96118
if blocked:
97119
return blocked
98120
try:
99-
from duckduckgo_search import DDGS
121+
DDGS = _resolve_ddgs_class()
100122

101-
limit = max_results if max_results and max_results > 0 else self.max_results
123+
requested = max_results if max_results and max_results > 0 else num_results
124+
limit = requested if requested and requested > 0 else self.max_results
102125
results = []
103126
with DDGS() as ddgs:
104127
for result in ddgs.news(query, max_results=limit):
@@ -122,7 +145,7 @@ async def news_search(self, query: str, max_results: Optional[int] = None) -> st
122145
)
123146
return "\n".join(formatted)
124147
except ImportError:
125-
return "Missing dependency: install duckduckgo-search"
148+
return "Missing dependency: install ddgs"
126149
except Exception as e:
127150
logger.error("News search failed: %s", e)
128151
return f"Error: news search failed: {e}"

config.py

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
from pydantic_settings import BaseSettings, SettingsConfigDict
1010
from dotenv import dotenv_values
1111

12+
PROJECT_ROOT = Path(__file__).resolve().parent
13+
ENV_FILE = PROJECT_ROOT / ".env"
14+
DEFAULT_CONFIG_PATH = PROJECT_ROOT / "config" / "default.json"
15+
LEGACY_CONFIG_PATH = PROJECT_ROOT / "config.json"
16+
1217

1318
class SystemConfig(BaseSettings):
1419
version: str = Field(default="1.0")
15-
base_dir: Path = Field(default_factory=lambda: Path(__file__).parent)
16-
log_dir: Path = Field(default_factory=lambda: Path(__file__).parent / "logs")
20+
base_dir: Path = Field(default_factory=lambda: PROJECT_ROOT)
21+
log_dir: Path = Field(default_factory=lambda: PROJECT_ROOT / "logs")
1722
stream_mode: bool = Field(default=True)
1823
debug: bool = Field(default=False)
1924
log_level: str = Field(default="INFO")
@@ -297,7 +302,7 @@ class PrometheaConfig(BaseSettings):
297302
sandbox: SandboxConfig = Field(default_factory=SandboxConfig)
298303

299304
model_config = SettingsConfigDict(
300-
env_file=".env",
305+
env_file=str(ENV_FILE),
301306
env_file_encoding="utf-8",
302307
env_nested_delimiter="__",
303308
extra="ignore",
@@ -324,36 +329,70 @@ def _set_nested_value(target: dict, path: tuple[str, ...], value: Any) -> None:
324329
current[path[-1]] = value
325330

326331

327-
def _overlay_explicit_env_values(merged_data: dict, base_from_env: PrometheaConfig) -> None:
328-
env_map = {
329-
("api", "api_key"): "API__API_KEY",
330-
("api", "model"): "API__MODEL",
331-
("api", "failover_models"): "API__FAILOVER_MODELS",
332-
("memory", "api", "api_key"): "MEMORY__API__API_KEY",
333-
("memory", "neo4j", "password"): "MEMORY__NEO4J__PASSWORD",
334-
}
332+
def _resolve_key_case_insensitive(payload: dict, key: str) -> Optional[str]:
333+
if key in payload:
334+
return key
335+
key_lower = key.lower()
336+
for existing in payload.keys():
337+
if str(existing).lower() == key_lower:
338+
return str(existing)
339+
return None
340+
341+
342+
def _get_nested_value_ci(payload: dict, path: tuple[str, ...]) -> tuple[bool, Any]:
343+
current: Any = payload
344+
for segment in path:
345+
if not isinstance(current, dict):
346+
return False, None
347+
resolved = _resolve_key_case_insensitive(current, segment)
348+
if resolved is None:
349+
return False, None
350+
current = current.get(resolved)
351+
return True, current
352+
353+
354+
def _set_nested_value_ci(payload: dict, path: tuple[str, ...], value: Any) -> bool:
355+
current: Any = payload
356+
for segment in path[:-1]:
357+
if not isinstance(current, dict):
358+
return False
359+
resolved = _resolve_key_case_insensitive(current, segment)
360+
if resolved is None:
361+
return False
362+
current = current.get(resolved)
363+
if not isinstance(current, dict):
364+
return False
365+
last = _resolve_key_case_insensitive(current, path[-1])
366+
if last is None:
367+
return False
368+
current[last] = value
369+
return True
370+
335371

372+
def _overlay_explicit_env_values(merged_data: dict, base_from_env: PrometheaConfig) -> None:
336373
env_data = base_from_env.model_dump()
337374
explicit_env_keys = set(os.environ.keys())
338-
env_file = dotenv_values(".env")
375+
env_file = dotenv_values(str(ENV_FILE)) if ENV_FILE.exists() else {}
339376
explicit_env_keys.update(str(k) for k in env_file.keys() if k)
340-
for path, env_name in env_map.items():
341-
if env_name not in explicit_env_keys:
377+
for env_name in sorted(explicit_env_keys):
378+
if "__" not in env_name:
342379
continue
343-
344-
value: Any = env_data
345-
for segment in path:
346-
value = value[segment]
347-
_set_nested_value(merged_data, path, value)
380+
path = tuple(seg for seg in str(env_name).split("__") if seg)
381+
if not path:
382+
continue
383+
found, value = _get_nested_value_ci(env_data, path)
384+
if not found:
385+
continue
386+
_set_nested_value_ci(merged_data, path, value)
348387

349388

350389
def load_config() -> PrometheaConfig:
351390
base_from_env = PrometheaConfig()
352391
merged_data = base_from_env.model_dump()
353392

354-
config_path = Path("config/default.json")
393+
config_path = DEFAULT_CONFIG_PATH
355394
if not config_path.exists():
356-
legacy_path = Path("config.json")
395+
legacy_path = LEGACY_CONFIG_PATH
357396
config_path = legacy_path if legacy_path.exists() else None
358397

359398
if config_path and config_path.exists():

0 commit comments

Comments
 (0)