langgraph-agent-toolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langgraph_agent_toolkit/__init__.py +7 -0
- langgraph_agent_toolkit/agents/__init__.py +0 -0
- langgraph_agent_toolkit/agents/agent.py +14 -0
- langgraph_agent_toolkit/agents/agent_executor.py +415 -0
- langgraph_agent_toolkit/agents/blueprints/__init__.py +0 -0
- langgraph_agent_toolkit/agents/blueprints/bg_task_agent/__init__.py +0 -0
- langgraph_agent_toolkit/agents/blueprints/bg_task_agent/agent.py +69 -0
- langgraph_agent_toolkit/agents/blueprints/bg_task_agent/task.py +52 -0
- langgraph_agent_toolkit/agents/blueprints/bg_task_agent/utils.py +17 -0
- langgraph_agent_toolkit/agents/blueprints/chatbot/__init__.py +0 -0
- langgraph_agent_toolkit/agents/blueprints/chatbot/agent.py +34 -0
- langgraph_agent_toolkit/agents/blueprints/command_agent/__init__.py +0 -0
- langgraph_agent_toolkit/agents/blueprints/command_agent/agent.py +54 -0
- langgraph_agent_toolkit/agents/blueprints/interrupt_agent/__init__.py +0 -0
- langgraph_agent_toolkit/agents/blueprints/interrupt_agent/agent.py +140 -0
- langgraph_agent_toolkit/agents/blueprints/react/__init__.py +0 -0
- langgraph_agent_toolkit/agents/blueprints/react/agent.py +67 -0
- langgraph_agent_toolkit/agents/blueprints/react_so/__init__.py +0 -0
- langgraph_agent_toolkit/agents/blueprints/react_so/agent.py +39 -0
- langgraph_agent_toolkit/agents/blueprints/supervisor_agent/__init__.py +0 -0
- langgraph_agent_toolkit/agents/blueprints/supervisor_agent/agent.py +44 -0
- langgraph_agent_toolkit/agents/components/__init__.py +0 -0
- langgraph_agent_toolkit/agents/components/creators/__init__.py +4 -0
- langgraph_agent_toolkit/agents/components/creators/create_react_agent.py +459 -0
- langgraph_agent_toolkit/agents/components/tools.py +13 -0
- langgraph_agent_toolkit/agents/components/utils.py +42 -0
- langgraph_agent_toolkit/client/__init__.py +4 -0
- langgraph_agent_toolkit/client/client.py +344 -0
- langgraph_agent_toolkit/core/__init__.py +5 -0
- langgraph_agent_toolkit/core/memory/__init__.py +0 -0
- langgraph_agent_toolkit/core/memory/base.py +33 -0
- langgraph_agent_toolkit/core/memory/factory.py +30 -0
- langgraph_agent_toolkit/core/memory/postgres.py +76 -0
- langgraph_agent_toolkit/core/memory/sqlite.py +21 -0
- langgraph_agent_toolkit/core/memory/types.py +6 -0
- langgraph_agent_toolkit/core/models/__init__.py +5 -0
- langgraph_agent_toolkit/core/models/chat_openai.py +21 -0
- langgraph_agent_toolkit/core/models/factory.py +118 -0
- langgraph_agent_toolkit/core/models/fake.py +25 -0
- langgraph_agent_toolkit/core/observability/__init__.py +10 -0
- langgraph_agent_toolkit/core/observability/base.py +331 -0
- langgraph_agent_toolkit/core/observability/empty.py +67 -0
- langgraph_agent_toolkit/core/observability/factory.py +43 -0
- langgraph_agent_toolkit/core/observability/langfuse.py +118 -0
- langgraph_agent_toolkit/core/observability/langsmith.py +131 -0
- langgraph_agent_toolkit/core/observability/types.py +34 -0
- langgraph_agent_toolkit/core/prompts/__init__.py +0 -0
- langgraph_agent_toolkit/core/prompts/chat_prompt_template.py +528 -0
- langgraph_agent_toolkit/core/settings.py +164 -0
- langgraph_agent_toolkit/helper/__init__.py +0 -0
- langgraph_agent_toolkit/helper/constants.py +10 -0
- langgraph_agent_toolkit/helper/logging.py +111 -0
- langgraph_agent_toolkit/helper/types.py +7 -0
- langgraph_agent_toolkit/helper/utils.py +80 -0
- langgraph_agent_toolkit/run_agent.py +68 -0
- langgraph_agent_toolkit/run_client.py +55 -0
- langgraph_agent_toolkit/run_service.py +19 -0
- langgraph_agent_toolkit/schema/__init__.py +28 -0
- langgraph_agent_toolkit/schema/models.py +25 -0
- langgraph_agent_toolkit/schema/schema.py +210 -0
- langgraph_agent_toolkit/schema/task_data.py +72 -0
- langgraph_agent_toolkit/service/__init__.py +0 -0
- langgraph_agent_toolkit/service/exception_handlers.py +46 -0
- langgraph_agent_toolkit/service/factory.py +213 -0
- langgraph_agent_toolkit/service/handler.py +122 -0
- langgraph_agent_toolkit/service/middleware.py +18 -0
- langgraph_agent_toolkit/service/routes.py +169 -0
- langgraph_agent_toolkit/service/types.py +8 -0
- langgraph_agent_toolkit/service/utils.py +136 -0
- langgraph_agent_toolkit/streamlit_app.py +368 -0
- langgraph_agent_toolkit-0.1.0.dist-info/METADATA +424 -0
- langgraph_agent_toolkit-0.1.0.dist-info/RECORD +74 -0
- langgraph_agent_toolkit-0.1.0.dist-info/WHEEL +4 -0
- langgraph_agent_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
DEFAULT_AGENT = "react-agent"
|
|
2
|
+
DEFAULT_MAX_MESSAGE_HISTORY_LENGTH = 6 + 1 # N messages + 1 system message
|
|
3
|
+
DEFAULT_RECURSION_LIMIT = 25
|
|
4
|
+
DEFAULT_OPENAI_COMPATIBLE_MODEL_PARAMS = dict(
|
|
5
|
+
temperature=0.0,
|
|
6
|
+
max_tokens=1024,
|
|
7
|
+
top_p=0.7,
|
|
8
|
+
streaming=True,
|
|
9
|
+
)
|
|
10
|
+
DEFAULT_CACHE_TTL_SECOND = 60 * 10 # 10 minutes
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from loguru import logger as loguru_logger
|
|
7
|
+
|
|
8
|
+
from langgraph_agent_toolkit.helper.types import EnvironmentMode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SingletonMeta(type):
|
|
12
|
+
"""Singleton metaclass to ensure only one instance of LoggerConfig exists."""
|
|
13
|
+
|
|
14
|
+
_instances = {}
|
|
15
|
+
|
|
16
|
+
def __call__(cls, *args, **kwargs):
|
|
17
|
+
if cls not in cls._instances:
|
|
18
|
+
instance = super().__call__(*args, **kwargs)
|
|
19
|
+
cls._instances[cls] = instance
|
|
20
|
+
return cls._instances[cls]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LoggerConfig(metaclass=SingletonMeta):
|
|
24
|
+
"""Singleton class for configuring and managing the logger."""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.env = EnvironmentMode(os.environ.get("ENV_MODE", EnvironmentMode.PRODUCTION))
|
|
28
|
+
self.log_level = os.environ.get("LOG_LEVEL", "INFO" if self.env == EnvironmentMode.PRODUCTION else "DEBUG")
|
|
29
|
+
self.json_logs = os.environ.get("JSON_LOGS", "false").lower() == "true"
|
|
30
|
+
self.colorize = os.environ.get("COLORIZE", "true").lower() == "true"
|
|
31
|
+
|
|
32
|
+
# Configure logger on initialization
|
|
33
|
+
self._setup_logger()
|
|
34
|
+
|
|
35
|
+
def _format_record(self, record: Dict[str, Any]) -> str:
|
|
36
|
+
"""Format log records for human-readable output (text mode only)."""
|
|
37
|
+
# This formatter is only used when NOT in JSON mode
|
|
38
|
+
return (
|
|
39
|
+
"<green>[{level}]</green> <blue>{time:YYYY-MM-DD HH:mm:ss.SS}</blue>"
|
|
40
|
+
" | <cyan>{module}:{function}:{line}</cyan>"
|
|
41
|
+
" | <white>{message}</white>\n"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def _setup_logger(self) -> None:
|
|
45
|
+
"""Configure Loguru logger based on environment settings."""
|
|
46
|
+
# Remove default handlers
|
|
47
|
+
try:
|
|
48
|
+
loguru_logger.remove(0)
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
# Configure stderr handler with the correct settings for JSON or text mode
|
|
53
|
+
if self.json_logs:
|
|
54
|
+
# For JSON output, don't provide a formatter function - let Loguru handle serialization
|
|
55
|
+
loguru_logger.add(
|
|
56
|
+
sys.stderr,
|
|
57
|
+
level=self.log_level,
|
|
58
|
+
format=self._format_record, # No custom formatter for JSON mode
|
|
59
|
+
serialize=True, # Enable JSON serialization
|
|
60
|
+
colorize=False, # No color in JSON mode
|
|
61
|
+
enqueue=True,
|
|
62
|
+
backtrace=True,
|
|
63
|
+
diagnose=True,
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
# For text output, use the formatter function
|
|
67
|
+
loguru_logger.add(
|
|
68
|
+
sys.stderr,
|
|
69
|
+
level=self.log_level,
|
|
70
|
+
format=self._format_record,
|
|
71
|
+
serialize=False, # Disable JSON serialization
|
|
72
|
+
colorize=self.colorize,
|
|
73
|
+
enqueue=True,
|
|
74
|
+
backtrace=True,
|
|
75
|
+
diagnose=True,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Log startup configuration
|
|
79
|
+
loguru_logger.debug(
|
|
80
|
+
"Logger initialized",
|
|
81
|
+
extra={
|
|
82
|
+
"environment": self.env,
|
|
83
|
+
"log_level": self.log_level,
|
|
84
|
+
"json_logs": self.json_logs,
|
|
85
|
+
"colorize": self.colorize,
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def logger(self):
|
|
91
|
+
"""Get the configured logger instance."""
|
|
92
|
+
return loguru_logger
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class InterceptHandler(logging.Handler):
|
|
96
|
+
"""Redirect FastAPI's built-in logger to Loguru."""
|
|
97
|
+
|
|
98
|
+
def emit(self, record: Optional[logging.LogRecord]) -> None:
|
|
99
|
+
if record is None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
loguru_level = record.levelname.upper()
|
|
103
|
+
if record.exc_info is not None:
|
|
104
|
+
logger.opt(exception=record.exc_info).log(loguru_level, record.getMessage())
|
|
105
|
+
else:
|
|
106
|
+
logger.log(loguru_level, record.getMessage())
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Initialize singleton and expose logger
|
|
110
|
+
_logger_config = LoggerConfig()
|
|
111
|
+
logger = _logger_config.logger
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from langchain_core.messages import (
|
|
2
|
+
AIMessage,
|
|
3
|
+
BaseMessage,
|
|
4
|
+
HumanMessage,
|
|
5
|
+
ToolMessage,
|
|
6
|
+
)
|
|
7
|
+
from langchain_core.messages import (
|
|
8
|
+
ChatMessage as LangchainChatMessage,
|
|
9
|
+
)
|
|
10
|
+
from pydantic import HttpUrl, TypeAdapter
|
|
11
|
+
|
|
12
|
+
from langgraph_agent_toolkit.schema import ChatMessage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_str_is_http(x: str) -> str:
|
|
16
|
+
http_url_adapter = TypeAdapter(HttpUrl)
|
|
17
|
+
return str(http_url_adapter.validate_python(x))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def convert_message_content_to_string(content: str | list[str | dict]) -> str:
|
|
21
|
+
if isinstance(content, str):
|
|
22
|
+
return content
|
|
23
|
+
text: list[str] = []
|
|
24
|
+
for content_item in content:
|
|
25
|
+
if isinstance(content_item, str):
|
|
26
|
+
text.append(content_item)
|
|
27
|
+
continue
|
|
28
|
+
if content_item["type"] == "text":
|
|
29
|
+
text.append(content_item["text"])
|
|
30
|
+
return "".join(text)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def langchain_to_chat_message(message: BaseMessage) -> ChatMessage:
|
|
34
|
+
"""Create a ChatMessage from a LangChain message."""
|
|
35
|
+
match message:
|
|
36
|
+
case HumanMessage():
|
|
37
|
+
human_message = ChatMessage(
|
|
38
|
+
type="human",
|
|
39
|
+
content=convert_message_content_to_string(message.content),
|
|
40
|
+
)
|
|
41
|
+
return human_message
|
|
42
|
+
case AIMessage():
|
|
43
|
+
ai_message = ChatMessage(
|
|
44
|
+
type="ai",
|
|
45
|
+
content=convert_message_content_to_string(message.content),
|
|
46
|
+
)
|
|
47
|
+
if message.tool_calls:
|
|
48
|
+
ai_message.tool_calls = message.tool_calls
|
|
49
|
+
if message.response_metadata:
|
|
50
|
+
ai_message.response_metadata = message.response_metadata
|
|
51
|
+
return ai_message
|
|
52
|
+
case ToolMessage():
|
|
53
|
+
tool_message = ChatMessage(
|
|
54
|
+
type="tool",
|
|
55
|
+
content=convert_message_content_to_string(message.content),
|
|
56
|
+
tool_call_id=message.tool_call_id,
|
|
57
|
+
)
|
|
58
|
+
return tool_message
|
|
59
|
+
case LangchainChatMessage():
|
|
60
|
+
if message.role == "custom":
|
|
61
|
+
custom_message = ChatMessage(
|
|
62
|
+
type="custom",
|
|
63
|
+
content="",
|
|
64
|
+
custom_data=message.content[0],
|
|
65
|
+
)
|
|
66
|
+
return custom_message
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"Unsupported chat message role: {message.role}")
|
|
69
|
+
case _:
|
|
70
|
+
raise ValueError(f"Unsupported message type: {message.__class__.__name__}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def remove_tool_calls(content: str | list[str | dict]) -> str | list[str | dict]:
|
|
74
|
+
"""Remove tool calls from content."""
|
|
75
|
+
if isinstance(content, str):
|
|
76
|
+
return content
|
|
77
|
+
# Currently only Anthropic models stream tool calls, using content item type tool_use.
|
|
78
|
+
return [
|
|
79
|
+
content_item for content_item in content if isinstance(content_item, str) or content_item["type"] != "tool_use"
|
|
80
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import random
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from dotenv import find_dotenv, load_dotenv
|
|
6
|
+
from langchain_core.runnables import RunnableConfig
|
|
7
|
+
from langfuse.callback import CallbackHandler
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
load_dotenv(find_dotenv())
|
|
11
|
+
|
|
12
|
+
from langgraph_agent_toolkit.agents.agent_executor import AgentExecutor
|
|
13
|
+
from langgraph_agent_toolkit.core.settings import settings
|
|
14
|
+
from langgraph_agent_toolkit.helper.constants import DEFAULT_AGENT
|
|
15
|
+
from langgraph_agent_toolkit.helper.logging import logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# agent = AgentExecutor(*settings.AGENT_PATHS).get_agent("react-agent").graph
|
|
19
|
+
# agent = AgentExecutor(*settings.AGENT_PATHS).get_agent("react-agent-so").graph
|
|
20
|
+
agent = AgentExecutor(*settings.AGENT_PATHS).get_agent(DEFAULT_AGENT).graph
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def main() -> None:
|
|
24
|
+
inputs = {
|
|
25
|
+
"messages": [("user", "Find me a recipe for chocolate chip cookies and how Dynamo Kyiv played last game")]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
rd = random.Random()
|
|
29
|
+
rd.seed(0)
|
|
30
|
+
thread_id = UUID(int=rd.getrandbits(128), version=4)
|
|
31
|
+
|
|
32
|
+
logger.info(f"Starting agent with thread {thread_id}")
|
|
33
|
+
handler = CallbackHandler(session_id=str(thread_id))
|
|
34
|
+
|
|
35
|
+
result = await agent.ainvoke(
|
|
36
|
+
inputs,
|
|
37
|
+
config=RunnableConfig(
|
|
38
|
+
configurable={
|
|
39
|
+
"thread_id": thread_id,
|
|
40
|
+
"agent_temperature": 0.9,
|
|
41
|
+
"agent_top_p": 0.75,
|
|
42
|
+
"agent_max_tokens": 512,
|
|
43
|
+
"memory_saver_params": {"k": 3},
|
|
44
|
+
},
|
|
45
|
+
callbacks=[handler],
|
|
46
|
+
recursion_limit=15,
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
logger.info(result.keys())
|
|
50
|
+
logger.info(result)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
result["messages"][-1].pretty_print()
|
|
54
|
+
except Exception as _:
|
|
55
|
+
logger.warning("Can't print pretty following message.")
|
|
56
|
+
|
|
57
|
+
# Draw the agent graph as png
|
|
58
|
+
# requires:
|
|
59
|
+
# brew install graphviz
|
|
60
|
+
# export CFLAGS="-I $(brew --prefix graphviz)/include"
|
|
61
|
+
# export LDFLAGS="-L $(brew --prefix graphviz)/lib"
|
|
62
|
+
# pip install pygraphviz
|
|
63
|
+
#
|
|
64
|
+
# agent.get_graph().draw_png("agent_diagram.png")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from langgraph_agent_toolkit.client import AgentClient
|
|
4
|
+
from langgraph_agent_toolkit.core import settings
|
|
5
|
+
from langgraph_agent_toolkit.schema import ChatMessage
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def amain() -> None:
|
|
9
|
+
client = AgentClient(settings.BASE_URL)
|
|
10
|
+
|
|
11
|
+
print("Agent info:")
|
|
12
|
+
print(client.info)
|
|
13
|
+
|
|
14
|
+
print("Chat example:")
|
|
15
|
+
response = await client.ainvoke("Tell me a brief joke?", model="gpt-4o")
|
|
16
|
+
response.pretty_print()
|
|
17
|
+
|
|
18
|
+
print("\nStream example:")
|
|
19
|
+
async for message in client.astream("Share a quick fun fact?"):
|
|
20
|
+
if isinstance(message, str):
|
|
21
|
+
print(message, flush=True, end="")
|
|
22
|
+
elif isinstance(message, ChatMessage):
|
|
23
|
+
print("\n", flush=True)
|
|
24
|
+
message.pretty_print()
|
|
25
|
+
else:
|
|
26
|
+
print(f"ERROR: Unknown type - {type(message)}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
client = AgentClient(settings.BASE_URL)
|
|
31
|
+
|
|
32
|
+
print("Agent info:")
|
|
33
|
+
print(client.info)
|
|
34
|
+
|
|
35
|
+
print("Chat example:")
|
|
36
|
+
response = client.invoke("Tell me a brief joke?", model="gpt-4o")
|
|
37
|
+
response.pretty_print()
|
|
38
|
+
|
|
39
|
+
print("\nStream example:")
|
|
40
|
+
for message in client.stream("Share a quick fun fact?"):
|
|
41
|
+
if isinstance(message, str):
|
|
42
|
+
print(message, flush=True, end="")
|
|
43
|
+
elif isinstance(message, ChatMessage):
|
|
44
|
+
print("\n", flush=True)
|
|
45
|
+
message.pretty_print()
|
|
46
|
+
else:
|
|
47
|
+
print(f"ERROR: Unknown type - {type(message)}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
print("Running in sync mode")
|
|
52
|
+
main()
|
|
53
|
+
print("\n\n\n\n\n")
|
|
54
|
+
print("Running in async mode")
|
|
55
|
+
asyncio.run(amain())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from dotenv import load_dotenv
|
|
2
|
+
|
|
3
|
+
from langgraph_agent_toolkit.service.factory import RunnerType, ServiceRunner
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
# Create and run the service with the ServiceRunner
|
|
10
|
+
service = ServiceRunner(
|
|
11
|
+
custom_settings=dict(
|
|
12
|
+
AGENT_PATHS=[
|
|
13
|
+
"langgraph_agent_toolkit.agents.blueprints.react.agent:react_agent",
|
|
14
|
+
"langgraph_agent_toolkit.agents.blueprints.supervisor_agent.agent:supervisor_agent",
|
|
15
|
+
"langgraph_agent_toolkit.agents.blueprints.chatbot.agent:chatbot_agent",
|
|
16
|
+
]
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
handler = service.run(runner_type=RunnerType.UVICORN)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from langgraph_agent_toolkit.schema.models import AllModelEnum
|
|
2
|
+
from langgraph_agent_toolkit.schema.schema import (
|
|
3
|
+
AgentInfo,
|
|
4
|
+
ChatHistory,
|
|
5
|
+
ChatHistoryInput,
|
|
6
|
+
ChatMessage,
|
|
7
|
+
Feedback,
|
|
8
|
+
FeedbackResponse,
|
|
9
|
+
HealthCheck,
|
|
10
|
+
ServiceMetadata,
|
|
11
|
+
StreamInput,
|
|
12
|
+
UserInput,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AgentInfo",
|
|
18
|
+
"AllModelEnum",
|
|
19
|
+
"UserInput",
|
|
20
|
+
"ChatMessage",
|
|
21
|
+
"ServiceMetadata",
|
|
22
|
+
"StreamInput",
|
|
23
|
+
"Feedback",
|
|
24
|
+
"FeedbackResponse",
|
|
25
|
+
"ChatHistoryInput",
|
|
26
|
+
"ChatHistory",
|
|
27
|
+
"HealthCheck",
|
|
28
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from enum import StrEnum, auto
|
|
2
|
+
from typing import TypeAlias
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Provider(StrEnum):
|
|
6
|
+
OPENAI_COMPATIBLE = auto()
|
|
7
|
+
FAKE = auto()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OpenAICompatibleName(StrEnum):
|
|
11
|
+
"""OpenAI compatible model names.
|
|
12
|
+
|
|
13
|
+
https://platform.openai.com/docs/guides/text-generation
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
OPENAI_COMPATIBLE = "openai-compatible"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FakeModelName(StrEnum):
|
|
20
|
+
"""Fake model for testing."""
|
|
21
|
+
|
|
22
|
+
FAKE = "fake"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
AllModelEnum: TypeAlias = OpenAICompatibleName | FakeModelName
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from typing import Any, Literal, NotRequired
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, SerializeAsAny
|
|
4
|
+
from typing_extensions import TypedDict
|
|
5
|
+
|
|
6
|
+
from langgraph_agent_toolkit.helper.constants import (
|
|
7
|
+
DEFAULT_AGENT,
|
|
8
|
+
DEFAULT_OPENAI_COMPATIBLE_MODEL_PARAMS,
|
|
9
|
+
DEFAULT_RECURSION_LIMIT,
|
|
10
|
+
)
|
|
11
|
+
from langgraph_agent_toolkit.schema.models import AllModelEnum, OpenAICompatibleName
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentInfo(BaseModel):
|
|
15
|
+
"""Info about an available agent."""
|
|
16
|
+
|
|
17
|
+
key: str = Field(
|
|
18
|
+
description="Agent key.",
|
|
19
|
+
examples=["langgraph-supervisor-agent"],
|
|
20
|
+
)
|
|
21
|
+
description: str = Field(
|
|
22
|
+
description="Description of the agent.",
|
|
23
|
+
examples=["A research assistant."],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ServiceMetadata(BaseModel):
|
|
28
|
+
"""Metadata about the service including available agents and models."""
|
|
29
|
+
|
|
30
|
+
agents: list[AgentInfo] = Field(
|
|
31
|
+
description="List of available agents.",
|
|
32
|
+
)
|
|
33
|
+
models: list[AllModelEnum] = Field(
|
|
34
|
+
description="List of available LLMs.",
|
|
35
|
+
)
|
|
36
|
+
default_agent: str = Field(
|
|
37
|
+
description="Default agent used when none is specified.",
|
|
38
|
+
examples=[DEFAULT_AGENT],
|
|
39
|
+
)
|
|
40
|
+
default_model: AllModelEnum = Field(
|
|
41
|
+
description="Default model used when none is specified.",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UserInput(BaseModel):
|
|
46
|
+
"""Basic user input for the agent."""
|
|
47
|
+
|
|
48
|
+
message: str = Field(
|
|
49
|
+
description="User input to the agent.",
|
|
50
|
+
examples=["What is the weather in Tokyo?"],
|
|
51
|
+
)
|
|
52
|
+
model: SerializeAsAny[AllModelEnum] | None = Field(
|
|
53
|
+
title="Model",
|
|
54
|
+
description="LLM Model to use for the agent.",
|
|
55
|
+
default=OpenAICompatibleName.OPENAI_COMPATIBLE,
|
|
56
|
+
examples=[OpenAICompatibleName.OPENAI_COMPATIBLE],
|
|
57
|
+
)
|
|
58
|
+
thread_id: str | None = Field(
|
|
59
|
+
description="Thread ID to persist and continue a multi-turn conversation.",
|
|
60
|
+
default=None,
|
|
61
|
+
examples=["847c6285-8fc9-4560-a83f-4e6285809254"],
|
|
62
|
+
)
|
|
63
|
+
agent_config: dict[str, Any] = Field(
|
|
64
|
+
description="Additional configuration to pass through to the agent",
|
|
65
|
+
default={},
|
|
66
|
+
examples=[
|
|
67
|
+
{
|
|
68
|
+
"memory_saver_params": {"k": 6},
|
|
69
|
+
**DEFAULT_OPENAI_COMPATIBLE_MODEL_PARAMS,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
recursion_limit: int | None = Field(
|
|
74
|
+
description="Recursion limit for the agent.",
|
|
75
|
+
default=None, # Changed from DEFAULT_RECURSION_LIMIT to None
|
|
76
|
+
examples=[DEFAULT_RECURSION_LIMIT],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class StreamInput(UserInput):
|
|
81
|
+
"""User input for streaming the agent's response."""
|
|
82
|
+
|
|
83
|
+
stream_tokens: bool = Field(
|
|
84
|
+
description="Whether to stream LLM tokens to the client.",
|
|
85
|
+
default=True,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ToolCall(TypedDict):
|
|
90
|
+
"""Represents a request to call a tool."""
|
|
91
|
+
|
|
92
|
+
name: str
|
|
93
|
+
"""The name of the tool to be called."""
|
|
94
|
+
args: dict[str, Any]
|
|
95
|
+
"""The arguments to the tool call."""
|
|
96
|
+
id: str | None
|
|
97
|
+
"""An identifier associated with the tool call."""
|
|
98
|
+
type: NotRequired[Literal["tool_call"]]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ChatMessage(BaseModel):
|
|
102
|
+
"""Message in a chat."""
|
|
103
|
+
|
|
104
|
+
type: Literal["human", "ai", "tool", "custom"] = Field(
|
|
105
|
+
description="Role of the message.",
|
|
106
|
+
examples=["human", "ai", "tool", "custom"],
|
|
107
|
+
)
|
|
108
|
+
content: str = Field(
|
|
109
|
+
description="Content of the message.",
|
|
110
|
+
examples=["Hello, world!"],
|
|
111
|
+
)
|
|
112
|
+
tool_calls: list[ToolCall] = Field(
|
|
113
|
+
description="Tool calls in the message.",
|
|
114
|
+
default=[],
|
|
115
|
+
)
|
|
116
|
+
tool_call_id: str | None = Field(
|
|
117
|
+
description="Tool call that this message is responding to.",
|
|
118
|
+
default=None,
|
|
119
|
+
examples=["call_Jja7J89XsjrOLA5r!MEOW!SL"],
|
|
120
|
+
)
|
|
121
|
+
run_id: str | None = Field(
|
|
122
|
+
description="Run ID of the message.",
|
|
123
|
+
default=None,
|
|
124
|
+
examples=["847c6285-8fc9-4560-a83f-4e6285809254"],
|
|
125
|
+
)
|
|
126
|
+
response_metadata: dict[str, Any] = Field(
|
|
127
|
+
description="Response metadata. For example: response headers, logprobs, token counts.",
|
|
128
|
+
default={},
|
|
129
|
+
)
|
|
130
|
+
custom_data: dict[str, Any] = Field(
|
|
131
|
+
description="Custom message data.",
|
|
132
|
+
default={},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def pretty_repr(self) -> str:
|
|
136
|
+
"""Get a pretty representation of the message."""
|
|
137
|
+
base_title = self.type.title() + " Message"
|
|
138
|
+
padded = " " + base_title + " "
|
|
139
|
+
sep_len = (80 - len(padded)) // 2
|
|
140
|
+
sep = "=" * sep_len
|
|
141
|
+
second_sep = sep + "=" if len(padded) % 2 else sep
|
|
142
|
+
title = f"{sep}{padded}{second_sep}"
|
|
143
|
+
return f"{title}\n\n{self.content}"
|
|
144
|
+
|
|
145
|
+
def pretty_print(self) -> None:
|
|
146
|
+
print(self.pretty_repr()) # noqa: T201
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Feedback(BaseModel):
|
|
150
|
+
"""Feedback for a run, to record to LangSmith."""
|
|
151
|
+
|
|
152
|
+
run_id: str = Field(
|
|
153
|
+
description="Run ID to record feedback for.",
|
|
154
|
+
examples=["847c6285-8fc9-4560-a83f-4e6285809254"],
|
|
155
|
+
)
|
|
156
|
+
key: str = Field(
|
|
157
|
+
description="Feedback key.",
|
|
158
|
+
examples=["human-feedback-stars"],
|
|
159
|
+
)
|
|
160
|
+
score: float = Field(
|
|
161
|
+
description="Feedback score.",
|
|
162
|
+
examples=[0.8],
|
|
163
|
+
)
|
|
164
|
+
kwargs: dict[str, Any] = Field(
|
|
165
|
+
description="Additional feedback kwargs, passed to LangSmith.",
|
|
166
|
+
default={},
|
|
167
|
+
examples=[{"comment": "In-line human feedback"}],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class FeedbackResponse(BaseModel):
|
|
172
|
+
"""Response after recording feedback."""
|
|
173
|
+
|
|
174
|
+
status: Literal["success"] = "success"
|
|
175
|
+
run_id: str = Field(
|
|
176
|
+
description="Run ID for which feedback was recorded.",
|
|
177
|
+
examples=["847c6285-8fc9-4560-a83f-4e6285809254"],
|
|
178
|
+
)
|
|
179
|
+
message: str = Field(
|
|
180
|
+
description="Descriptive message about the feedback operation.",
|
|
181
|
+
default="Feedback recorded successfully.",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class ChatHistoryInput(BaseModel):
|
|
186
|
+
"""Input for retrieving chat history."""
|
|
187
|
+
|
|
188
|
+
thread_id: str = Field(
|
|
189
|
+
description="Thread ID to persist and continue a multi-turn conversation.",
|
|
190
|
+
examples=["847c6285-8fc9-4560-a83f-4e6285809254"],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ChatHistory(BaseModel):
|
|
195
|
+
messages: list[ChatMessage]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class HealthCheck(BaseModel):
|
|
199
|
+
"""Response model to validate and return when performing a health check."""
|
|
200
|
+
|
|
201
|
+
content: str = Field(
|
|
202
|
+
...,
|
|
203
|
+
description="Health status of the service.",
|
|
204
|
+
examples=["healthy"],
|
|
205
|
+
)
|
|
206
|
+
version: str = Field(
|
|
207
|
+
...,
|
|
208
|
+
description="Version of the service.",
|
|
209
|
+
examples=["1.0.0"],
|
|
210
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TaskData(BaseModel):
|
|
7
|
+
name: str | None = Field(description="Name of the task.", default=None, examples=["Check input safety"])
|
|
8
|
+
run_id: str = Field(
|
|
9
|
+
description="ID of the task run to pair state updates to.",
|
|
10
|
+
default="",
|
|
11
|
+
examples=["847c6285-8fc9-4560-a83f-4e6285809254"],
|
|
12
|
+
)
|
|
13
|
+
state: Literal["new", "running", "complete"] | None = Field(
|
|
14
|
+
description="Current state of given task instance.",
|
|
15
|
+
default=None,
|
|
16
|
+
examples=["running"],
|
|
17
|
+
)
|
|
18
|
+
result: Literal["success", "error"] | None = Field(
|
|
19
|
+
description="Result of given task instance.",
|
|
20
|
+
default=None,
|
|
21
|
+
examples=["running"],
|
|
22
|
+
)
|
|
23
|
+
data: dict[str, Any] = Field(
|
|
24
|
+
description="Additional data generated by the task.",
|
|
25
|
+
default={},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def completed(self) -> bool:
|
|
29
|
+
return self.state == "complete"
|
|
30
|
+
|
|
31
|
+
def completed_with_error(self) -> bool:
|
|
32
|
+
return self.state == "complete" and self.result == "error"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TaskDataStatus:
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
import streamlit as st
|
|
38
|
+
|
|
39
|
+
self.status = st.status("")
|
|
40
|
+
self.current_task_data: dict[str, TaskData] = {}
|
|
41
|
+
|
|
42
|
+
def add_and_draw_task_data(self, task_data: TaskData) -> None:
|
|
43
|
+
status = self.status
|
|
44
|
+
status_str = f"Task **{task_data.name}** "
|
|
45
|
+
match task_data.state:
|
|
46
|
+
case "new":
|
|
47
|
+
status_str += "has :blue[started]. Input:"
|
|
48
|
+
case "running":
|
|
49
|
+
status_str += "wrote:"
|
|
50
|
+
case "complete":
|
|
51
|
+
if task_data.result == "success":
|
|
52
|
+
status_str += ":green[completed successfully]. Output:"
|
|
53
|
+
else:
|
|
54
|
+
status_str += ":red[ended with error]. Output:"
|
|
55
|
+
status.write(status_str)
|
|
56
|
+
status.write(task_data.data)
|
|
57
|
+
status.write("---")
|
|
58
|
+
if task_data.run_id not in self.current_task_data:
|
|
59
|
+
# Status label always shows the last newly started task
|
|
60
|
+
status.update(label=f"""Task: {task_data.name}""")
|
|
61
|
+
self.current_task_data[task_data.run_id] = task_data
|
|
62
|
+
if all(entry.completed() for entry in self.current_task_data.values()):
|
|
63
|
+
# Status is "error" if any task has errored
|
|
64
|
+
if any(entry.completed_with_error() for entry in self.current_task_data.values()):
|
|
65
|
+
state = "error"
|
|
66
|
+
# Status is "complete" if all tasks have completed successfully
|
|
67
|
+
else:
|
|
68
|
+
state = "complete"
|
|
69
|
+
# Status is "running" until all tasks have completed
|
|
70
|
+
else:
|
|
71
|
+
state = "running"
|
|
72
|
+
status.update(state=state)
|