Skip to content
Merged

V2 #6

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
605 changes: 312 additions & 293 deletions README.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions TODOD.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# TODO: 1. Add fix api, in-case we have anytool call and its crash we need to cleanup tool call
So we can create a new api called fix_broken_graph

# TODO: 2. And setup api to register frontend tools

12 changes: 4 additions & 8 deletions agentflow.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
{
"graphs": {
"agent": "graph.react:app",
"injectq": null
},
"agent": "graph.react:app",
"thread_name_generator": "graph.thread_name_generator:MyNameGenerator",
"env": ".env",
"auth": null,
"thread_model_name": "gemini/gemini-2.0-flash",
"generate_thread_name": false
}
"auth": null
}
4 changes: 4 additions & 0 deletions agentflow_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
# Lets expose few things the user suppose to use
from .src.app.core.auth.base_auth import BaseAuth
from .src.app.utils.snowflake_id_generator import SnowFlakeIdGenerator
from .src.app.utils.thread_name_generator import (
ThreadNameGenerator,
)


__all__ = [
"BaseAuth",
"SnowFlakeIdGenerator",
"ThreadNameGenerator",
]
36 changes: 12 additions & 24 deletions agentflow_cli/cli/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ def load_config(self, config_path: str | None = None) -> dict[str, Any]:
config_path=str(actual_path),
) from e

# Ensure config data was loaded
if self._config_data is None:
raise ConfigurationError(
"Failed to load configuration data",
config_path=str(actual_path),
)

# Validate configuration
self._validate_config(self._config_data)

Expand All @@ -152,7 +159,7 @@ def _validate_config(self, config_data: dict[str, Any]) -> None:
Raises:
ConfigurationError: If validation fails
"""
required_fields = ["graphs"]
required_fields = ["agent"]

for field in required_fields:
if field not in config_data:
Expand All @@ -161,33 +168,14 @@ def _validate_config(self, config_data: dict[str, Any]) -> None:
config_path=self.config_path,
)

# Validate graphs section
graphs = config_data["graphs"]
if not isinstance(graphs, dict):
# Validate agent field
agent = config_data["agent"]
if not isinstance(agent, str):
raise ConfigurationError(
"Field 'graphs' must be a dictionary",
"Field 'agent' must be a string",
config_path=self.config_path,
)

# Additional validation can be added here
self._validate_graphs_config(graphs)

def _validate_graphs_config(self, graphs: dict[str, Any]) -> None:
"""Validate graphs configuration section.

Args:
graphs: Graphs configuration to validate

Raises:
ConfigurationError: If validation fails
"""
for graph_name, graph_config in graphs.items():
if graph_config is not None and not isinstance(graph_config, str):
raise ConfigurationError(
f"Graph '{graph_name}' configuration must be a string or null",
config_path=self.config_path,
)

def get_config(self) -> dict[str, Any]:
"""Get loaded configuration data.

Expand Down
17 changes: 5 additions & 12 deletions agentflow_cli/cli/core/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,22 +166,15 @@ def validate_config_structure(config: dict[str, Any]) -> dict[str, Any]:
raise ValidationError("Configuration must be a dictionary")

# Required fields
required_fields = ["graphs"]
required_fields = ["agent"]
for field in required_fields:
if field not in config:
raise ValidationError(f"Missing required field: {field}")

# Validate graphs section
graphs = config["graphs"]
if not isinstance(graphs, dict):
raise ValidationError("Field 'graphs' must be a dictionary")

# Validate individual graph entries
for graph_name, graph_value in graphs.items():
if graph_value is not None and not isinstance(graph_value, str):
raise ValidationError(
f"Graph '{graph_name}' must be a string or null", field=f"graphs.{graph_name}"
)
# Validate agent field
agent = config["agent"]
if not isinstance(agent, str):
raise ValidationError("Field 'agent' must be a string")

return config

Expand Down
42 changes: 18 additions & 24 deletions agentflow_cli/cli/templates/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@
# Default configuration template
DEFAULT_CONFIG_JSON: Final[str] = json.dumps(
{
"graphs": {
"agent": "graph.react:app",
"container": None,
},
"agent": "graph.react:app",
"env": ".env",
"auth": None,
"thread_model_name": "gemini/gemini-2.0-flash",
"generate_thread_name": False,
"checkpointer": None,
"injectq": None,
"store": None,
"thread_name_generator": None,
},
indent=2,
)
Expand Down Expand Up @@ -57,21 +56,19 @@
- Python logging: For debug and info messages
"""

import asyncio
import logging
from typing import Any

from agentflow.adapters.llm.model_response_converter import ModelResponseConverter
from agentflow.checkpointer import InMemoryCheckpointer
from agentflow.graph import StateGraph, ToolNode
from agentflow.state.agent_state import AgentState
from agentflow.utils.callbacks import CallbackManager
from agentflow.utils.constants import END
from agentflow.utils.converter import convert_messages
from dotenv import load_dotenv
from injectq import Inject
from litellm import acompletion
from agentflowadapters.llm.model_response_converter import ModelResponseConverter
from agentflowcheckpointer import InMemoryCheckpointer
from agentflowgraph import StateGraph, ToolNode
from agentflowstate.agent_state import AgentState
from agentflowutils import Message
from agentflowutils.callbacks import CallbackManager
from agentflowutils.constants import END
from agentflowutils.converter import convert_messages


# Configure logging for the module
Expand Down Expand Up @@ -134,7 +131,7 @@ def get_weather(
tool_call_id: str,
state: AgentState,
checkpointer: InMemoryCheckpointer = Inject[InMemoryCheckpointer],
) -> Message:
) -> str:
"""Retrieve current weather information for a specified location."""
# Demonstrate access to injected parameters
logger.debug("***** Checkpointer instance: %s", checkpointer)
Expand Down Expand Up @@ -178,11 +175,11 @@ async def main_agent(
2. Otherwise, generate a response with available tools for potential tool usage
"""
# System prompt defining the agent's role and capabilities
system_prompt = \"\"\"
system_prompt = """
You are a helpful assistant.
Your task is to assist the user in finding information and answering questions.
You have access to various tools that can help you provide accurate information.
\"\"\"
"""

# Convert state messages to the format expected by the AI model
messages = convert_messages(
Expand All @@ -208,12 +205,7 @@ async def main_agent(
is_stream = config.get("is_stream", False)

# Determine response strategy based on conversation context
if (
state.context
and len(state.context) > 0
and state.context[-1].role == "tool"
and state.context[-1].tool_call_id is not None
):
if state.context and len(state.context) > 0 and state.context[-1].role == "tool":
# Last message was a tool result - generate final response without tools
logger.info("Generating final response after tool execution")
response = await acompletion(
Expand Down Expand Up @@ -309,6 +301,8 @@ def should_use_tools(state: AgentState) -> str:
checkpointer=checkpointer,
)



'''

# Production templates (mirroring root repo tooling for convenience)
Expand Down
33 changes: 10 additions & 23 deletions agentflow_cli/src/app/core/config/graph_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,32 @@ def __init__(self, path: str = "agentflow.json"):

@property
def graph_path(self) -> str:
graphs = self.data.get("graphs", {})
if "agent" in graphs:
return graphs["agent"]
agent = self.data.get("agent")
if agent:
return agent

raise ValueError("Agent graph not found")

@property
def checkpointer_path(self) -> str | None:
graphs = self.data.get("graphs", {})
if "checkpointer" in graphs:
return graphs["checkpointer"]
return None
return self.data.get("checkpointer", None)

@property
def injectq_path(self) -> str | None:
graphs = self.data.get("graphs", {})
if "injectq" in graphs:
return graphs["injectq"]
return None
return self.data.get("injectq", None)

@property
def store_path(self) -> str | None:
graphs = self.data.get("graphs", {})
if "store" in graphs:
return graphs["store"]
return None
return self.data.get("store", None)

@property
def redis_url(self) -> str | None:
return self.data.get("redis", None)

@property
def thread_name_generator_path(self) -> str | None:
return self.data.get("thread_name_generator", None)

def auth_config(self) -> dict | None:
res = self.data.get("auth", None)
if not res:
Expand Down Expand Up @@ -78,11 +73,3 @@ def auth_config(self) -> dict | None:
}

raise ValueError(f"Unsupported auth method: {res}")

@property
def generate_thread_name(self) -> bool:
return self.data.get("generate_thread_name", False)

@property
def thread_model_name(self) -> str | None:
return self.data.get("thread_model_name", None)
7 changes: 7 additions & 0 deletions agentflow_cli/src/app/core/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ class Settings(BaseSettings):
ORIGINS: str = "*"
ALLOWED_HOST: str = "*"

#################################
###### Paths ####################
#################################
ROOT_PATH: str = ""
DOCS_PATH: str = ""
REDOCS_PATH: str = ""

#################################
###### REDIS Config ##########
#################################
Expand Down
3 changes: 3 additions & 0 deletions agentflow_cli/src/app/core/exceptions/handle_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
)


# Handle all exceptions of agentflow here


def init_errors_handler(app: FastAPI):
"""
Initialize error handlers for the FastAPI application.
Expand Down
36 changes: 36 additions & 0 deletions agentflow_cli/src/app/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from agentflow_cli import BaseAuth
from agentflow_cli.src.app.core.config.graph_config import GraphConfig
from agentflow_cli.src.app.utils.thread_name_generator import ThreadNameGenerator


logger = logging.getLogger("agentflow-cli.loader")
Expand Down Expand Up @@ -149,6 +150,32 @@ def load_auth(path: str | None) -> BaseAuth | None:
return auth


def load_thread_name_generator(path: str | None) -> ThreadNameGenerator | None:
if not path:
return None

module_name_importable, function_name = path.split(":")

try:
module = importlib.import_module(module_name_importable)
entry_point_obj = getattr(module, function_name)

# If it's a class, instantiate it; if it's an instance, use as is
if inspect.isclass(entry_point_obj) and issubclass(entry_point_obj, ThreadNameGenerator):
thread_name_generator = entry_point_obj()
elif isinstance(entry_point_obj, ThreadNameGenerator):
thread_name_generator = entry_point_obj
else:
raise TypeError("Loaded object is not a subclass or instance of ThreadNameGenerator.")

logger.info(f"Successfully loaded ThreadNameGenerator '{function_name}' from {path}.")
except Exception as e:
logger.error(f"Error loading ThreadNameGenerator from {path}: {e}")
raise Exception(f"Failed to load ThreadNameGenerator from {path}: {e}")

return thread_name_generator


async def attach_all_modules(
config: GraphConfig,
container: InjectQ,
Expand Down Expand Up @@ -179,6 +206,15 @@ async def attach_all_modules(
# bind None
container.bind_instance(BaseAuth, None, allow_none=True)

# load thread name generator
thread_name_generator_path = config.thread_name_generator_path
if thread_name_generator_path:
thread_name_generator = load_thread_name_generator(thread_name_generator_path)
container.bind_instance(ThreadNameGenerator, thread_name_generator)
else:
# bind None if not configured
container.bind_instance(ThreadNameGenerator, None, allow_none=True)

logger.info("Container loaded successfully")
logger.debug(f"Container dependency graph: {container.get_dependency_graph()}")

Expand Down
5 changes: 3 additions & 2 deletions agentflow_cli/src/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ async def lifespan(app: FastAPI):
version=settings.APP_VERSION,
debug=settings.MODE == "DEVELOPMENT",
summary=settings.SUMMARY,
docs_url="/docs",
redoc_url="/redocs",
docs_url=settings.DOCS_PATH if settings.DOCS_PATH else None,
redoc_url=settings.REDOCS_PATH if settings.REDOCS_PATH else None,
default_response_class=ORJSONResponse,
lifespan=lifespan,
root_path=settings.ROOT_PATH,
)

setup_middleware(app)
Expand Down
Loading
Loading