Skip to content

Pydantic AI #159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: tool-packages
Choose a base branch
from
Draft
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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,14 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

# aider
.aider*

# VSCode
.vscode

# AgentStack
.agentstack*
example_project/
ex/
**/ex/
Expand All @@ -171,5 +178,6 @@ cookiecutter.json
examples/tests/
examples/tests/**/*

uv.lock
.DS_Store

10 changes: 6 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ The best place to engage in conversation about your contribution is in the Issue
## Setup

1. Clone the repo
2. `poetry install`
3. `pip install -e .[dev,test]`
- This will install the CLI locally and in editable mode so you can use `agentstack <command>` to test your latest changes
`git clone https://github.com/AgentOps-AI/AgentStack.git`
`cd AgentStack`
2. Install agentstack as an edtiable project and set it up for development and testing
`pip install -e .[dev,test]`
This will install the CLI locally and in editable mode so you can use `agentstack <command>` to test your latest changes

## Project Structure
TODO
Expand Down Expand Up @@ -59,4 +61,4 @@ pre-commit install
```

## Tests
HAHAHAHAHAHAHA good one
HAHAHAHAHAHAHA good one
94 changes: 82 additions & 12 deletions agentstack/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
"""
This it the beginning of the agentstack public API.
This it the beginning of the agentstack public API.

Methods that have been imported into this file are expected to be used by the
end user inside of their project.
end user inside of their project.
"""
from typing import Callable

from typing import Optional, Callable, Any
from functools import wraps
from pathlib import Path
from agentstack import conf
from agentstack.utils import get_framework
from agentstack.inputs import get_inputs
from agentstack import frameworks

___all___ = [
"conf",
"tools",
"get_tags",
"get_framework",
"get_inputs",
"conf",
"extra",
"tools",
"get_tags",
"get_framework",
"get_inputs",
]


Expand All @@ -27,16 +30,83 @@ def get_tags() -> list[str]:
return ['agentstack', get_framework(), *conf.get_installed_tools()]


class ToolLoader:
def agent(agent_name: Optional[str] = None):
"""
The `agent` decorator.
This will pass kwargs needed when instantiating an agent in your framework.
"""

def decorator(func):
nonlocal agent_name
if agent_name is None:
agent_name = func.__name__

@wraps(func)
def wrapper(**kwargs):
return func(**frameworks.get_agent_decorator_kwargs(agent_name))

return wrapper

if callable(agent_name):
# This handles the case when the decorator is used without parentheses
f = agent_name
agent_name = None
return decorator(f)

return decorator


def task(task_name: Optional[str] = None):
"""
The `task` decorator.
This will pass kwargs needed when instantiating a task in your framework.
"""
Provides the public interface for accessing tools, wrapped in the
framework-specific callable format.

def decorator(func):
nonlocal task_name
if task_name is None:
task_name = func.__name__

@wraps(func)
def wrapper(**kwargs):
return func(**frameworks.get_task_decorator_kwargs(task_name))

return wrapper

if callable(task_name):
# This handles the case when the decorator is used without parentheses
f = task_name
task_name = None
return decorator(f)

return decorator


class ConfExtraLoader:
"""
Provides the public interface for accessing the `extra` vars in the project's
agentstack.json file.
"""

def __getitem__(self, name: str) -> Any:
conf_file = conf.ConfigFile()
return conf_file.extra.get(name) if conf_file.extra else None


extra = ConfExtraLoader()


class ToolLoader:
"""
Provides the public interface for accessing tools, wrapped in the
framework-specific callable format.

Get a tool's callables by name with `agentstack.tools[tool_name]`
Include them in your agent's tool list with `tools = [*agentstack.tools[tool_name], ]`
"""

def __getitem__(self, tool_name: str) -> list[Callable]:
return frameworks.get_tool_callables(tool_name)

tools = ToolLoader()

tools = ToolLoader()
6 changes: 5 additions & 1 deletion agentstack/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,18 @@ class AgentConfig(pydantic.BaseModel):
The backstory of the agent.
llm: str
The model this agent should use.
Adheres to the format set by the framework.
Always follows the format `org/repo`, regardless of the framework.
# TODO we need to reformat this on-read depending on the framework
retries: int
The number of times the agent should retry a task.
"""

name: str
role: str = ""
goal: str = ""
backstory: str = ""
llm: str = ""
retries: int = 0

def __init__(self, name: str):
filename = conf.PATH / AGENTS_FILENAME
Expand Down
2 changes: 1 addition & 1 deletion agentstack/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .cli import init_project_builder, configure_default_model, export_template
from .tools import list_tools, add_tool
from .run import run_project
from .run import run_project
11 changes: 9 additions & 2 deletions agentstack/cli/run.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import Optional
import sys
import asyncio
import inspect
import traceback
from pathlib import Path
import importlib.util
Expand Down Expand Up @@ -84,7 +86,7 @@ def _import_project_module(path: Path):
assert spec.loader is not None # appease type checker

project_module = importlib.util.module_from_spec(spec)
sys.path.append(str((path / MAIN_FILENAME).parent))
sys.path.insert(0, str((path / MAIN_FILENAME).parent))
spec.loader.exec_module(project_module)
return project_module

Expand Down Expand Up @@ -116,7 +118,12 @@ def run_project(command: str = 'run', debug: bool = False, cli_args: Optional[st
try:
print("Running your agent...")
project_main = _import_project_module(conf.PATH)
getattr(project_main, command)()
callback = getattr(project_main, command)

if inspect.iscoroutinefunction(callback):
asyncio.run(callback())
else:
callback()
except ImportError as e:
print(term_color(f"Failed to import project. Does '{MAIN_FILENAME}' exist?:\n{e}", 'red'))
sys.exit(1)
Expand Down
3 changes: 3 additions & 0 deletions agentstack/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ class ConfigFile(BaseModel):
The template used to generate the project.
template_version: Optional[str]
The version of the template system used to generate the project.
extra: Optional[dict]
Variables that will be available inside the project with `agentstack.conf[<var_name>]`.
"""

framework: str = DEFAULT_FRAMEWORK # TODO this should probably default to None
Expand All @@ -74,6 +76,7 @@ class ConfigFile(BaseModel):
agentstack_version: Optional[str] = get_version()
template: Optional[str] = None
template_version: Optional[str] = None
extra: Optional[dict] = None

def __init__(self):
if os.path.exists(PATH / CONFIG_FILENAME):
Expand Down
46 changes: 44 additions & 2 deletions agentstack/frameworks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@


CREWAI = 'crewai'
SUPPORTED_FRAMEWORKS = [CREWAI, ]
PYDANTIC_AI = 'pydantic_ai'
SUPPORTED_FRAMEWORKS = [
CREWAI,
PYDANTIC_AI,
]


class FrameworkModule(Protocol):
"""
Protocol spec for a framework implementation module.
"""

ENTRYPOINT: Path
"""
Relative path to the entrypoint file for the framework in the user's project.
Expand Down Expand Up @@ -60,6 +66,12 @@ def get_agent_names(self) -> list[str]:
"""
...

def get_agent_decorator_kwargs(self, agent_name: str) -> dict:
"""
Get the kwargs needed to instantiate an agent in the user's project.
"""
...

def get_agent_tool_names(self, agent_name: str) -> list[str]:
"""
Get a list of tool names in an agent in the user's project.
Expand All @@ -83,6 +95,12 @@ def get_task_names(self) -> list[str]:
Get a list of task names in the user's project.
"""
...

def get_task_decorator_kwargs(task_name: str) -> dict:
"""
Get the keyword arguments for the function affected by the task decorator.
"""
...


def get_framework_module(framework: str) -> FrameworkModule:
Expand All @@ -94,65 +112,89 @@ def get_framework_module(framework: str) -> FrameworkModule:
except ImportError:
raise Exception(f"Framework {framework} could not be imported.")


def get_entrypoint_path(framework: str) -> Path:
"""
Get the path to the entrypoint file for a framework.
"""
return conf.PATH / get_framework_module(framework).ENTRYPOINT


def validate_project():
"""
Validate that the user's project is ready to run.
"""
return get_framework_module(get_framework()).validate_project()


def add_tool(tool: ToolConfig, agent_name: str):
"""
Add a tool to the user's project.
Add a tool to the user's project.
The tool will have aready been installed in the user's application and have
all dependencies installed. We're just handling code generation here.
"""
return get_framework_module(get_framework()).add_tool(tool, agent_name)


def remove_tool(tool: ToolConfig, agent_name: str):
"""
Remove a tool from the user's project.
"""
return get_framework_module(get_framework()).remove_tool(tool, agent_name)


def get_tool_callables(tool_name: str) -> list[Callable]:
"""
Get a tool by name and return it as a list of framework-native callables.
"""
return get_framework_module(get_framework()).get_tool_callables(tool_name)


def get_agent_names() -> list[str]:
"""
Get a list of agent names in the user's project.
"""
return get_framework_module(get_framework()).get_agent_names()


def get_agent_decorator_kwargs(agent_name: str) -> dict:
"""
Get the kwargs needed to instantiate an agent in the user's project.
"""
return get_framework_module(get_framework()).get_agent_decorator_kwargs(agent_name)


def get_agent_tool_names(agent_name: str) -> list[str]:
"""
Get a list of tool names in the user's project.
"""
return get_framework_module(get_framework()).get_agent_tool_names(agent_name)


def add_agent(agent: AgentConfig):
"""
Add an agent to the user's project.
"""
return get_framework_module(get_framework()).add_agent(agent)


def add_task(task: TaskConfig):
"""
Add a task to the user's project.
"""
return get_framework_module(get_framework()).add_task(task)


def get_task_names() -> list[str]:
"""
Get a list of task names in the user's project.
"""
return get_framework_module(get_framework()).get_task_names()


def get_task_decorator_kwargs(task_name: str) -> dict:
"""
Get the keyword arguments for the function affected by the task decorator.
"""
return get_framework_module(get_framework()).get_task_decorator_kwargs(task_name)

Loading