consolidate workspace to uv monorepo

This commit is contained in:
bryan
2026-02-03 17:47:57 -08:00
parent 3cca41aab1
commit ea30e5c631
17 changed files with 136 additions and 142 deletions
@@ -218,19 +218,9 @@ class OnlineResearchAgent:
tool_registry = ToolRegistry() tool_registry = ToolRegistry()
# Load MCP servers (always load, needed for tool validation) # Load MCP servers (always load, needed for tool validation)
agent_dir = Path(__file__).parent mcp_config_path = Path(__file__).parent / "mcp_servers.json"
mcp_config_path = agent_dir / "mcp_servers.json"
if mcp_config_path.exists(): if mcp_config_path.exists():
with open(mcp_config_path) as f: tool_registry.load_mcp_config(mcp_config_path)
mcp_servers = json.load(f)
for server_config in mcp_servers.get("servers", []):
# Resolve relative cwd paths
cwd = server_config.get("cwd")
if cwd and not Path(cwd).is_absolute():
server_config["cwd"] = str(agent_dir / cwd)
tool_registry.register_mcp_server(server_config)
llm = None llm = None
if not mock_mode: if not mock_mode:
+1 -2
View File
@@ -83,8 +83,7 @@ jobs:
run: | run: |
cd tools cd tools
uv sync --extra dev uv sync --extra dev
uv pip install --python .venv/bin/python -e ../core uv run pytest tests/ -v
uv run --extra dev pytest tests/ -v
validate: validate:
name: Validate Agent Exports name: Validate Agent Exports
+6 -12
View File
@@ -1,20 +1,14 @@
{ {
"mcpServers": { "mcpServers": {
"agent-builder": { "agent-builder": {
"command": ".venv/bin/python", "command": "uv",
"args": ["-m", "framework.mcp.agent_builder_server"], "args": ["run", "-m", "framework.mcp.agent_builder_server"],
"cwd": "core", "cwd": "core"
"env": {
"PYTHONPATH": "../tools/src"
}
}, },
"tools": { "tools": {
"command": ".venv/bin/python", "command": "uv",
"args": ["mcp_server.py", "--stdio"], "args": ["run", "mcp_server.py", "--stdio"],
"cwd": "tools", "cwd": "tools"
"env": {
"PYTHONPATH": "src:../core"
}
} }
} }
} }
+1 -1
View File
@@ -18,7 +18,7 @@ def _get_api_key_from_credential_store() -> str | None:
try: try:
from aden_tools.credentials import CredentialStoreAdapter from aden_tools.credentials import CredentialStoreAdapter
creds = CredentialStoreAdapter.with_env_storage() creds = CredentialStoreAdapter.default()
if creds.is_available("anthropic"): if creds.is_available("anthropic"):
return creds.get("anthropic") return creds.get("anthropic")
except ImportError: except ImportError:
+2 -19
View File
@@ -411,25 +411,8 @@ class AgentRunner:
return self._tool_registry.register_mcp_server(server_config) return self._tool_registry.register_mcp_server(server_config)
def _load_mcp_servers_from_config(self, config_path: Path) -> None: def _load_mcp_servers_from_config(self, config_path: Path) -> None:
""" """Load and register MCP servers from a configuration file."""
Load and register MCP servers from a configuration file. self._tool_registry.load_mcp_config(config_path)
Args:
config_path: Path to mcp_servers.json file
"""
try:
with open(config_path) as f:
config = json.load(f)
servers = config.get("servers", [])
for server_config in servers:
try:
self._tool_registry.register_mcp_server(server_config)
except Exception as e:
server_name = server_config.get("name", "unknown")
logger.warning(f"Failed to register MCP server '{server_name}': {e}")
except Exception as e:
logger.warning(f"Failed to load MCP servers config from {config_path}: {e}")
def set_approval_callback(self, callback: Callable) -> None: def set_approval_callback(self, callback: Callable) -> None:
""" """
+43 -4
View File
@@ -257,6 +257,34 @@ class ToolRegistry:
""" """
self._session_context.update(context) self._session_context.update(context)
def load_mcp_config(self, config_path: Path) -> None:
"""
Load and register MCP servers from a config file.
Resolves relative ``cwd`` paths against the config file's parent
directory so callers never need to handle path resolution themselves.
Args:
config_path: Path to an ``mcp_servers.json`` file.
"""
try:
with open(config_path) as f:
config = json.load(f)
except Exception as e:
logger.warning(f"Failed to load MCP config from {config_path}: {e}")
return
base_dir = config_path.parent
for server_config in config.get("servers", []):
cwd = server_config.get("cwd")
if cwd and not Path(cwd).is_absolute():
server_config["cwd"] = str((base_dir / cwd).resolve())
try:
self.register_mcp_server(server_config)
except Exception as e:
name = server_config.get("name", "unknown")
logger.warning(f"Failed to register MCP server '{name}': {e}")
def register_mcp_server( def register_mcp_server(
self, self,
server_config: dict[str, Any], server_config: dict[str, Any],
@@ -309,11 +337,21 @@ class ToolRegistry:
tool = self._convert_mcp_tool_to_framework_tool(mcp_tool) tool = self._convert_mcp_tool_to_framework_tool(mcp_tool)
# Create executor that calls the MCP server # Create executor that calls the MCP server
def make_mcp_executor(client_ref: MCPClient, tool_name: str, registry_ref): def make_mcp_executor(
client_ref: MCPClient,
tool_name: str,
registry_ref,
tool_params: set[str],
):
def executor(inputs: dict) -> Any: def executor(inputs: dict) -> Any:
try: try:
# Inject session context for tools that need it # Only inject session context params the tool accepts
merged_inputs = {**registry_ref._session_context, **inputs} filtered_context = {
k: v
for k, v in registry_ref._session_context.items()
if k in tool_params
}
merged_inputs = {**filtered_context, **inputs}
result = client_ref.call_tool(tool_name, merged_inputs) result = client_ref.call_tool(tool_name, merged_inputs)
# MCP tools return content array, extract the result # MCP tools return content array, extract the result
if isinstance(result, list) and len(result) > 0: if isinstance(result, list) and len(result) > 0:
@@ -327,10 +365,11 @@ class ToolRegistry:
return executor return executor
tool_params = set(mcp_tool.input_schema.get("properties", {}).keys())
self.register( self.register(
mcp_tool.name, mcp_tool.name,
tool, tool,
make_mcp_executor(client, mcp_tool.name, self), make_mcp_executor(client, mcp_tool.name, self, tool_params),
) )
count += 1 count += 1
+2 -2
View File
@@ -24,7 +24,7 @@ def _get_api_key():
# 1. Try CredentialStoreAdapter for Anthropic # 1. Try CredentialStoreAdapter for Anthropic
try: try:
from aden_tools.credentials import CredentialStoreAdapter from aden_tools.credentials import CredentialStoreAdapter
creds = CredentialStoreAdapter.with_env_storage() creds = CredentialStoreAdapter.default()
if creds.is_available("anthropic"): if creds.is_available("anthropic"):
return creds.get("anthropic") return creds.get("anthropic")
except (ImportError, KeyError): except (ImportError, KeyError):
@@ -57,7 +57,7 @@ def _get_api_key():
"""Get API key from CredentialStoreAdapter or environment.""" """Get API key from CredentialStoreAdapter or environment."""
try: try:
from aden_tools.credentials import CredentialStoreAdapter from aden_tools.credentials import CredentialStoreAdapter
creds = CredentialStoreAdapter.with_env_storage() creds = CredentialStoreAdapter.default()
if creds.is_available("anthropic"): if creds.is_available("anthropic"):
return creds.get("anthropic") return creds.get("anthropic")
except (ImportError, KeyError): except (ImportError, KeyError):
+4
View File
@@ -14,6 +14,7 @@ dependencies = [
"pytest>=8.0", "pytest>=8.0",
"pytest-asyncio>=0.23", "pytest-asyncio>=0.23",
"pytest-xdist>=3.0", "pytest-xdist>=3.0",
"tools",
] ]
# [project.optional-dependencies] # [project.optional-dependencies]
@@ -21,6 +22,9 @@ dependencies = [
[project.scripts] [project.scripts]
hive = "framework.cli:main" hive = "framework.cli:main"
[tool.uv.sources]
tools = { workspace = true }
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
+2
View File
@@ -0,0 +1,2 @@
[tool.uv.workspace]
members = ["core", "tools"]
+18 -74
View File
@@ -183,41 +183,26 @@ echo ""
echo -e "${DIM}This may take a minute...${NC}" echo -e "${DIM}This may take a minute...${NC}"
echo "" echo ""
# Install framework package from core/ # Install all workspace packages (core + tools) from workspace root
echo -n " Installing framework... " echo -n " Installing workspace packages... "
cd "$SCRIPT_DIR/core" cd "$SCRIPT_DIR"
if [ -f "pyproject.toml" ]; then if [ -f "pyproject.toml" ]; then
if uv sync > /dev/null 2>&1; then if uv sync > /dev/null 2>&1; then
echo -e "${GREEN}framework package installed${NC}" echo -e "${GREEN}workspace packages installed${NC}"
else else
echo -e "${YELLOW} ⚠ framework installation had issues (may be OK)${NC}" echo -e "${RED} ✗ workspace installation failed${NC}"
fi
else
echo -e "${RED}failed (no pyproject.toml)${NC}"
exit 1
fi
# Install aden_tools package from tools/
echo -n " Installing tools... "
cd "$SCRIPT_DIR/tools"
if [ -f "pyproject.toml" ]; then
if uv sync > /dev/null 2>&1; then
echo -e "${GREEN} ✓ aden_tools package installed${NC}"
else
echo -e "${RED} ✗ aden_tools installation failed${NC}"
exit 1 exit 1
fi fi
else else
echo -e "${RED}failed${NC}" echo -e "${RED}failed (no root pyproject.toml)${NC}"
exit 1 exit 1
fi fi
# Install Playwright browser # Install Playwright browser
echo -n " Installing Playwright browser... " echo -n " Installing Playwright browser... "
if $PYTHON_CMD -c "import playwright" > /dev/null 2>&1; then if uv run python -c "import playwright" > /dev/null 2>&1; then
if $PYTHON_CMD -m playwright install chromium > /dev/null 2>&1; then if uv run python -m playwright install chromium > /dev/null 2>&1; then
echo -e "${GREEN}ok${NC}" echo -e "${GREEN}ok${NC}"
else else
echo -e "${YELLOW}${NC}" echo -e "${YELLOW}${NC}"
@@ -236,33 +221,6 @@ echo ""
# ============================================================ # ============================================================
echo -e "${YELLOW}${NC} ${BLUE}${BOLD}Step 3: Configuring LLM provider...${NC}" echo -e "${YELLOW}${NC} ${BLUE}${BOLD}Step 3: Configuring LLM provider...${NC}"
# Install MCP dependencies (in tools venv)
echo " Installing MCP dependencies..."
TOOLS_PYTHON="$SCRIPT_DIR/tools/.venv/bin/python"
uv pip install --python "$TOOLS_PYTHON" mcp fastmcp > /dev/null 2>&1
echo -e "${GREEN} ✓ MCP dependencies installed${NC}"
# Fix openai version compatibility (in tools venv)
TOOLS_PYTHON="$SCRIPT_DIR/tools/.venv/bin/python"
OPENAI_VERSION=$($TOOLS_PYTHON -c "import openai; print(openai.__version__)" 2>/dev/null || echo "not_installed")
if [ "$OPENAI_VERSION" = "not_installed" ]; then
echo " Installing openai package..."
uv pip install --python "$TOOLS_PYTHON" "openai>=1.0.0" > /dev/null 2>&1
echo -e "${GREEN} ✓ openai installed${NC}"
elif [[ "$OPENAI_VERSION" =~ ^0\. ]]; then
echo " Upgrading openai to 1.x+ for litellm compatibility..."
uv pip install --python "$TOOLS_PYTHON" --upgrade "openai>=1.0.0" > /dev/null 2>&1
echo -e "${GREEN} ✓ openai upgraded${NC}"
else
echo -e "${GREEN} ✓ openai $OPENAI_VERSION is compatible${NC}"
fi
# Install click for CLI (in tools venv)
TOOLS_PYTHON="$SCRIPT_DIR/tools/.venv/bin/python"
uv pip install --python "$TOOLS_PYTHON" click > /dev/null 2>&1
echo -e "${GREEN} ✓ click installed${NC}"
cd "$SCRIPT_DIR"
echo "" echo ""
# ============================================================ # ============================================================
@@ -274,42 +232,28 @@ echo ""
IMPORT_ERRORS=0 IMPORT_ERRORS=0
# Test imports using their respective venvs # Test imports using workspace venv via uv run
CORE_PYTHON="$SCRIPT_DIR/core/.venv/bin/python" if uv run python -c "import framework" > /dev/null 2>&1; then
TOOLS_PYTHON="$SCRIPT_DIR/tools/.venv/bin/python"
# Test framework import (from core venv)
if [ -f "$CORE_PYTHON" ] && $CORE_PYTHON -c "import framework" > /dev/null 2>&1; then
echo -e "${GREEN} ✓ framework imports OK${NC}" echo -e "${GREEN} ✓ framework imports OK${NC}"
else else
echo -e "${RED} ✗ framework import failed${NC}" echo -e "${RED} ✗ framework import failed${NC}"
IMPORT_ERRORS=$((IMPORT_ERRORS + 1)) IMPORT_ERRORS=$((IMPORT_ERRORS + 1))
fi fi
# Test aden_tools import (from tools venv) if uv run python -c "import aden_tools" > /dev/null 2>&1; then
if [ -f "$TOOLS_PYTHON" ] && $TOOLS_PYTHON -c "import aden_tools" > /dev/null 2>&1; then
echo -e "${GREEN} ✓ aden_tools imports OK${NC}" echo -e "${GREEN} ✓ aden_tools imports OK${NC}"
else else
echo -e "${RED} ✗ aden_tools import failed${NC}" echo -e "${RED} ✗ aden_tools import failed${NC}"
IMPORT_ERRORS=$((IMPORT_ERRORS + 1)) IMPORT_ERRORS=$((IMPORT_ERRORS + 1))
fi fi
# Test litellm import (from core venv) if uv run python -c "import litellm" > /dev/null 2>&1; then
if [ -f "$CORE_PYTHON" ] && $CORE_PYTHON -c "import litellm" > /dev/null 2>&1; then echo -e "${GREEN} ✓ litellm imports OK${NC}"
echo -e "${GREEN} ✓ litellm imports OK (core)${NC}"
else else
echo -e "${YELLOW} ⚠ litellm import issues in core (may be OK)${NC}" echo -e "${YELLOW} ⚠ litellm import issues (may be OK)${NC}"
fi fi
# Test litellm import (from tools venv) if uv run python -c "from framework.mcp import agent_builder_server" > /dev/null 2>&1; then
if [ -f "$TOOLS_PYTHON" ] && $TOOLS_PYTHON -c "import litellm" > /dev/null 2>&1; then
echo -e "${GREEN} ✓ litellm imports OK (tools)${NC}"
else
echo -e "${YELLOW} ⚠ litellm import issues in tools (may be OK)${NC}"
fi
# Test MCP server module (from core venv)
if [ -f "$CORE_PYTHON" ] && $CORE_PYTHON -c "from framework.mcp import agent_builder_server" > /dev/null 2>&1; then
echo -e "${GREEN} ✓ MCP server module OK${NC}" echo -e "${GREEN} ✓ MCP server module OK${NC}"
else else
echo -e "${RED} ✗ MCP server module failed${NC}" echo -e "${RED} ✗ MCP server module failed${NC}"
@@ -647,7 +591,7 @@ ERRORS=0
# Test imports # Test imports
echo -n " ⬡ framework... " echo -n " ⬡ framework... "
if $CORE_PYTHON -c "import framework" > /dev/null 2>&1; then if uv run python -c "import framework" > /dev/null 2>&1; then
echo -e "${GREEN}ok${NC}" echo -e "${GREEN}ok${NC}"
else else
echo -e "${RED}failed${NC}" echo -e "${RED}failed${NC}"
@@ -655,7 +599,7 @@ else
fi fi
echo -n " ⬡ aden_tools... " echo -n " ⬡ aden_tools... "
if $TOOLS_PYTHON -c "import aden_tools" > /dev/null 2>&1; then if uv run python -c "import aden_tools" > /dev/null 2>&1; then
echo -e "${GREEN}ok${NC}" echo -e "${GREEN}ok${NC}"
else else
echo -e "${RED}failed${NC}" echo -e "${RED}failed${NC}"
@@ -663,7 +607,7 @@ else
fi fi
echo -n " ⬡ litellm... " echo -n " ⬡ litellm... "
if $CORE_PYTHON -c "import litellm" > /dev/null 2>&1 || $TOOLS_PYTHON -c "import litellm" > /dev/null 2>&1; then if uv run python -c "import litellm" > /dev/null 2>&1; then
echo -e "${GREEN}ok${NC}" echo -e "${GREEN}ok${NC}"
else else
echo -e "${YELLOW}--${NC}" echo -e "${YELLOW}--${NC}"
+1 -12
View File
@@ -68,18 +68,7 @@ from starlette.responses import PlainTextResponse # noqa: E402
from aden_tools.credentials import CredentialError, CredentialStoreAdapter # noqa: E402 from aden_tools.credentials import CredentialError, CredentialStoreAdapter # noqa: E402
from aden_tools.tools import register_all_tools # noqa: E402 from aden_tools.tools import register_all_tools # noqa: E402
# Create credential store with access to both env vars AND encrypted store credentials = CredentialStoreAdapter.default()
# This allows using Aden-synced credentials from ~/.hive/credentials
try:
from framework.credentials import CredentialStore
store = CredentialStore.with_encrypted_storage() # ~/.hive/credentials
credentials = CredentialStoreAdapter(store)
logger.info("Using CredentialStoreAdapter with encrypted storage")
except Exception as e:
# Fall back to env-only adapter if encrypted storage fails
credentials = CredentialStoreAdapter.with_env_storage()
logger.warning(f"Falling back to env-only CredentialStoreAdapter: {e}")
# Tier 1: Validate startup-required credentials (if any) # Tier 1: Validate startup-required credentials (if any)
try: try:
+4
View File
@@ -30,6 +30,7 @@ dependencies = [
"playwright-stealth>=1.0.5", "playwright-stealth>=1.0.5",
"litellm>=1.81.0", "litellm>=1.81.0",
"resend>=2.0.0", "resend>=2.0.0",
"framework",
] ]
[project.optional-dependencies] [project.optional-dependencies]
@@ -54,6 +55,9 @@ all = [
"duckdb>=1.0.0", "duckdb>=1.0.0",
] ]
[tool.uv.sources]
framework = { workspace = true }
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
+1 -1
View File
@@ -10,7 +10,7 @@ Usage:
from aden_tools.credentials import CredentialStoreAdapter from aden_tools.credentials import CredentialStoreAdapter
mcp = FastMCP("my-server") mcp = FastMCP("my-server")
credentials = CredentialStoreAdapter.with_env_storage() credentials = CredentialStoreAdapter.default()
register_all_tools(mcp, credentials=credentials) register_all_tools(mcp, credentials=credentials)
""" """
+2 -2
View File
@@ -15,8 +15,8 @@ Usage:
store = CredentialStore.with_encrypted_storage() # defaults to ~/.hive/credentials store = CredentialStore.with_encrypted_storage() # defaults to ~/.hive/credentials
credentials = CredentialStoreAdapter(store) credentials = CredentialStoreAdapter(store)
# With env vars only (simple setup) # With composite storage (encrypted primary + env fallback)
credentials = CredentialStoreAdapter.with_env_storage() credentials = CredentialStoreAdapter.default()
# In agent runner (validate at agent load time) # In agent runner (validate at agent load time)
credentials.validate_for_tools(["web_search", "file_read"]) credentials.validate_for_tools(["web_search", "file_read"])
@@ -348,6 +348,41 @@ class CredentialStoreAdapter:
# --- Factory Methods --- # --- Factory Methods ---
@classmethod
def default(
cls,
specs: dict[str, CredentialSpec] | None = None,
) -> CredentialStoreAdapter:
"""Create adapter with encrypted storage primary and env var fallback."""
from framework.credentials import CredentialStore
from framework.credentials.storage import (
CompositeStorage,
EncryptedFileStorage,
EnvVarStorage,
)
if specs is None:
from . import CREDENTIAL_SPECS
specs = CREDENTIAL_SPECS
env_mapping = {name: spec.env_var for name, spec in specs.items()}
try:
encrypted = EncryptedFileStorage()
env = EnvVarStorage(env_mapping)
composite = CompositeStorage(primary=encrypted, fallbacks=[env])
store = CredentialStore(storage=composite)
except Exception as e:
import logging
logging.getLogger(__name__).warning(
"Encrypted credential storage unavailable, falling back to env vars: %s", e
)
store = CredentialStore.with_env_storage(env_mapping)
return cls(store=store, specs=specs)
@classmethod @classmethod
def for_testing( def for_testing(
cls, cls,
+1 -1
View File
@@ -7,7 +7,7 @@ Usage:
from aden_tools.credentials import CredentialStoreAdapter from aden_tools.credentials import CredentialStoreAdapter
mcp = FastMCP("my-server") mcp = FastMCP("my-server")
credentials = CredentialStoreAdapter.with_env_storage() credentials = CredentialStoreAdapter.default()
register_all_tools(mcp, credentials=credentials) register_all_tools(mcp, credentials=credentials)
""" """
+11
View File
@@ -10,6 +10,17 @@ from aden_tools.credentials import (
) )
@pytest.fixture(autouse=True)
def _no_dotenv(tmp_path, monkeypatch):
"""Isolate tests from the project .env file.
EnvVarStorage falls back to reading Path.cwd()/.env when a key is
missing from os.environ. Changing cwd to a temp dir ensures
monkeypatch.delenv() truly simulates a missing credential.
"""
monkeypatch.chdir(tmp_path)
class TestCredentialStoreAdapter: class TestCredentialStoreAdapter:
"""Tests for CredentialStoreAdapter class.""" """Tests for CredentialStoreAdapter class."""