Skip to content

Commit 5a7d41e

Browse files
committed
Harden orchestrator optional deps and packaging
1 parent 5328847 commit 5a7d41e

8 files changed

Lines changed: 78 additions & 16 deletions

File tree

orchestrator.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import json
1818
import asyncio
1919
import aiohttp
20+
import logging
2021
# Redis: Optional for MVP. PT uses file-based state (.state/agents.json) by default.
2122
# Redis enables distributed coordination for multi-instance deployments (v1.1+).
2223
# Soft import — app starts cleanly even if the redis package is not installed.
@@ -29,11 +30,37 @@
2930
from fastapi.middleware.trustedhost import TrustedHostMiddleware
3031
# fix(orchestrator): migrate from deprecated Pydantic V1 @validator to V2 @field_validator
3132
from pydantic import BaseModel, Field, field_validator
32-
from loguru import logger
33-
from dotenv import load_dotenv
34-
from slowapi import Limiter, _rate_limit_exceeded_handler
35-
from slowapi.util import get_remote_address
36-
from slowapi.errors import RateLimitExceeded
33+
try:
34+
from loguru import logger
35+
except ImportError:
36+
logger = logging.getLogger("perplexity_tools.orchestrator")
37+
try:
38+
from dotenv import load_dotenv
39+
except ImportError:
40+
def load_dotenv(*_args, **_kwargs):
41+
return False
42+
try:
43+
from slowapi import Limiter, _rate_limit_exceeded_handler
44+
from slowapi.util import get_remote_address
45+
from slowapi.errors import RateLimitExceeded
46+
except ImportError:
47+
class RateLimitExceeded(Exception):
48+
pass
49+
50+
def _rate_limit_exceeded_handler(*_args, **_kwargs):
51+
raise RateLimitExceeded("slowapi is not installed")
52+
53+
def get_remote_address(_request):
54+
return "local"
55+
56+
class Limiter:
57+
def __init__(self, *args, **kwargs):
58+
pass
59+
60+
def limit(self, _rule):
61+
def decorator(fn):
62+
return fn
63+
return decorator
3764

3865
load_dotenv()
3966

orchestrator/fastapi_app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ def ecc_sync(force: bool = Query(False)) -> Dict[str, Any]:
106106

107107
@app.get("/health", tags=["system"])
108108
def health(
109-
ollama_host: str = Query("http://127.0.0.1:11434"),
110-
lm_studio_host: str = Query("http://127.0.0.1:1234"),
111-
mlx_host: str = Query("http://127.0.0.1:8081"),
109+
ollama_host: str = "http://127.0.0.1:11434",
110+
lm_studio_host: str = "http://127.0.0.1:1234",
111+
mlx_host: str = "http://127.0.0.1:8081",
112112
) -> Dict[str, Any]:
113113
"""Backend connectivity health check — supports Mac+Win+shared Ollama."""
114114
return {

orchestrator/lan_discovery.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@
3131
try:
3232
import httpx
3333
except ImportError:
34-
print("[lan_discovery] ERROR: httpx not installed. Run: pip install httpx")
35-
sys.exit(1)
34+
httpx = None
3635

3736
# Common AI inference server ports
3837
DEFAULT_PORTS = [
@@ -114,6 +113,9 @@ async def _probe_endpoint(self, host: str, port: int) -> Optional[AIEndpoint]:
114113
Probe a single host:port for AI inference server.
115114
Returns AIEndpoint if found, None otherwise.
116115
"""
116+
if httpx is None:
117+
raise RuntimeError("httpx not installed. Run: pip install httpx")
118+
117119
base_url = f"http://{host}:{port}"
118120

119121
try:

orchestrator/ultrathink_mcp_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ async def _start(self) -> None:
5959
"initialize",
6060
{
6161
"protocolVersion": _MCP_PROTOCOL_VERSION,
62-
"clientInfo": {"name": "perplexity-tools", "version": "1.0-rc"},
62+
"clientInfo": {"name": "perplexity-tools", "version": "0.9.9.0"},
6363
"capabilities": {},
6464
},
6565
)

pyproject.toml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55

66
[build-system]
77
requires = ["setuptools>=68", "wheel"]
8-
build-backend = "setuptools.backends.legacy:build"
8+
build-backend = "setuptools.build_meta"
99

1010
[project]
1111
name = "perplexity-tools"
12-
dynamic = ["version"]
12+
version = "0.9.9.0"
1313
description = "Top-level idempotent multi-agent orchestrator: per-device model selection, fallback routing, cost guard, and ultrathink-system integration."
1414
readme = "README.md"
1515
license = { text = "AGPL-3.0" }
@@ -84,9 +84,6 @@ pt-orchestrator = "orchestrator.fastapi_app:app"
8484
pt-setup = "setup_wizard:main"
8585
pt-launch = "agent_launcher:main"
8686

87-
[tool.setuptools.dynamic]
88-
version = { attr = "orchestrator.__version__" }
89-
9087
[tool.setuptools.packages.find]
9188
where = ["."]
9289
include = ["orchestrator*"]

tests/test_fastapi_health.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from __future__ import annotations
2+
3+
from orchestrator import fastapi_app
4+
5+
6+
def test_health_uses_plain_string_defaults(monkeypatch):
7+
captured = {}
8+
9+
def fake_backend_health_map(*, ollama_host, lm_studio_host, mlx_host):
10+
captured["ollama_host"] = ollama_host
11+
captured["lm_studio_host"] = lm_studio_host
12+
captured["mlx_host"] = mlx_host
13+
return {"ok": True}
14+
15+
monkeypatch.setattr(fastapi_app, "backend_health_map", fake_backend_health_map)
16+
17+
response = fastapi_app.health()
18+
19+
assert response["status"] == "ok"
20+
assert captured == {
21+
"ollama_host": "http://127.0.0.1:11434",
22+
"lm_studio_host": "http://127.0.0.1:1234",
23+
"mlx_host": "http://127.0.0.1:8081",
24+
}

tests/test_lan_discovery.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from __future__ import annotations
22

3+
import asyncio
34
import json
45
import sys
56
from datetime import datetime
67
from pathlib import Path
78

9+
import pytest
10+
811

912
REPO_ROOT = Path(__file__).parent.parent
1013
sys.path.insert(0, str(REPO_ROOT))
@@ -48,3 +51,11 @@ def test_save_discovery_state_writes_timezone_aware_timestamp(tmp_path, monkeypa
4851

4952
state = json.loads(state_file.read_text())
5053
_assert_timezone_aware_utc(state["discovered_at"])
54+
55+
56+
def test_probe_endpoint_requires_httpx(monkeypatch):
57+
monkeypatch.setattr(lan_discovery, "httpx", None)
58+
discovery = lan_discovery.LANDiscovery(subnet="127.0.0.0/30", ports=[11434])
59+
60+
with pytest.raises(RuntimeError, match="httpx not installed"):
61+
asyncio.run(discovery._probe_endpoint("127.0.0.1", 11434))

tests/test_resilience.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def raise_permission_error(self, *args, **kwargs):
4242
def test_sync_returns_structured_error_when_vendor_clone_unavailable(monkeypatch):
4343
import orchestrator.ecc_tools_sync as sync_mod
4444

45+
monkeypatch.setattr(sync_mod, "ECC_SYNC_ENABLED", True)
4546
monkeypatch.setattr(sync_mod, "_ensure_cloned", lambda: False)
4647
result = sync_mod.sync_ecc_tools(force=False)
4748

0 commit comments

Comments
 (0)