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,46 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, Request, status
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
from langgraph_agent_toolkit.helper.logging import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def handle_value_error(request: Request, exc: ValueError) -> JSONResponse:
|
|
10
|
+
"""Handle ValueError exceptions."""
|
|
11
|
+
logger.error(f"ValueError: {exc}")
|
|
12
|
+
return JSONResponse(
|
|
13
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
14
|
+
content={"detail": str(exc)},
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def handle_message_conversion_error(request: Request, exc: Exception) -> JSONResponse:
|
|
19
|
+
"""Handle errors when converting between message types."""
|
|
20
|
+
if isinstance(exc, ValueError) and "Unsupported message type" in str(exc):
|
|
21
|
+
logger.error(f"Message conversion error: {exc}")
|
|
22
|
+
return JSONResponse(
|
|
23
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
24
|
+
content={"detail": str(exc)},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
raise exc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def handle_unexpected_error(request: Request, exc: Exception) -> JSONResponse:
|
|
31
|
+
"""Handle unexpected exceptions."""
|
|
32
|
+
error_detail = f"{exc.__class__.__name__}: {exc}"
|
|
33
|
+
logger.error(f"Unexpected error: {error_detail}")
|
|
34
|
+
logger.error(traceback.format_exc())
|
|
35
|
+
return JSONResponse(
|
|
36
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
37
|
+
content={"detail": f"An unexpected error occurred: {str(exc)}"},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register_exception_handlers(app: FastAPI) -> None:
|
|
42
|
+
"""Register all exception handlers to the FastAPI app."""
|
|
43
|
+
app.exception_handler(ValueError)(handle_value_error)
|
|
44
|
+
app.exception_handler(Exception)(handle_unexpected_error)
|
|
45
|
+
# Register the message conversion error handler
|
|
46
|
+
app.add_exception_handler(ValueError, handle_message_conversion_error)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import uvicorn
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
|
|
10
|
+
from langgraph_agent_toolkit.core import settings as base_settings
|
|
11
|
+
from langgraph_agent_toolkit.helper.logging import logger
|
|
12
|
+
from langgraph_agent_toolkit.service.handler import create_app
|
|
13
|
+
from langgraph_agent_toolkit.service.types import RunnerType
|
|
14
|
+
from langgraph_agent_toolkit.service.utils import setup_logging
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ServiceRunner:
|
|
18
|
+
"""A factory class to run the API service in different ways.
|
|
19
|
+
|
|
20
|
+
This class provides methods to run the service with different runners:
|
|
21
|
+
- With Uvicorn
|
|
22
|
+
- With Gunicorn
|
|
23
|
+
- With Mangum (AWS Lambda)
|
|
24
|
+
- With Azure Functions
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, custom_settings: Optional[Dict[str, Any]] = None):
|
|
28
|
+
"""Initialize the ServiceRunner.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
custom_settings: Optional dictionary of settings to override the default settings.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
_ = setup_logging()
|
|
35
|
+
|
|
36
|
+
# Override global settings if provided
|
|
37
|
+
if custom_settings:
|
|
38
|
+
for key, value in custom_settings.items():
|
|
39
|
+
if hasattr(base_settings, key):
|
|
40
|
+
# Set the value in the global settings object
|
|
41
|
+
setattr(base_settings, key, value)
|
|
42
|
+
logger.info(f"Overriding setting {key}")
|
|
43
|
+
|
|
44
|
+
# Also set it as an environment variable for child processes
|
|
45
|
+
# Handle different types appropriately for environment variables
|
|
46
|
+
env_value = None
|
|
47
|
+
if isinstance(value, list):
|
|
48
|
+
# Convert lists to JSON strings
|
|
49
|
+
env_value = json.dumps(value)
|
|
50
|
+
elif isinstance(value, bool):
|
|
51
|
+
# Convert booleans to string "True" or "False"
|
|
52
|
+
env_value = str(value)
|
|
53
|
+
elif value is not None:
|
|
54
|
+
# Convert other values to strings
|
|
55
|
+
env_value = str(value)
|
|
56
|
+
|
|
57
|
+
if env_value is not None:
|
|
58
|
+
os.environ[f"LANGGRAPH_{key}"] = env_value
|
|
59
|
+
logger.info(f"Set environment variable LANGGRAPH_{key}={env_value}")
|
|
60
|
+
else:
|
|
61
|
+
logger.warning(f"Setting {key} not found in settings")
|
|
62
|
+
|
|
63
|
+
self.app = create_app()
|
|
64
|
+
|
|
65
|
+
def run_uvicorn(self):
|
|
66
|
+
"""Run the API service with uvicorn."""
|
|
67
|
+
# Set Compatible event loop policy on Windows Systems.
|
|
68
|
+
if sys.platform == "win32":
|
|
69
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
70
|
+
|
|
71
|
+
# In development mode with reload=True, we need to use an import string
|
|
72
|
+
# instead of passing the app instance directly
|
|
73
|
+
if base_settings.is_dev():
|
|
74
|
+
logger.info("Starting in development mode with hot reload enabled")
|
|
75
|
+
uvicorn.run(
|
|
76
|
+
"langgraph_agent_toolkit.service.handler:create_app",
|
|
77
|
+
host=base_settings.HOST,
|
|
78
|
+
port=base_settings.PORT,
|
|
79
|
+
reload=True,
|
|
80
|
+
factory=True,
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
# In production mode, use the app instance directly
|
|
84
|
+
uvicorn.run(
|
|
85
|
+
self.app,
|
|
86
|
+
host=base_settings.HOST,
|
|
87
|
+
port=base_settings.PORT,
|
|
88
|
+
reload=False,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def run_gunicorn(self, workers: int = 4):
|
|
92
|
+
"""Run the API service with gunicorn.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
workers: Number of worker processes.
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
from gunicorn.app.base import BaseApplication
|
|
100
|
+
|
|
101
|
+
class GunicornApp(BaseApplication):
|
|
102
|
+
def __init__(self, app, options=None):
|
|
103
|
+
self.options = options or {}
|
|
104
|
+
self.application = app
|
|
105
|
+
super().__init__()
|
|
106
|
+
|
|
107
|
+
def load_config(self):
|
|
108
|
+
for key, value in self.options.items():
|
|
109
|
+
self.cfg.set(key, value)
|
|
110
|
+
|
|
111
|
+
def load(self):
|
|
112
|
+
return self.application
|
|
113
|
+
|
|
114
|
+
options = {
|
|
115
|
+
"bind": f"{base_settings.HOST}:{base_settings.PORT}",
|
|
116
|
+
"workers": workers,
|
|
117
|
+
"worker_class": "uvicorn.workers.UvicornWorker",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
GunicornApp(self.app, options).run()
|
|
121
|
+
except ImportError:
|
|
122
|
+
logger.error("Gunicorn not installed. Install it with 'pip install gunicorn'")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
def run_aws_lambda(self):
|
|
126
|
+
"""Prepare the API service for AWS Lambda."""
|
|
127
|
+
try:
|
|
128
|
+
from mangum import Mangum
|
|
129
|
+
|
|
130
|
+
return Mangum(self.app)
|
|
131
|
+
except ImportError:
|
|
132
|
+
logger.error("Mangum not installed. Install it with 'pip install mangum'")
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
|
|
135
|
+
def run_azure_functions(self):
|
|
136
|
+
"""Prepare the API service for Azure Functions."""
|
|
137
|
+
try:
|
|
138
|
+
import azure.functions as func
|
|
139
|
+
|
|
140
|
+
async def main(req: func.HttpRequest) -> func.HttpResponse:
|
|
141
|
+
# Process the request through ASGI app
|
|
142
|
+
return await self._handle_azure_request(self.app, req)
|
|
143
|
+
|
|
144
|
+
return main
|
|
145
|
+
except ImportError:
|
|
146
|
+
logger.error("Azure Functions package not installed. Install with 'pip install azure-functions'")
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
async def _handle_azure_request(app: FastAPI, req: "func.HttpRequest") -> "func.HttpResponse":
|
|
151
|
+
"""Handle Azure Functions HTTP request."""
|
|
152
|
+
import azure.functions as func
|
|
153
|
+
|
|
154
|
+
# Convert request to ASGI format
|
|
155
|
+
scope = {
|
|
156
|
+
"type": "http",
|
|
157
|
+
"http_version": "1.1",
|
|
158
|
+
"method": req.method,
|
|
159
|
+
"path": req.url.path,
|
|
160
|
+
"query_string": req.url.query.encode(),
|
|
161
|
+
"headers": [(k.encode(), v.encode()) for k, v in req.headers.items()],
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Create response container
|
|
165
|
+
response_body = []
|
|
166
|
+
response_status = None
|
|
167
|
+
response_headers = []
|
|
168
|
+
|
|
169
|
+
async def receive():
|
|
170
|
+
return {"type": "http.request", "body": req.get_body() or b""}
|
|
171
|
+
|
|
172
|
+
async def send(message):
|
|
173
|
+
nonlocal response_status, response_headers
|
|
174
|
+
|
|
175
|
+
if message["type"] == "http.response.start":
|
|
176
|
+
response_status = message["status"]
|
|
177
|
+
response_headers = message["headers"]
|
|
178
|
+
elif message["type"] == "http.response.body":
|
|
179
|
+
response_body.append(message["body"])
|
|
180
|
+
|
|
181
|
+
# Process the request through ASGI app
|
|
182
|
+
await app(scope, receive, send)
|
|
183
|
+
|
|
184
|
+
# Build Azure Functions response
|
|
185
|
+
headers = {k.decode(): v.decode() for k, v in response_headers}
|
|
186
|
+
body = b"".join(response_body)
|
|
187
|
+
|
|
188
|
+
return func.HttpResponse(
|
|
189
|
+
body=body,
|
|
190
|
+
status_code=response_status,
|
|
191
|
+
headers=headers,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def run(self, runner_type: RunnerType = RunnerType.UVICORN, **kwargs):
|
|
195
|
+
"""Run the API service with the specified runner type.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
runner_type: The type of runner to use.
|
|
199
|
+
**kwargs: Additional arguments to pass to the runner.
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
match runner_type:
|
|
203
|
+
case RunnerType.UVICORN:
|
|
204
|
+
self.run_uvicorn()
|
|
205
|
+
case RunnerType.GUNICORN:
|
|
206
|
+
workers = kwargs.get("workers", 4)
|
|
207
|
+
self.run_gunicorn(workers=workers)
|
|
208
|
+
case RunnerType.AWS_LAMBDA:
|
|
209
|
+
return self.run_aws_lambda()
|
|
210
|
+
case RunnerType.AZURE_FUNCTIONS:
|
|
211
|
+
return self.run_azure_functions()
|
|
212
|
+
case _:
|
|
213
|
+
raise ValueError(f"Unknown runner type: {runner_type}")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from collections.abc import AsyncGenerator
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends, FastAPI
|
|
6
|
+
from langchain_core._api import LangChainBetaWarning
|
|
7
|
+
|
|
8
|
+
from langgraph_agent_toolkit import __version__
|
|
9
|
+
from langgraph_agent_toolkit.agents.agent_executor import AgentExecutor
|
|
10
|
+
from langgraph_agent_toolkit.core.memory.factory import MemoryFactory
|
|
11
|
+
from langgraph_agent_toolkit.core.observability.empty import EmptyObservability
|
|
12
|
+
from langgraph_agent_toolkit.core.observability.factory import ObservabilityFactory
|
|
13
|
+
from langgraph_agent_toolkit.core.observability.types import ObservabilityBackend
|
|
14
|
+
from langgraph_agent_toolkit.core.settings import settings
|
|
15
|
+
from langgraph_agent_toolkit.helper.logging import logger
|
|
16
|
+
from langgraph_agent_toolkit.service.exception_handlers import register_exception_handlers
|
|
17
|
+
from langgraph_agent_toolkit.service.middleware import LoggingMiddleware
|
|
18
|
+
from langgraph_agent_toolkit.service.routes import private_router, public_router
|
|
19
|
+
from langgraph_agent_toolkit.service.utils import verify_bearer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
warnings.filterwarnings("ignore", category=LangChainBetaWarning)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@asynccontextmanager
|
|
26
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
27
|
+
"""Create a lifespan context manager for the FastAPI app."""
|
|
28
|
+
observability = None
|
|
29
|
+
initialized_agents = []
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# Initialize observability platform
|
|
33
|
+
try:
|
|
34
|
+
observability = ObservabilityFactory.create(settings.OBSERVABILITY_BACKEND or ObservabilityBackend.EMPTY)
|
|
35
|
+
logger.info(f"Initialized observability backend: {settings.OBSERVABILITY_BACKEND}")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logger.error(f"Failed to initialize observability backend: {e}")
|
|
38
|
+
observability = EmptyObservability()
|
|
39
|
+
|
|
40
|
+
# Initialize memory backend
|
|
41
|
+
try:
|
|
42
|
+
memory_backend = MemoryFactory.create(settings.MEMORY_BACKEND)
|
|
43
|
+
logger.info(f"Initialized memory backend: {settings.MEMORY_BACKEND}")
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.error(f"Failed to initialize memory backend: {e}")
|
|
46
|
+
yield
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
# Initialize agent executor
|
|
50
|
+
try:
|
|
51
|
+
executor = AgentExecutor(*settings.AGENT_PATHS)
|
|
52
|
+
logger.info(f"Initialized AgentExecutor: {settings.AGENT_PATHS}")
|
|
53
|
+
# Store the executor in app.state for access in routes
|
|
54
|
+
app.state.agent_executor = executor
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Failed to initialize AgentExecutor: {e}")
|
|
57
|
+
yield
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
checkpoint = memory_backend.get_checkpoint_saver()
|
|
61
|
+
async with checkpoint as saver:
|
|
62
|
+
try:
|
|
63
|
+
await saver.setup()
|
|
64
|
+
agents = executor.get_all_agent_info()
|
|
65
|
+
|
|
66
|
+
if not agents:
|
|
67
|
+
logger.warning("No agents found in the executor.")
|
|
68
|
+
|
|
69
|
+
for a in agents:
|
|
70
|
+
try:
|
|
71
|
+
agent = executor.get_agent(a.key)
|
|
72
|
+
agent.graph.checkpointer = saver
|
|
73
|
+
|
|
74
|
+
# do not set up observability if it's already set
|
|
75
|
+
if not agent.observability:
|
|
76
|
+
agent.observability = observability
|
|
77
|
+
|
|
78
|
+
initialized_agents.append(a.key)
|
|
79
|
+
logger.info(f"Successfully initialized agent: {a.key}")
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Error setting up agent {a.key}: {e}")
|
|
82
|
+
|
|
83
|
+
if initialized_agents:
|
|
84
|
+
logger.info(f"Successfully initialized {len(initialized_agents)} agents")
|
|
85
|
+
else:
|
|
86
|
+
logger.warning("No agents were successfully initialized")
|
|
87
|
+
|
|
88
|
+
yield
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Error during database setup: {e}")
|
|
91
|
+
yield
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Error during initialization: {e}")
|
|
94
|
+
yield
|
|
95
|
+
finally:
|
|
96
|
+
if observability:
|
|
97
|
+
try:
|
|
98
|
+
logger.info("Closing observability platform...")
|
|
99
|
+
observability.before_shutdown()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Error closing observability: {e}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def create_app() -> FastAPI:
|
|
105
|
+
"""Create and configure the FastAPI application."""
|
|
106
|
+
logger.info(f"Initializing API service v{__version__}")
|
|
107
|
+
|
|
108
|
+
app = FastAPI(lifespan=lifespan)
|
|
109
|
+
|
|
110
|
+
# add middleware
|
|
111
|
+
app.add_middleware(LoggingMiddleware)
|
|
112
|
+
|
|
113
|
+
# Register exception handlers
|
|
114
|
+
register_exception_handlers(app)
|
|
115
|
+
|
|
116
|
+
# Include public router without authentication
|
|
117
|
+
app.include_router(public_router)
|
|
118
|
+
|
|
119
|
+
# Include private router with authentication
|
|
120
|
+
app.include_router(private_router, dependencies=[Depends(verify_bearer)])
|
|
121
|
+
|
|
122
|
+
return app
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from http.client import responses
|
|
2
|
+
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
5
|
+
|
|
6
|
+
from langgraph_agent_toolkit.helper.logging import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LoggingMiddleware(BaseHTTPMiddleware):
|
|
10
|
+
"""Middleware to log incoming requests and outgoing responses."""
|
|
11
|
+
|
|
12
|
+
async def dispatch(self, request: Request, call_next):
|
|
13
|
+
logger.info(f"HTTP Request: {request.method} {request.url}")
|
|
14
|
+
response = await call_next(request)
|
|
15
|
+
logger.info(
|
|
16
|
+
f'HTTP Response: {request.method} {request.url} "{response.status_code} {responses[response.status_code]}"'
|
|
17
|
+
)
|
|
18
|
+
return response
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException, Request, status
|
|
2
|
+
from fastapi.responses import RedirectResponse, StreamingResponse
|
|
3
|
+
from langchain_core.messages import AnyMessage
|
|
4
|
+
from langchain_core.runnables import RunnableConfig
|
|
5
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
6
|
+
|
|
7
|
+
from langgraph_agent_toolkit import __version__
|
|
8
|
+
from langgraph_agent_toolkit.agents.agent import Agent
|
|
9
|
+
from langgraph_agent_toolkit.core import settings
|
|
10
|
+
from langgraph_agent_toolkit.helper.constants import DEFAULT_AGENT
|
|
11
|
+
from langgraph_agent_toolkit.helper.logging import logger
|
|
12
|
+
from langgraph_agent_toolkit.helper.utils import langchain_to_chat_message
|
|
13
|
+
from langgraph_agent_toolkit.schema import (
|
|
14
|
+
ChatHistory,
|
|
15
|
+
ChatHistoryInput,
|
|
16
|
+
ChatMessage,
|
|
17
|
+
Feedback,
|
|
18
|
+
FeedbackResponse,
|
|
19
|
+
HealthCheck,
|
|
20
|
+
ServiceMetadata,
|
|
21
|
+
StreamInput,
|
|
22
|
+
UserInput,
|
|
23
|
+
)
|
|
24
|
+
from langgraph_agent_toolkit.service.utils import (
|
|
25
|
+
_sse_response_example,
|
|
26
|
+
get_agent,
|
|
27
|
+
get_agent_executor,
|
|
28
|
+
get_all_agent_info,
|
|
29
|
+
message_generator,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Create separate routers for private and public endpoints
|
|
34
|
+
private_router = APIRouter()
|
|
35
|
+
public_router = APIRouter()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@private_router.get("/info", tags=["info"])
|
|
39
|
+
async def info(request: Request) -> ServiceMetadata:
|
|
40
|
+
models = list(settings.AVAILABLE_MODELS)
|
|
41
|
+
models.sort()
|
|
42
|
+
return ServiceMetadata(
|
|
43
|
+
agents=get_all_agent_info(request),
|
|
44
|
+
models=models,
|
|
45
|
+
default_agent=DEFAULT_AGENT,
|
|
46
|
+
default_model=settings.DEFAULT_MODEL,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@private_router.post("/{agent_id}/invoke", tags=["agent"])
|
|
51
|
+
@private_router.post("/invoke", tags=["agent"])
|
|
52
|
+
async def invoke(user_input: UserInput, agent_id: str = DEFAULT_AGENT, request: Request = None) -> ChatMessage:
|
|
53
|
+
"""Invoke an agent with user input to retrieve a final response.
|
|
54
|
+
|
|
55
|
+
If agent_id is not provided, the default agent will be used.
|
|
56
|
+
Use thread_id to persist and continue a multi-turn conversation. run_id kwarg
|
|
57
|
+
is also attached to messages for recording feedback.
|
|
58
|
+
"""
|
|
59
|
+
executor = get_agent_executor(request)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
return await executor.invoke(
|
|
63
|
+
agent_id=agent_id,
|
|
64
|
+
message=user_input.message,
|
|
65
|
+
thread_id=user_input.thread_id,
|
|
66
|
+
model=user_input.model,
|
|
67
|
+
agent_config=user_input.agent_config,
|
|
68
|
+
recursion_limit=user_input.recursion_limit,
|
|
69
|
+
)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f"An exception occurred: {e}")
|
|
72
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Unexpected error")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@private_router.post(
|
|
76
|
+
"/{agent_id}/stream",
|
|
77
|
+
response_class=StreamingResponse,
|
|
78
|
+
responses=_sse_response_example(),
|
|
79
|
+
tags=["agent"],
|
|
80
|
+
)
|
|
81
|
+
@private_router.post(
|
|
82
|
+
"/stream",
|
|
83
|
+
response_class=StreamingResponse,
|
|
84
|
+
responses=_sse_response_example(),
|
|
85
|
+
tags=["agent"],
|
|
86
|
+
)
|
|
87
|
+
async def stream(user_input: StreamInput, agent_id: str = DEFAULT_AGENT, request: Request = None) -> StreamingResponse:
|
|
88
|
+
"""Stream an agent's response to a user input, including intermediate messages and tokens.
|
|
89
|
+
|
|
90
|
+
If agent_id is not provided, the default agent will be used.
|
|
91
|
+
Use thread_id to persist and continue a multi-turn conversation. run_id kwarg
|
|
92
|
+
is also attached to all messages for recording feedback.
|
|
93
|
+
|
|
94
|
+
Set `stream_tokens=false` to return intermediate messages but not token-by-token.
|
|
95
|
+
"""
|
|
96
|
+
return StreamingResponse(
|
|
97
|
+
message_generator(user_input, request, agent_id),
|
|
98
|
+
media_type="text/event-stream",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@private_router.post("/feedback", status_code=status.HTTP_201_CREATED, tags=["feedback"])
|
|
103
|
+
async def feedback(feedback: Feedback, agent_id: str = DEFAULT_AGENT, request: Request = None) -> FeedbackResponse:
|
|
104
|
+
"""Record feedback for a run to the configured observability platform.
|
|
105
|
+
|
|
106
|
+
This routes the feedback to the appropriate platform based on the agent's configuration.
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
agent = get_agent(request, agent_id)
|
|
110
|
+
agent.observability.record_feedback(
|
|
111
|
+
run_id=feedback.run_id,
|
|
112
|
+
key=feedback.key,
|
|
113
|
+
score=feedback.score,
|
|
114
|
+
**feedback.kwargs,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return FeedbackResponse(
|
|
118
|
+
run_id=feedback.run_id,
|
|
119
|
+
message=f"Feedback '{feedback.key}' recorded successfully for run {feedback.run_id}.",
|
|
120
|
+
)
|
|
121
|
+
except ValueError as e:
|
|
122
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f"An exception occurred while recording feedback: {e}")
|
|
125
|
+
raise HTTPException(
|
|
126
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Unexpected error recording feedback"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@private_router.post("/history", tags=["history"])
|
|
131
|
+
def history(input: ChatHistoryInput, request: Request = None) -> ChatHistory:
|
|
132
|
+
"""Get chat history."""
|
|
133
|
+
agent: Agent = get_agent(request, DEFAULT_AGENT)
|
|
134
|
+
try:
|
|
135
|
+
agent_graph: CompiledStateGraph = agent.graph
|
|
136
|
+
state_snapshot = agent_graph.get_state(
|
|
137
|
+
config=RunnableConfig(
|
|
138
|
+
configurable={
|
|
139
|
+
"thread_id": input.thread_id,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
messages: list[AnyMessage] = state_snapshot.values["messages"]
|
|
144
|
+
chat_messages: list[ChatMessage] = [langchain_to_chat_message(m) for m in messages]
|
|
145
|
+
return ChatHistory(messages=chat_messages)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"An exception occurred: {e}")
|
|
148
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Unexpected error")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@public_router.get("/", tags=["public"])
|
|
152
|
+
async def redirect_to_docs() -> RedirectResponse:
|
|
153
|
+
return RedirectResponse(url="/docs")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@public_router.get(
|
|
157
|
+
"/health",
|
|
158
|
+
tags=["public", "healthcheck"],
|
|
159
|
+
summary="Perform a Health Check",
|
|
160
|
+
response_description="Return HTTP Status Code 200 (OK)",
|
|
161
|
+
status_code=status.HTTP_200_OK,
|
|
162
|
+
response_model=HealthCheck,
|
|
163
|
+
)
|
|
164
|
+
async def health_check() -> HealthCheck:
|
|
165
|
+
"""Health check endpoint."""
|
|
166
|
+
return HealthCheck(
|
|
167
|
+
content="healthy",
|
|
168
|
+
version=__version__,
|
|
169
|
+
)
|