Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c2261d4
LangGraph stubs
tcdent Jan 7, 2025
8e6ef7b
Merge branch 'main' into langgraph
tcdent Jan 10, 2025
c3c8d40
LangGraph support.
tcdent Jan 14, 2025
03f8283
Add `graph` data structure to project templates and implement in Lang…
tcdent Jan 14, 2025
698b960
Merge branch 'main' into langgraph
tcdent Jan 15, 2025
fb6146b
Implement adding and removing tools from LangGraph projects. Move Lan…
tcdent Jan 15, 2025
d87c8b3
Get all tests passing.
tcdent Jan 15, 2025
f03add4
Can't use variable as Literal.
tcdent Jan 15, 2025
5f24c49
Code coverage for langgraph framework implementation.
tcdent Jan 15, 2025
3eb6346
Merge branch 'main' into langgraph
tcdent Jan 16, 2025
a235a4a
Add get_tool_callables to langgraph framework.
tcdent Jan 16, 2025
dfc887e
LangGraph tool callables bugfix.
tcdent Jan 16, 2025
743297d
ToolConfig should raise exceptions instead of exit.
tcdent Jan 16, 2025
0641b1b
Partial tests for frameworks.get_tool_callables
tcdent Jan 16, 2025
0f0de88
Remove empty file
tcdent Jan 16, 2025
e945223
Exclude abstract Protocol from test coverage.
tcdent Jan 16, 2025
a41b6d2
Fix imports for AgentConfig / frameworks.
tcdent Jan 16, 2025
3b249f3
Allow manipulating the graph inside of a LangGraph project.
tcdent Jan 16, 2025
eb5dd66
Use tasks.yaml and agents.yaml for lookup of agents and tasks when ex…
tcdent Jan 17, 2025
510d998
Support adding graph nodes where there are no existing nodes
tcdent Jan 17, 2025
f531884
Allow specifying position to insert new agents into the graph.
tcdent Jan 17, 2025
e693c8c
Supoort adding node edges where no start/end node exists yet.
tcdent Jan 17, 2025
6d5f81e
Remove unused methods.
tcdent Jan 17, 2025
cddafe4
Install dependencies and add import statements for third party LangCh…
tcdent Jan 17, 2025
2fbdd8e
Fix type checking.
tcdent Jan 17, 2025
2d66797
Bugfixes, tests for imports.
tcdent Jan 17, 2025
25fbfbb
Update coverage exclusions
tcdent Jan 17, 2025
a40b0e7
Cleanup coverage exclusion comments
tcdent Jan 17, 2025
fd0898d
Configurable positions for inserting tasks.
tcdent Jan 17, 2025
1bf5b5a
Cleanup print statements in CLI init commands.
tcdent Jan 17, 2025
acd1d38
Refactor CLI `init` to dynamically insert agents, tasks & tools after…
tcdent Jan 18, 2025
e7d8c60
Add langchain as a dependency, remove langgraph
tcdent Jan 20, 2025
a2f3ee8
Merge branch 'main' into langgraph
tcdent Jan 20, 2025
8b61ffb
template readme
bboynton97 Jan 21, 2025
e1e9756
readme and template changes
bboynton97 Jan 22, 2025
2b87a69
Merge branch 'main' into langgraph
bboynton97 Jan 23, 2025
7facf9b
manually capture langgraph tool use
bboynton97 Jan 23, 2025
2ac1e34
Fix LangGraph tool use.
tcdent Jan 23, 2025
60ad579
allow delegation warning fix
bboynton97 Jan 23, 2025
4a4e64c
better printing
bboynton97 Jan 23, 2025
305c944
Add `agent.bind_tools` when first tool is added. #231
tcdent Jan 24, 2025
3d79347
version bump
bboynton97 Jan 24, 2025
8676d68
Fix wizard!
tcdent Jan 24, 2025
0467a79
Migrate project metadata to standard format (not poetry).
tcdent Jan 24, 2025
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
19 changes: 19 additions & 0 deletions agentstack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,34 @@
from pathlib import Path
from agentstack import conf
from agentstack.utils import get_framework
from agentstack.agents import get_agent
from agentstack.tasks import get_task
from agentstack.inputs import get_inputs

___all___ = [
"conf",
"agent",
"task",
"get_tags",
"get_framework",
"get_agent",
"get_task",
"get_inputs",
]

def agent():
"""
The `agent` decorator is used to mark a method that implements an Agent.
"""
pass


def task():
"""
The `task` decorator is used to mark a method that implements a Task.
"""
pass


def get_tags() -> list[str]:
"""
Expand Down
26 changes: 26 additions & 0 deletions agentstack/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@


AGENTS_FILENAME: Path = Path("src/config/agents.yaml")
AGENTS_PROMPT_TPL: str = "You are {role}. {backstory}\nYour personal goal is: {goal}"

yaml = YAML()
yaml.preserve_quotes = True # Preserve quotes in existing data
Expand Down Expand Up @@ -67,6 +68,25 @@ def __init__(self, name: str):
error_str += f"{' '.join([str(loc) for loc in error['loc']])}: {error['msg']}\n"
raise ValidationError(f"Error loading agent {name} from {filename}.\n{error_str}")

@property
def provider(self) -> str:
from agentstack import frameworks
return frameworks.parse_llm(self.llm)[0]

@property
def model(self) -> str:
from agentstack import frameworks
return frameworks.parse_llm(self.llm)[1]

@property
def prompt(self) -> str:
"""Concatenate the prompts for role, goal, and backstory."""
return AGENTS_PROMPT_TPL.format(**{
'role': self.role,
'goal': self.goal,
'backstory': self.backstory,
})

def model_dump(self, *args, **kwargs) -> dict:
dump = super().model_dump(*args, **kwargs)
dump.pop('name') # name is the key, so keep it out of the data
Expand Down Expand Up @@ -104,3 +124,9 @@ def get_all_agent_names() -> list[str]:

def get_all_agents() -> list[AgentConfig]:
return [AgentConfig(name) for name in get_all_agent_names()]


def get_agent(name: str) -> Optional[AgentConfig]:
"""Get an agent configuration by name."""
return AgentConfig(name)

5 changes: 5 additions & 0 deletions agentstack/cli/agentstack_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def __init__(
):
self.agents = []
self.tasks = []
self.graph = []
self.inputs = {}
self.method = method
self.manager_agent = manager_agent
Expand All @@ -68,6 +69,9 @@ def add_agent(self, agent):
def add_task(self, task):
self.tasks.append(task)

def add_edge(self, edge):
self.graph.append(edge)

def set_inputs(self, inputs):
self.inputs = inputs

Expand All @@ -77,6 +81,7 @@ def to_dict(self):
'manager_agent': self.manager_agent,
'agents': self.agents,
'tasks': self.tasks,
'graph': self.graph,
'inputs': self.inputs,
}

Expand Down
10 changes: 8 additions & 2 deletions agentstack/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
def init_project_builder(
slug_name: Optional[str] = None,
template: Optional[str] = None,
framework: Optional[str] = None,
use_wizard: bool = False,
):
if not slug_name and not use_wizard:
Expand Down Expand Up @@ -77,10 +78,12 @@ def init_project_builder(
"author": "Name <Email>",
"license": "MIT",
}
framework = template_data.framework
if framework is None:
framework = template_data.framework
design = {
'agents': [agent.model_dump() for agent in template_data.agents],
'tasks': [task.model_dump() for task in template_data.tasks],
'graph': [[node[0].model_dump(), node[1].model_dump()] for node in template_data.graph],
'inputs': template_data.inputs,
}
tools = [tools.model_dump() for tools in template_data.tools]
Expand All @@ -102,10 +105,12 @@ def init_project_builder(
"author": "Name <Email>",
"license": "MIT",
}
framework = default_project.framework
if framework is None:
framework = default_project.framework
design = {
'agents': [agent.model_dump() for agent in default_project.agents],
'tasks': [task.model_dump() for task in default_project.tasks],
'graph': [[node[0].model_dump(), node[1].model_dump()] for node in default_project.graph],
'inputs': default_project.inputs,
}
tools = [tools.model_dump() for tools in default_project.tools]
Expand Down Expand Up @@ -387,6 +392,7 @@ def insert_template(
project_structure.agents = design["agents"]
project_structure.tasks = design["tasks"]
project_structure.inputs = design["inputs"]
project_structure.graph = design.get("graph", [])

cookiecutter_data = CookiecutterData(
project_metadata=project_metadata,
Expand Down
10 changes: 9 additions & 1 deletion agentstack/cli/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path
from agentstack import conf
from agentstack import packaging
from agentstack import frameworks
from agentstack.cli import welcome_message, init_project_builder
from agentstack.utils import term_color

Expand All @@ -28,6 +29,7 @@ def require_uv():
def init_project(
slug_name: Optional[str] = None,
template: Optional[str] = None,
framework: Optional[str] = None,
use_wizard: bool = False,
):
"""
Expand Down Expand Up @@ -55,8 +57,14 @@ def init_project(
print(term_color("🦾 Creating a new AgentStack project...", 'blue'))
print(f"Using project directory: {conf.PATH.absolute()}")

if framework:
if not framework in frameworks.SUPPORTED_FRAMEWORKS:
print(f"Error: Framework '{framework}' is not supported.")
sys.exit(1)
print(f"Using framework: {framework}")

# copy the project skeleton, create a virtual environment, and install dependencies
init_project_builder(slug_name, template, use_wizard)
init_project_builder(slug_name, template, framework, use_wizard)
packaging.create_venv()
packaging.install_project()

Expand Down
37 changes: 34 additions & 3 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, ]
LANGGRAPH = 'langgraph'
SUPPORTED_FRAMEWORKS = [
CREWAI,
LANGGRAPH,
]


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 All @@ -30,6 +36,12 @@ def validate_project(self) -> None:
"""
...

def parse_llm(self, llm: str) -> tuple[str, str]:
"""
Parse a language model string into a provider and model.
"""
...

def get_tool_names(self) -> list[str]:
"""
Get a list of tool names in the user's project.
Expand Down Expand Up @@ -88,59 +100,78 @@ 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 parse_llm(llm: str) -> tuple[str, str]:
"""
Parse a language model string into a provider and model.
"""
return get_framework_module(get_framework()).parse_llm(llm)


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_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_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.
"""
if agent.name in get_agent_names():
raise ValidationError(f"Agent `{agent.name}` already exists in {get_entrypoint_path()}")
return get_framework_module(get_framework()).add_agent(agent)


def add_task(task: TaskConfig):
"""
Add a task to the user's project.
"""
if task.name in get_task_names():
raise ValidationError(f"Task `{task.name}` already exists in {get_entrypoint_path()}")
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()

17 changes: 11 additions & 6 deletions agentstack/frameworks/crewai.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ def get_task_methods(self) -> list[ast.FunctionDef]:
def add_task_method(self, task: TaskConfig):
"""Add a new task method to the CrewAI entrypoint."""
task_methods = self.get_task_methods()
if task.name in [method.name for method in task_methods]:
# TODO this should check all methods in the class for duplicates
raise ValidationError(f"Task `{task.name}` already exists in {ENTRYPOINT}")
if task_methods:
# Add after the existing task methods
_, pos = self.get_node_range(task_methods[-1])
Expand All @@ -71,6 +68,7 @@ def {task.name}(self) -> Task:
return Task(
config=self.tasks_config['{task.name}'],
)"""

if not self.source[:pos].endswith('\n'):
code = '\n\n' + code
if not self.source[pos:].startswith('\n'):
Expand All @@ -85,9 +83,6 @@ def add_agent_method(self, agent: AgentConfig):
"""Add a new agent method to the CrewAI entrypoint."""
# TODO do we want to pre-populate any tools?
agent_methods = self.get_agent_methods()
if agent.name in [method.name for method in agent_methods]:
# TODO this should check all methods in the class for duplicates
raise ValidationError(f"Agent `{agent.name}` already exists in {ENTRYPOINT}")
if agent_methods:
# Add after the existing agent methods
_, pos = self.get_node_range(agent_methods[-1])
Expand All @@ -103,6 +98,7 @@ def {agent.name}(self) -> Agent:
tools=[], # add tools here or use `agentstack tools add <tool_name>
verbose=True,
)"""

if not self.source[:pos].endswith('\n'):
code = '\n\n' + code
if not self.source[pos:].startswith('\n'):
Expand Down Expand Up @@ -238,6 +234,15 @@ def validate_project() -> None:
)


def parse_llm(llm: str) -> tuple[str, str]:
"""
Parse the llm string into a `LLM` dataclass.
Crew separates providers and models with a forward slash.
"""
provider, model = llm.split('/')
return provider, model


def get_task_names() -> list[str]:
"""
Get a list of task names (methods with an @task decorator).
Expand Down
Loading