firts agent concept
This commit is contained in:
1
.env.example
Normal file
1
.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DEEPSEEK_API_KEY="sk-..."
|
||||||
1828
poetry.lock
generated
Normal file
1828
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,14 @@ authors = [
|
|||||||
{name = "aserrador",email = "alejandro.ayala@onecluster.org"}
|
{name = "aserrador",email = "alejandro.ayala@onecluster.org"}
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13,<4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"pytest (>=9.0.2,<10.0.0)",
|
||||||
|
"langchain[standard] (>=1.2.6,<2.0.0)",
|
||||||
|
"langgraph[standard] (>=1.0.6,<2.0.0)",
|
||||||
|
"langchain-anthropic (>=1.3.1,<2.0.0)",
|
||||||
|
"langchain-deepseek (>=1.0.1,<2.0.0)",
|
||||||
|
"dotenv (>=0.9.9,<0.10.0)"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
|
|||||||
5
src/naliiabot/bot/__init__.py
Normal file
5
src/naliiabot/bot/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class Agent:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
agent = Agent()
|
||||||
81
src/naliiabot/bot/agent/agent.py
Normal file
81
src/naliiabot/bot/agent/agent.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from langgraph.graph import StateGraph, START, END
|
||||||
|
from langchain.messages import SystemMessage, ToolMessage
|
||||||
|
from typing import Literal
|
||||||
|
from .schemas import MessagesState
|
||||||
|
|
||||||
|
|
||||||
|
class Agent:
|
||||||
|
|
||||||
|
def __init__(self, model):
|
||||||
|
self._model = model
|
||||||
|
self._tools = []
|
||||||
|
self._tools_by_name = []
|
||||||
|
self.model_with_tools = self._model.bind_tools(
|
||||||
|
self._tools)
|
||||||
|
self.agent = self._compiler_agent()
|
||||||
|
|
||||||
|
def invoke(self, messages):
|
||||||
|
self.agent.invoke(
|
||||||
|
messages
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compiler_agent(self):
|
||||||
|
agent = self._build_agent()
|
||||||
|
|
||||||
|
return agent.compile()
|
||||||
|
|
||||||
|
def _build_agent(self):
|
||||||
|
agent_builder = StateGraph(MessagesState)
|
||||||
|
agent_builder.add_node("llm_call", self.llm_call)
|
||||||
|
agent_builder.add_node("tool_node", self.tool_node)
|
||||||
|
agent_builder.add_edge(START, "llm_call")
|
||||||
|
|
||||||
|
return agent_builder
|
||||||
|
|
||||||
|
def llm_call(self, state: dict):
|
||||||
|
"""LLM decides whether to call a tool or not"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"messages": [
|
||||||
|
self.model_with_tools.invoke(
|
||||||
|
[
|
||||||
|
SystemMessage(
|
||||||
|
content=(
|
||||||
|
"You are a helpful assistant tasked with "
|
||||||
|
"performing arithmetic on a set of inputs."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
+ state["messages"]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"llm_calls": state.get('llm_calls', 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
def tool_node(self, state: dict):
|
||||||
|
"""Performs the tool call"""
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for tool_call in state["messages"][-1].tool_calls:
|
||||||
|
tool = self._tools_by_name[tool_call["name"]]
|
||||||
|
observation = tool.invoke(tool_call["args"])
|
||||||
|
result.append(
|
||||||
|
ToolMessage(
|
||||||
|
content=observation,
|
||||||
|
tool_call_id=tool_call["id"]
|
||||||
|
))
|
||||||
|
return {"messages": result}
|
||||||
|
|
||||||
|
def should_continue(state: MessagesState) -> Literal["tool_node", END]:
|
||||||
|
"""
|
||||||
|
Decide if we should continue the loop or stop base
|
||||||
|
d upon whether the LLM made a tool call
|
||||||
|
"""
|
||||||
|
|
||||||
|
messages = state["messages"]
|
||||||
|
last_message = messages[-1]
|
||||||
|
|
||||||
|
if last_message.tool_calls:
|
||||||
|
return "tool_node"
|
||||||
|
|
||||||
|
return END
|
||||||
8
src/naliiabot/bot/agent/schemas.py
Normal file
8
src/naliiabot/bot/agent/schemas.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from langchain.messages import AnyMessage
|
||||||
|
from typing_extensions import TypedDict, Annotated
|
||||||
|
import operator
|
||||||
|
|
||||||
|
|
||||||
|
class MessagesState(TypedDict):
|
||||||
|
messages: Annotated[list[AnyMessage], operator.add]
|
||||||
|
llm_calls: int
|
||||||
4
src/naliiabot/bot/exceptions.py
Normal file
4
src/naliiabot/bot/exceptions.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
class NotFoundProviderError(Exception):
|
||||||
|
"""LLM Provider is not supported."""
|
||||||
|
pass
|
||||||
0
src/naliiabot/bot/factories/__init__.py
Normal file
0
src/naliiabot/bot/factories/__init__.py
Normal file
48
src/naliiabot/bot/factories/llm_factory.py
Normal file
48
src/naliiabot/bot/factories/llm_factory.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from langchain_anthropic import ChatAnthropic
|
||||||
|
from langchain_deepseek import ChatDeepSeek
|
||||||
|
from src.naliiabot.bot.exceptions import NotFoundProviderError
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
class LLMProvider(str, Enum):
|
||||||
|
"""Supported Providers."""
|
||||||
|
|
||||||
|
ANTHROPIC = "anthropic"
|
||||||
|
DEEPSEEK = "deepseek"
|
||||||
|
|
||||||
|
|
||||||
|
class LLMFactory:
|
||||||
|
|
||||||
|
def __init__(self, provider):
|
||||||
|
self.provider = provider
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
model = None
|
||||||
|
|
||||||
|
if self.provider == LLMProvider.ANTHROPIC:
|
||||||
|
model = self._create_anthropic()
|
||||||
|
elif self.provider == LLMProvider.DEEPSEEK:
|
||||||
|
model = self._create_deepseek()
|
||||||
|
else:
|
||||||
|
raise NotFoundProviderError(
|
||||||
|
f"{self.provider.upper()} Not Provider Found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
def _create_anthropic(self):
|
||||||
|
model_name = "claude-sonnet-4-5-20250929"
|
||||||
|
model = ChatAnthropic(model=model_name)
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
def _create_deepseek(self):
|
||||||
|
model_name = "deepseek-chat"
|
||||||
|
model = ChatDeepSeek(
|
||||||
|
model=model_name
|
||||||
|
)
|
||||||
|
|
||||||
|
return model
|
||||||
16
tests/conftest.py
Normal file
16
tests/conftest.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import pytest
|
||||||
|
from src.naliiabot.bot.agent.agent import Agent
|
||||||
|
from src.naliiabot.bot.factories.llm_factory import LLMFactory
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def anthropic_model():
|
||||||
|
llmFactory = LLMFactory("anthropic")
|
||||||
|
anthropic_model = llmFactory.get_model()
|
||||||
|
|
||||||
|
return anthropic_model
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def agent(anthropic_model):
|
||||||
|
return Agent(model=anthropic_model)
|
||||||
9
tests/test_agent.py
Normal file
9
tests/test_agent.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# import pytest
|
||||||
|
from langchain.messages import HumanMessage
|
||||||
|
|
||||||
|
|
||||||
|
def test_invoke_agent(agent):
|
||||||
|
messages = [HumanMessage(content="divide 3 and 4.")]
|
||||||
|
messages = agent.invoke({"messages": messages})
|
||||||
|
|
||||||
|
assert messages is not None
|
||||||
23
tests/test_factories.py
Normal file
23
tests/test_factories.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import pytest
|
||||||
|
from src.naliiabot.bot.factories.llm_factory import LLMFactory
|
||||||
|
from src.naliiabot.bot.exceptions import NotFoundProviderError
|
||||||
|
from langchain_anthropic import ChatAnthropic
|
||||||
|
from langchain_deepseek import ChatDeepSeek
|
||||||
|
|
||||||
|
|
||||||
|
def test_llm_factory():
|
||||||
|
|
||||||
|
antrophicModel = LLMFactory("anthropic").get_model()
|
||||||
|
assert isinstance(antrophicModel, ChatAnthropic) is True
|
||||||
|
|
||||||
|
deepseekModel = LLMFactory("deepseek").get_model()
|
||||||
|
assert isinstance(deepseekModel, ChatDeepSeek)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_model_raises_exception_when_provider_not_found():
|
||||||
|
factory = LLMFactory(provider="openai")
|
||||||
|
|
||||||
|
with pytest.raises(NotFoundProviderError) as exc_info:
|
||||||
|
factory.get_model()
|
||||||
|
|
||||||
|
assert str(exc_info.value) == "OPENAI Not Provider Found"
|
||||||
Reference in New Issue
Block a user