Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/autogen-direct/test_skills/get_hn_headlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def run(count: int = 10) -> list[str]:
import html
import re

tools = globals()["tools"]

# Fetch the HackerNews front page with enough content to get all headlines
html_content = tools.web.fetch(url="https://news.ycombinator.com/", raw=True, max_length=20000)

Expand Down
2 changes: 1 addition & 1 deletion examples/azure-container-apps/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@

def get_model_client():
"""Get Azure OpenAI model client."""
from autogen_ext.models.openai import AzureOpenAIChatCompletionClient
from autogen_core.models import ModelFamily, ModelInfo
from autogen_ext.models.openai import AzureOpenAIChatCompletionClient
from azure.identity import DefaultAzureCredential, get_bearer_token_provider

# Use managed identity for Azure OpenAI auth
Expand Down
2 changes: 1 addition & 1 deletion examples/azure-container-apps/agent_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class TaskResponse(BaseModel):

def get_model_client():
"""Get Azure OpenAI model client."""
from autogen_ext.models.openai import AzureOpenAIChatCompletionClient
from autogen_core.models import ModelFamily, ModelInfo
from autogen_ext.models.openai import AzureOpenAIChatCompletionClient
from azure.identity import DefaultAzureCredential, get_bearer_token_provider

# Use managed identity for Azure OpenAI auth
Expand Down
1 change: 1 addition & 0 deletions examples/shared/skills/analyze_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async def run(repo: str) -> dict:

base_url = f"https://api.github.com/repos/{repo}"
results = {}
tools = globals()["tools"]

# Step 1: Fetch repo metadata
repo_raw = tools.curl(url=base_url)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ dev-dependencies = [
"docker>=7.1.0",
"pytest-xdist>=3.8.0",
"testcontainers[redis]>=4.13.3",
"chromadb>=0.5",
]

[tool.pytest.ini_options]
Expand Down
101 changes: 42 additions & 59 deletions src/py_code_mode/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,44 @@ async def bootstrap_namespaces(config: dict[str, Any]) -> NamespaceBundle:
raise ValueError(f"Unknown storage type: {storage_type!r}. Expected 'file' or 'redis'.")


async def _load_tools_namespace(tools_path_str: str | None) -> ToolsNamespace:
"""Load tools namespace from optional tools path."""
from py_code_mode.tools import ToolRegistry, ToolsNamespace

if tools_path_str:
tools_path = Path(tools_path_str)
registry = ToolRegistry()
await registry.load_from_directory(tools_path)
return ToolsNamespace(registry)

return ToolsNamespace(ToolRegistry())


def _build_namespace_bundle(
storage: Any,
tools_ns: ToolsNamespace,
deps_ns: DepsNamespace,
artifact_store: ArtifactStoreProtocol,
) -> NamespaceBundle:
"""Wire up namespaces into a NamespaceBundle."""
from py_code_mode.execution.in_process.skills_namespace import SkillsNamespace

namespace_dict: dict[str, Any] = {}
skills_ns = SkillsNamespace(storage.get_skill_library(), namespace_dict)

namespace_dict["tools"] = tools_ns
namespace_dict["skills"] = skills_ns
namespace_dict["artifacts"] = artifact_store
namespace_dict["deps"] = deps_ns

return NamespaceBundle(
tools=tools_ns,
skills=skills_ns,
artifacts=artifact_store,
deps=deps_ns,
)


async def _bootstrap_file_storage(config: dict[str, Any]) -> NamespaceBundle:
"""Bootstrap namespaces from FileStorage config.

Expand All @@ -88,48 +126,20 @@ async def _bootstrap_file_storage(config: dict[str, Any]) -> NamespaceBundle:
"""
# Import lazily to avoid circular imports
from py_code_mode.deps import DepsNamespace, FileDepsStore, PackageInstaller
from py_code_mode.execution.in_process.skills_namespace import SkillsNamespace
from py_code_mode.storage import FileStorage
from py_code_mode.tools import ToolRegistry, ToolsNamespace

base_path = Path(config["base_path"])
storage = FileStorage(base_path)

# Tools are owned by executor, loaded from config if provided
tools_path_str = config.get("tools_path")
if tools_path_str:
tools_path = Path(tools_path_str)
registry = ToolRegistry()
await registry.load_from_directory(tools_path)
tools_ns = ToolsNamespace(registry)
else:
# Empty registry when no tools_path provided
tools_ns = ToolsNamespace(ToolRegistry())

tools_ns = await _load_tools_namespace(config.get("tools_path"))
artifact_store = storage.get_artifact_store()

# Create deps namespace
deps_store = FileDepsStore(base_path)
installer = PackageInstaller()
deps_ns = DepsNamespace(deps_store, installer)

# SkillsNamespace needs a namespace dict for skill execution
# Create the dict first, wire up after creation
namespace_dict: dict[str, Any] = {}
skills_ns = SkillsNamespace(storage.get_skill_library(), namespace_dict)

# Wire up circular references
namespace_dict["tools"] = tools_ns
namespace_dict["skills"] = skills_ns
namespace_dict["artifacts"] = artifact_store
namespace_dict["deps"] = deps_ns

return NamespaceBundle(
tools=tools_ns,
skills=skills_ns,
artifacts=artifact_store,
deps=deps_ns,
)
return _build_namespace_bundle(storage, tools_ns, deps_ns, artifact_store)


async def _bootstrap_redis_storage(config: dict[str, Any]) -> NamespaceBundle:
Expand All @@ -146,47 +156,20 @@ async def _bootstrap_redis_storage(config: dict[str, Any]) -> NamespaceBundle:
"""
# Import lazily to avoid circular imports
from py_code_mode.deps import DepsNamespace, PackageInstaller, RedisDepsStore
from py_code_mode.execution.in_process.skills_namespace import SkillsNamespace
from py_code_mode.storage import RedisStorage
from py_code_mode.tools import ToolRegistry, ToolsNamespace

url = config["url"]
prefix = config["prefix"]

# Connect to Redis
storage = RedisStorage(url=url, prefix=prefix)

# Tools are owned by executor, loaded from config if provided
tools_path_str = config.get("tools_path")
if tools_path_str:
tools_path = Path(tools_path_str)
registry = ToolRegistry()
await registry.load_from_directory(tools_path)
tools_ns = ToolsNamespace(registry)
else:
# Empty registry when no tools_path provided
tools_ns = ToolsNamespace(ToolRegistry())

tools_ns = await _load_tools_namespace(config.get("tools_path"))
artifact_store = storage.get_artifact_store()

# Create deps namespace
deps_store = RedisDepsStore(storage.client, prefix=f"{prefix}:deps")
installer = PackageInstaller()
deps_ns = DepsNamespace(deps_store, installer)

# SkillsNamespace needs a namespace dict for skill execution
namespace_dict: dict[str, Any] = {}
skills_ns = SkillsNamespace(storage.get_skill_library(), namespace_dict)

# Wire up circular references
namespace_dict["tools"] = tools_ns
namespace_dict["skills"] = skills_ns
namespace_dict["artifacts"] = artifact_store
namespace_dict["deps"] = deps_ns

return NamespaceBundle(
tools=tools_ns,
skills=skills_ns,
artifacts=artifact_store,
deps=deps_ns,
)
return _build_namespace_bundle(storage, tools_ns, deps_ns, artifact_store)
2 changes: 2 additions & 0 deletions src/py_code_mode/deps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Provides storage, installation, and namespace for Python package dependencies.
"""

from py_code_mode.deps.config import collect_configured_deps
from py_code_mode.deps.installer import PackageInstaller, SyncResult
from py_code_mode.deps.namespace import (
ControlledDepsNamespace,
Expand All @@ -12,6 +13,7 @@
from py_code_mode.deps.store import DepsStore, FileDepsStore, MemoryDepsStore, RedisDepsStore

__all__ = [
"collect_configured_deps",
"DepsStore",
"FileDepsStore",
"MemoryDepsStore",
Expand Down
28 changes: 28 additions & 0 deletions src/py_code_mode/deps/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Helpers for parsing deps configuration."""

from __future__ import annotations

from collections.abc import Iterable
from pathlib import Path


def collect_configured_deps(
deps: Iterable[str] | None,
deps_file: Path | None,
) -> list[str]:
"""Collect configured deps from config list and optional file.

Args:
deps: Iterable of deps from config (e.g. config.deps).
deps_file: Optional requirements.txt-style file path.

Returns:
Combined list of dependency specifications.
"""
configured = list(deps) if deps else []
if deps_file and deps_file.exists():
for line in deps_file.read_text().splitlines():
stripped = line.strip()
if stripped and not stripped.startswith("#"):
configured.append(stripped)
return configured
5 changes: 0 additions & 5 deletions src/py_code_mode/execution/container/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ class ContainerConfig:
# Container settings
environment: dict[str, str] = field(default_factory=dict)
remove_on_exit: bool = True
name: str | None = None # Container name (auto-generated if None)

# Deps configuration
allow_runtime_deps: bool = True
Expand Down Expand Up @@ -323,8 +322,4 @@ def to_docker_config(
if platform.system() == "Linux":
config["extra_hosts"] = {"host.docker.internal": "host-gateway"}

# Add container name
if self.name:
config["name"] = self.name

return config
22 changes: 11 additions & 11 deletions src/py_code_mode/execution/container/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
HTTPX_AVAILABLE = False
httpx = None # type: ignore

from py_code_mode.deps import collect_configured_deps
from py_code_mode.execution.container.client import SessionClient
from py_code_mode.execution.container.config import DEFAULT_IMAGE, ContainerConfig
from py_code_mode.execution.protocol import (
Expand Down Expand Up @@ -179,16 +180,7 @@ def get_configured_deps(self) -> list[str]:
Returns:
List of package specifications.
"""
deps: list[str] = []
if self.config.deps:
deps.extend(self.config.deps)
if self.config.deps_file and self.config.deps_file.exists():
file_deps = self.config.deps_file.read_text().strip().splitlines()
for line in file_deps:
stripped = line.strip()
if stripped and not stripped.startswith("#"):
deps.append(stripped)
return deps
return collect_configured_deps(self.config.deps, self.config.deps_file)

async def __aenter__(self) -> ContainerExecutor:
"""Start container and connect."""
Expand Down Expand Up @@ -390,14 +382,18 @@ async def start(
tools_path = self.config.tools_path
skills_path = access.skills_path
artifacts_path = access.artifacts_path
deps_path = None # Deps owned by executor, not storage
deps_path = None
if artifacts_path is not None:
deps_path = artifacts_path.parent / "deps"
# Create directories on host before mounting
# Skills need to exist for volume mount
if skills_path:
skills_path.mkdir(parents=True, exist_ok=True)
# Artifacts need to exist for volume mount
if artifacts_path:
artifacts_path.mkdir(parents=True, exist_ok=True)
if deps_path:
deps_path.mkdir(parents=True, exist_ok=True)
elif isinstance(access, RedisStorageAccess):
redis_url = access.redis_url
# Transform localhost URLs for Docker container access
Expand Down Expand Up @@ -891,6 +887,10 @@ async def _wait_for_healthy(self) -> None:
# HTTP-level errors also expected during startup
logger.debug(f"Health check failed (HTTP error): {e}")
last_error = e
except Exception as e: # pragma: no cover
# Unexpected errors - capture and continue retrying
logger.debug(f"Health check failed (unexpected error): {e}")
last_error = e

await asyncio.sleep(self.config.health_check_interval)

Expand Down
15 changes: 1 addition & 14 deletions src/py_code_mode/execution/container/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
)
from py_code_mode.skills import FileSkillStore, SkillLibrary, create_skill_library # noqa: E402
from py_code_mode.tools import ToolRegistry # noqa: E402
from py_code_mode.tools.adapters.cli import CLIAdapter # noqa: E402

# Session expiration (seconds)
SESSION_EXPIRY = 3600 # 1 hour
Expand Down Expand Up @@ -433,19 +432,7 @@ async def initialize_server(config: SessionConfig) -> None:
tools_path = os.environ.get("TOOLS_PATH")
if tools_path:
logger.info(" Loading tools from directory: %s", tools_path)
tools_dir = Path(tools_path)

# Load CLI tools from YAML files
cli_adapter = CLIAdapter(tools_path=tools_dir)
registry = ToolRegistry()
if cli_adapter.list_tools():
registry.add_adapter(adapter=cli_adapter)

# Also load MCP tools from the same directory
mcp_registry = await ToolRegistry.from_dir(tools_path)
for adapter in mcp_registry.get_adapters():
registry.add_adapter(adapter=adapter)

registry = await ToolRegistry.from_dir(tools_path)
logger.info(" Tools in directory: %d", len(registry.list_tools()))
else:
# No TOOLS_PATH - no tools available
Expand Down
22 changes: 3 additions & 19 deletions src/py_code_mode/execution/in_process/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
DepsNamespace,
FileDepsStore,
PackageInstaller,
collect_configured_deps,
)
from py_code_mode.execution.in_process.config import InProcessConfig
from py_code_mode.execution.in_process.skills_namespace import SkillsNamespace
Expand Down Expand Up @@ -128,16 +129,7 @@ def get_configured_deps(self) -> list[str]:
Returns:
List of package specifications.
"""
deps: list[str] = []
if self._config.deps:
deps.extend(self._config.deps)
if self._config.deps_file and self._config.deps_file.exists():
file_deps = self._config.deps_file.read_text().strip().splitlines()
for line in file_deps:
stripped = line.strip()
if stripped and not stripped.startswith("#"):
deps.append(stripped)
return deps
return collect_configured_deps(self._config.deps, self._config.deps_file)

async def run(self, code: str, timeout: float | None = None) -> ExecutionResult:
"""Run code and return result.
Expand Down Expand Up @@ -288,15 +280,7 @@ async def start(

# Deps from executor config (NOT storage)
# Collect deps from config.deps list and config.deps_file
initial_deps: list[str] = []
if self._config.deps:
initial_deps.extend(self._config.deps)
if self._config.deps_file and self._config.deps_file.exists():
file_deps = self._config.deps_file.read_text().strip().splitlines()
for line in file_deps:
stripped = line.strip()
if stripped and not stripped.startswith("#"):
initial_deps.append(stripped)
initial_deps = collect_configured_deps(self._config.deps, self._config.deps_file)

# Create deps namespace with initial deps from config
if self._deps_namespace is None:
Expand Down
Loading
Loading