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()
# Load MCP servers (always load, needed for tool validation)
agent_dir = Path(__file__).parent
mcp_config_path = agent_dir / "mcp_servers.json"
mcp_config_path = Path(__file__).parent / "mcp_servers.json"
if mcp_config_path.exists():
with open(mcp_config_path) as f:
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)
tool_registry.load_mcp_config(mcp_config_path)
llm = None
if not mock_mode:
+1 -2
View File
@@ -83,8 +83,7 @@ jobs:
run: |
cd tools
uv sync --extra dev
uv pip install --python .venv/bin/python -e ../core
uv run --extra dev pytest tests/ -v
uv run pytest tests/ -v
validate:
name: Validate Agent Exports
+6 -12
View File
@@ -1,20 +1,14 @@
{
"mcpServers": {
"agent-builder": {
"command": ".venv/bin/python",
"args": ["-m", "framework.mcp.agent_builder_server"],
"cwd": "core",
"env": {
"PYTHONPATH": "../tools/src"
}
"command": "uv",
"args": ["run", "-m", "framework.mcp.agent_builder_server"],
"cwd": "core"
},
"tools": {
"command": ".venv/bin/python",
"args": ["mcp_server.py", "--stdio"],
"cwd": "tools",
"env": {
"PYTHONPATH": "src:../core"
}
"command": "uv",
"args": ["run", "mcp_server.py", "--stdio"],
"cwd": "tools"
}
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ def _get_api_key_from_credential_store() -> str | None:
try:
from aden_tools.credentials import CredentialStoreAdapter
creds = CredentialStoreAdapter.with_env_storage()
creds = CredentialStoreAdapter.default()
if creds.is_available("anthropic"):
return creds.get("anthropic")
except ImportError:
+2 -19
View File
@@ -411,25 +411,8 @@ class AgentRunner:
return self._tool_registry.register_mcp_server(server_config)
def _load_mcp_servers_from_config(self, config_path: Path) -> None:
"""
Load and register MCP servers from a configuration file.
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}")
"""Load and register MCP servers from a configuration file."""
self._tool_registry.load_mcp_config(config_path)
def set_approval_callback(self, callback: Callable) -> None:
"""
+43 -4
View File
@@ -257,6 +257,34 @@ class ToolRegistry:
"""
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(
self,
server_config: dict[str, Any],
@@ -309,11 +337,21 @@ class ToolRegistry:
tool = self._convert_mcp_tool_to_framework_tool(mcp_tool)
# 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:
try:
# Inject session context for tools that need it
merged_inputs = {**registry_ref._session_context, **inputs}
# Only inject session context params the tool accepts
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)
# MCP tools return content array, extract the result
if isinstance(result, list) and len(result) > 0:
@@ -327,10 +365,11 @@ class ToolRegistry:
return executor
tool_params = set(mcp_tool.input_schema.get("properties", {}).keys())
self.register(
mcp_tool.name,
tool,
make_mcp_executor(client, mcp_tool.name, self),
make_mcp_executor(client, mcp_tool.name, self, tool_params),
)
count += 1
+2 -2
View File
@@ -24,7 +24,7 @@ def _get_api_key():
# 1. Try CredentialStoreAdapter for Anthropic
try:
from aden_tools.credentials import CredentialStoreAdapter
creds = CredentialStoreAdapter.with_env_storage()
creds = CredentialStoreAdapter.default()
if creds.is_available("anthropic"):
return creds.get("anthropic")
except (ImportError, KeyError):
@@ -57,7 +57,7 @@ def _get_api_key():
"""Get API key from CredentialStoreAdapter or environment."""
try:
from aden_tools.credentials import CredentialStoreAdapter
creds = CredentialStoreAdapter.with_env_storage()
creds = CredentialStoreAdapter.default()
if creds.is_available("anthropic"):
return creds.get("anthropic")
except (ImportError, KeyError):
+4
View File
@@ -14,6 +14,7 @@ dependencies = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"pytest-xdist>=3.0",
"tools",
]
# [project.optional-dependencies]
@@ -21,6 +22,9 @@ dependencies = [
[project.scripts]
hive = "framework.cli:main"
[tool.uv.sources]
tools = { workspace = true }
[build-system]
requires = ["hatchling"]
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 ""
# Install framework package from core/
echo -n " Installing framework... "
cd "$SCRIPT_DIR/core"
# Install all workspace packages (core + tools) from workspace root
echo -n " Installing workspace packages... "
cd "$SCRIPT_DIR"
if [ -f "pyproject.toml" ]; then
if uv sync > /dev/null 2>&1; then
echo -e "${GREEN}framework package installed${NC}"
echo -e "${GREEN}workspace packages installed${NC}"
else
echo -e "${YELLOW} ⚠ framework installation had issues (may be OK)${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}"
echo -e "${RED} ✗ workspace installation failed${NC}"
exit 1
fi
else
echo -e "${RED}failed${NC}"
echo -e "${RED}failed (no root pyproject.toml)${NC}"
exit 1
fi
# Install Playwright browser
echo -n " Installing Playwright browser... "
if $PYTHON_CMD -c "import playwright" > /dev/null 2>&1; then
if $PYTHON_CMD -m playwright install chromium > /dev/null 2>&1; then
if uv run python -c "import playwright" > /dev/null 2>&1; then
if uv run python -m playwright install chromium > /dev/null 2>&1; then
echo -e "${GREEN}ok${NC}"
else
echo -e "${YELLOW}${NC}"
@@ -236,33 +221,6 @@ echo ""
# ============================================================
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 ""
# ============================================================
@@ -274,42 +232,28 @@ echo ""
IMPORT_ERRORS=0
# Test imports using their respective venvs
CORE_PYTHON="$SCRIPT_DIR/core/.venv/bin/python"
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
# Test imports using workspace venv via uv run
if uv run python -c "import framework" > /dev/null 2>&1; then
echo -e "${GREEN} ✓ framework imports OK${NC}"
else
echo -e "${RED} ✗ framework import failed${NC}"
IMPORT_ERRORS=$((IMPORT_ERRORS + 1))
fi
# Test aden_tools import (from tools venv)
if [ -f "$TOOLS_PYTHON" ] && $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} ✓ aden_tools imports OK${NC}"
else
echo -e "${RED} ✗ aden_tools import failed${NC}"
IMPORT_ERRORS=$((IMPORT_ERRORS + 1))
fi
# Test litellm import (from core venv)
if [ -f "$CORE_PYTHON" ] && $CORE_PYTHON -c "import litellm" > /dev/null 2>&1; then
echo -e "${GREEN} ✓ litellm imports OK (core)${NC}"
if uv run python -c "import litellm" > /dev/null 2>&1; then
echo -e "${GREEN} ✓ litellm imports OK${NC}"
else
echo -e "${YELLOW} ⚠ litellm import issues in core (may be OK)${NC}"
echo -e "${YELLOW} ⚠ litellm import issues (may be OK)${NC}"
fi
# Test litellm import (from tools venv)
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
if uv run python -c "from framework.mcp import agent_builder_server" > /dev/null 2>&1; then
echo -e "${GREEN} ✓ MCP server module OK${NC}"
else
echo -e "${RED} ✗ MCP server module failed${NC}"
@@ -647,7 +591,7 @@ ERRORS=0
# Test imports
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}"
else
echo -e "${RED}failed${NC}"
@@ -655,7 +599,7 @@ else
fi
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}"
else
echo -e "${RED}failed${NC}"
@@ -663,7 +607,7 @@ else
fi
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}"
else
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.tools import register_all_tools # noqa: E402
# Create credential store with access to both env vars AND encrypted store
# 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}")
credentials = CredentialStoreAdapter.default()
# Tier 1: Validate startup-required credentials (if any)
try:
+4
View File
@@ -30,6 +30,7 @@ dependencies = [
"playwright-stealth>=1.0.5",
"litellm>=1.81.0",
"resend>=2.0.0",
"framework",
]
[project.optional-dependencies]
@@ -54,6 +55,9 @@ all = [
"duckdb>=1.0.0",
]
[tool.uv.sources]
framework = { workspace = true }
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
+1 -1
View File
@@ -10,7 +10,7 @@ Usage:
from aden_tools.credentials import CredentialStoreAdapter
mcp = FastMCP("my-server")
credentials = CredentialStoreAdapter.with_env_storage()
credentials = CredentialStoreAdapter.default()
register_all_tools(mcp, credentials=credentials)
"""
+2 -2
View File
@@ -15,8 +15,8 @@ Usage:
store = CredentialStore.with_encrypted_storage() # defaults to ~/.hive/credentials
credentials = CredentialStoreAdapter(store)
# With env vars only (simple setup)
credentials = CredentialStoreAdapter.with_env_storage()
# With composite storage (encrypted primary + env fallback)
credentials = CredentialStoreAdapter.default()
# In agent runner (validate at agent load time)
credentials.validate_for_tools(["web_search", "file_read"])
@@ -348,6 +348,41 @@ class CredentialStoreAdapter:
# --- 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
def for_testing(
cls,
+1 -1
View File
@@ -7,7 +7,7 @@ Usage:
from aden_tools.credentials import CredentialStoreAdapter
mcp = FastMCP("my-server")
credentials = CredentialStoreAdapter.with_env_storage()
credentials = CredentialStoreAdapter.default()
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:
"""Tests for CredentialStoreAdapter class."""