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
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, List, Literal, Optional, Sequence, Union
|
|
5
|
+
|
|
6
|
+
from langchain_core.prompt_values import PromptValue
|
|
7
|
+
from langchain_core.prompts.chat import (
|
|
8
|
+
AIMessagePromptTemplate,
|
|
9
|
+
BaseChatPromptTemplate,
|
|
10
|
+
BaseMessage,
|
|
11
|
+
BaseMessagePromptTemplate,
|
|
12
|
+
ChatPromptTemplate,
|
|
13
|
+
ChatPromptValue,
|
|
14
|
+
HumanMessagePromptTemplate,
|
|
15
|
+
MessageLikeRepresentation,
|
|
16
|
+
MessagesPlaceholder,
|
|
17
|
+
SystemMessagePromptTemplate,
|
|
18
|
+
)
|
|
19
|
+
from langchain_core.prompts.string import get_template_variables
|
|
20
|
+
from pydantic import Field
|
|
21
|
+
|
|
22
|
+
from langgraph_agent_toolkit.core.observability.base import BaseObservabilityPlatform
|
|
23
|
+
from langgraph_agent_toolkit.core.observability.factory import ObservabilityFactory
|
|
24
|
+
from langgraph_agent_toolkit.core.observability.types import MessageRole, ObservabilityBackend, PromptReturnType
|
|
25
|
+
from langgraph_agent_toolkit.helper.constants import DEFAULT_CACHE_TTL_SECOND
|
|
26
|
+
from langgraph_agent_toolkit.helper.logging import logger
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _convert_template_format(content: str, target_format: str) -> str:
|
|
30
|
+
"""Convert template string between different formats."""
|
|
31
|
+
if not content or not isinstance(content, str):
|
|
32
|
+
return content
|
|
33
|
+
|
|
34
|
+
if target_format == "jinja2" and "{" in content and "{{" not in content:
|
|
35
|
+
# Convert f-string format to Jinja2 format
|
|
36
|
+
return re.sub(r"{(\w+)}", r"{{ \1 }}", content)
|
|
37
|
+
elif target_format == "f-string" and "{{" in content:
|
|
38
|
+
# Convert Jinja2 format to f-string format
|
|
39
|
+
return re.sub(r"{{\s*(\w+)\s*}}", r"{\1}", content)
|
|
40
|
+
return content
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ObservabilityChatPromptTemplate(ChatPromptTemplate):
|
|
44
|
+
"""A chat prompt template that loads prompts from observability platforms."""
|
|
45
|
+
|
|
46
|
+
prompt_name: Optional[str] = Field(default=None, description="Name of the prompt to load")
|
|
47
|
+
prompt_version: Optional[int] = Field(default=None, description="Version of the prompt")
|
|
48
|
+
prompt_label: Optional[str] = Field(default=None, description="Label of the prompt")
|
|
49
|
+
load_at_runtime: bool = Field(default=False, description="Whether to load prompt at runtime")
|
|
50
|
+
observability_backend: Optional[ObservabilityBackend] = Field(
|
|
51
|
+
default=None, description="Observability backend to use"
|
|
52
|
+
)
|
|
53
|
+
cache_ttl_seconds: int = Field(default=DEFAULT_CACHE_TTL_SECOND, description="Cache TTL for prompts")
|
|
54
|
+
template_format: str = Field(default="f-string", description="Format of the template")
|
|
55
|
+
|
|
56
|
+
_observability_platform: Optional[BaseObservabilityPlatform] = None
|
|
57
|
+
_loaded_prompt: Any = None
|
|
58
|
+
_last_load_time: float = 0
|
|
59
|
+
|
|
60
|
+
model_config = {"extra": "allow"}
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
messages: Optional[Sequence[MessageLikeRepresentation]] = None,
|
|
65
|
+
*,
|
|
66
|
+
prompt_name: Optional[str] = None,
|
|
67
|
+
prompt_version: Optional[int] = None,
|
|
68
|
+
prompt_label: Optional[str] = None,
|
|
69
|
+
load_at_runtime: bool = False,
|
|
70
|
+
observability_platform: Optional[BaseObservabilityPlatform] = None,
|
|
71
|
+
observability_backend: Optional[Union[ObservabilityBackend, str]] = None,
|
|
72
|
+
cache_ttl_seconds: int = DEFAULT_CACHE_TTL_SECOND,
|
|
73
|
+
template_format: Literal["f-string", "mustache", "jinja2"] = "f-string",
|
|
74
|
+
input_variables: Optional[List[str]] = None,
|
|
75
|
+
partial_variables: Optional[Dict[str, Any]] = None,
|
|
76
|
+
**kwargs: Any,
|
|
77
|
+
):
|
|
78
|
+
"""Initialize ObservabilityChatPromptTemplate."""
|
|
79
|
+
# Process observability platform/backend
|
|
80
|
+
_observability_platform = observability_platform
|
|
81
|
+
_observability_backend = observability_backend
|
|
82
|
+
|
|
83
|
+
if not observability_platform and observability_backend:
|
|
84
|
+
_observability_backend = (
|
|
85
|
+
ObservabilityBackend(observability_backend)
|
|
86
|
+
if isinstance(observability_backend, str)
|
|
87
|
+
else observability_backend
|
|
88
|
+
)
|
|
89
|
+
_observability_platform = ObservabilityFactory.create(_observability_backend)
|
|
90
|
+
|
|
91
|
+
# Load messages if needed
|
|
92
|
+
_messages = messages or []
|
|
93
|
+
if not load_at_runtime and prompt_name and _observability_platform:
|
|
94
|
+
try:
|
|
95
|
+
self._loaded_prompt = loaded_prompt = self._load_prompt_from_platform(
|
|
96
|
+
_observability_platform,
|
|
97
|
+
prompt_name=prompt_name,
|
|
98
|
+
prompt_version=prompt_version,
|
|
99
|
+
prompt_label=prompt_label,
|
|
100
|
+
cache_ttl_seconds=cache_ttl_seconds,
|
|
101
|
+
template_format=template_format,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Process returned prompt based on type
|
|
105
|
+
if not _messages:
|
|
106
|
+
if hasattr(loaded_prompt, "messages"):
|
|
107
|
+
_messages = self._process_messages_from_prompt(loaded_prompt.messages, template_format)
|
|
108
|
+
elif isinstance(loaded_prompt, BaseChatPromptTemplate):
|
|
109
|
+
_messages = loaded_prompt.messages
|
|
110
|
+
elif isinstance(loaded_prompt, list):
|
|
111
|
+
processed_messages = self._process_list_prompt(loaded_prompt, template_format)
|
|
112
|
+
if processed_messages:
|
|
113
|
+
_messages = processed_messages
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.warning(f"Failed to load prompt {prompt_name}: {e}")
|
|
116
|
+
|
|
117
|
+
# Save input variables and partial variables
|
|
118
|
+
_input_variables = list(input_variables) if input_variables else []
|
|
119
|
+
_partial_variables = dict(partial_variables) if partial_variables else {}
|
|
120
|
+
|
|
121
|
+
# Initialize parent class with messages only
|
|
122
|
+
super().__init__(messages=_messages)
|
|
123
|
+
|
|
124
|
+
# Set attributes
|
|
125
|
+
self.prompt_name = prompt_name
|
|
126
|
+
self.prompt_version = prompt_version
|
|
127
|
+
self.prompt_label = prompt_label
|
|
128
|
+
self.load_at_runtime = load_at_runtime
|
|
129
|
+
self.observability_backend = _observability_backend
|
|
130
|
+
self.cache_ttl_seconds = cache_ttl_seconds
|
|
131
|
+
self.template_format = template_format
|
|
132
|
+
|
|
133
|
+
# Explicitly set input variables and partial variables
|
|
134
|
+
if _input_variables:
|
|
135
|
+
self.input_variables = _input_variables
|
|
136
|
+
|
|
137
|
+
if _partial_variables:
|
|
138
|
+
self.partial_variables = _partial_variables
|
|
139
|
+
|
|
140
|
+
# Set private attributes
|
|
141
|
+
self._observability_platform = _observability_platform
|
|
142
|
+
self._last_load_time = time.time()
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def observability_platform(self) -> Optional[BaseObservabilityPlatform]:
|
|
146
|
+
"""Get the observability platform."""
|
|
147
|
+
return self._observability_platform
|
|
148
|
+
|
|
149
|
+
@observability_platform.setter
|
|
150
|
+
def observability_platform(self, platform: BaseObservabilityPlatform) -> None:
|
|
151
|
+
"""Set the observability platform."""
|
|
152
|
+
self._observability_platform = platform
|
|
153
|
+
self._loaded_prompt = None
|
|
154
|
+
self._last_load_time = 0
|
|
155
|
+
|
|
156
|
+
def _load_prompt_from_platform(
|
|
157
|
+
self,
|
|
158
|
+
platform: BaseObservabilityPlatform,
|
|
159
|
+
prompt_name: str,
|
|
160
|
+
prompt_version: Optional[int] = None,
|
|
161
|
+
prompt_label: Optional[str] = None,
|
|
162
|
+
cache_ttl_seconds: int = DEFAULT_CACHE_TTL_SECOND,
|
|
163
|
+
template_format: Literal["f-string", "mustache", "jinja2"] = "f-string",
|
|
164
|
+
) -> PromptReturnType:
|
|
165
|
+
"""Load prompt from observability platform."""
|
|
166
|
+
kwargs = {}
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
sig = inspect.signature(platform.pull_prompt).parameters
|
|
170
|
+
if "cache_ttl_seconds" in sig:
|
|
171
|
+
kwargs["cache_ttl_seconds"] = cache_ttl_seconds
|
|
172
|
+
if "template_format" in sig:
|
|
173
|
+
kwargs["template_format"] = template_format
|
|
174
|
+
except (ValueError, TypeError):
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
if prompt_version is not None:
|
|
178
|
+
kwargs["version"] = prompt_version
|
|
179
|
+
if prompt_label is not None:
|
|
180
|
+
kwargs["label"] = prompt_label
|
|
181
|
+
|
|
182
|
+
return platform.pull_prompt(prompt_name, **kwargs)
|
|
183
|
+
|
|
184
|
+
def _load_prompt_from_observability(self) -> PromptReturnType:
|
|
185
|
+
"""Load prompt from observability platform."""
|
|
186
|
+
if not self._observability_platform:
|
|
187
|
+
raise ValueError("No observability platform set")
|
|
188
|
+
|
|
189
|
+
if not self.prompt_name:
|
|
190
|
+
raise ValueError("No prompt name provided")
|
|
191
|
+
|
|
192
|
+
return self._load_prompt_from_platform(
|
|
193
|
+
self._observability_platform,
|
|
194
|
+
prompt_name=self.prompt_name,
|
|
195
|
+
prompt_version=self.prompt_version,
|
|
196
|
+
prompt_label=self.prompt_label,
|
|
197
|
+
cache_ttl_seconds=self.cache_ttl_seconds,
|
|
198
|
+
template_format=self.template_format,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def _update_messages_from_loaded_prompt(self) -> None:
|
|
202
|
+
"""Update the messages from the loaded prompt."""
|
|
203
|
+
if self._loaded_prompt is None:
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
MESSAGE_TYPE_MAP = {
|
|
207
|
+
"system": SystemMessagePromptTemplate,
|
|
208
|
+
"human": HumanMessagePromptTemplate,
|
|
209
|
+
"ai": AIMessagePromptTemplate,
|
|
210
|
+
"assistant": AIMessagePromptTemplate,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if hasattr(self._loaded_prompt, "messages"):
|
|
214
|
+
processed_messages = []
|
|
215
|
+
for msg in self._loaded_prompt.messages:
|
|
216
|
+
if isinstance(msg, BaseMessage) and msg.type in MESSAGE_TYPE_MAP:
|
|
217
|
+
content = _convert_template_format(msg.content, self.template_format)
|
|
218
|
+
template_class = MESSAGE_TYPE_MAP[msg.type]
|
|
219
|
+
processed_messages.append(
|
|
220
|
+
template_class.from_template(content, template_format=self.template_format)
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
processed_messages.append(msg)
|
|
224
|
+
|
|
225
|
+
self.messages = processed_messages
|
|
226
|
+
elif isinstance(self._loaded_prompt, list):
|
|
227
|
+
processed_messages = self._process_list_prompt(self._loaded_prompt, self.template_format)
|
|
228
|
+
if processed_messages:
|
|
229
|
+
self.messages = processed_messages
|
|
230
|
+
elif isinstance(self._loaded_prompt, BaseChatPromptTemplate):
|
|
231
|
+
self.messages = self._loaded_prompt.messages
|
|
232
|
+
|
|
233
|
+
def _process_messages_from_prompt(self, messages: Any, template_format: str) -> List[MessageLikeRepresentation]:
|
|
234
|
+
"""Process messages from a loaded prompt."""
|
|
235
|
+
MESSAGE_TYPE_MAP = {
|
|
236
|
+
MessageRole.SYSTEM: SystemMessagePromptTemplate,
|
|
237
|
+
MessageRole.HUMAN: HumanMessagePromptTemplate,
|
|
238
|
+
MessageRole.USER: HumanMessagePromptTemplate,
|
|
239
|
+
MessageRole.AI: AIMessagePromptTemplate,
|
|
240
|
+
MessageRole.ASSISTANT: AIMessagePromptTemplate,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
processed_messages = []
|
|
244
|
+
for msg in messages:
|
|
245
|
+
if isinstance(msg, MessagesPlaceholder):
|
|
246
|
+
# Preserve MessagesPlaceholder objects
|
|
247
|
+
processed_messages.append(msg)
|
|
248
|
+
elif isinstance(msg, BaseMessage) and msg.type in MESSAGE_TYPE_MAP:
|
|
249
|
+
content = _convert_template_format(msg.content, template_format)
|
|
250
|
+
template_class = MESSAGE_TYPE_MAP[MessageRole(msg.type)]
|
|
251
|
+
processed_messages.append(template_class.from_template(content, template_format=template_format))
|
|
252
|
+
elif isinstance(msg, tuple) and len(msg) == 2:
|
|
253
|
+
role, content = msg
|
|
254
|
+
if role in MESSAGE_TYPE_MAP:
|
|
255
|
+
content = _convert_template_format(content, template_format)
|
|
256
|
+
template_class = MESSAGE_TYPE_MAP[role]
|
|
257
|
+
processed_messages.append(template_class.from_template(content, template_format=template_format))
|
|
258
|
+
elif isinstance(msg, dict) and "role" in msg and "content" in msg:
|
|
259
|
+
role, content = msg["role"], msg["content"]
|
|
260
|
+
if role in MESSAGE_TYPE_MAP:
|
|
261
|
+
content = _convert_template_format(content, template_format)
|
|
262
|
+
template_class = MESSAGE_TYPE_MAP[role]
|
|
263
|
+
processed_messages.append(template_class.from_template(content, template_format=template_format))
|
|
264
|
+
else:
|
|
265
|
+
processed_messages.append(msg)
|
|
266
|
+
|
|
267
|
+
return processed_messages
|
|
268
|
+
|
|
269
|
+
def _process_list_prompt(
|
|
270
|
+
self, prompt_list: List[Any], template_format: str
|
|
271
|
+
) -> Optional[List[MessageLikeRepresentation]]:
|
|
272
|
+
"""Process a list prompt from an observability platform."""
|
|
273
|
+
MESSAGE_TYPE_MAP = {
|
|
274
|
+
MessageRole.SYSTEM: SystemMessagePromptTemplate,
|
|
275
|
+
MessageRole.HUMAN: HumanMessagePromptTemplate,
|
|
276
|
+
MessageRole.USER: HumanMessagePromptTemplate,
|
|
277
|
+
MessageRole.AI: AIMessagePromptTemplate,
|
|
278
|
+
MessageRole.ASSISTANT: AIMessagePromptTemplate,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
processed_messages = []
|
|
282
|
+
|
|
283
|
+
# Handle list of tuples (role, content)
|
|
284
|
+
if all(isinstance(item, tuple) and len(item) == 2 for item in prompt_list):
|
|
285
|
+
for role, content in prompt_list:
|
|
286
|
+
if role in MESSAGE_TYPE_MAP:
|
|
287
|
+
content = _convert_template_format(content, template_format)
|
|
288
|
+
template_class = MESSAGE_TYPE_MAP[role]
|
|
289
|
+
processed_messages.append(template_class.from_template(content, template_format=template_format))
|
|
290
|
+
return processed_messages
|
|
291
|
+
|
|
292
|
+
# Handle list of dicts with role and content
|
|
293
|
+
if all(isinstance(item, dict) and "role" in item and "content" in item for item in prompt_list):
|
|
294
|
+
for item in prompt_list:
|
|
295
|
+
role, content = item["role"], item["content"]
|
|
296
|
+
if role in MESSAGE_TYPE_MAP:
|
|
297
|
+
content = _convert_template_format(content, template_format)
|
|
298
|
+
template_class = MESSAGE_TYPE_MAP[role]
|
|
299
|
+
processed_messages.append(template_class.from_template(content, template_format=template_format))
|
|
300
|
+
# Handle MessagesPlaceholder
|
|
301
|
+
elif role.lower() in (MessageRole.PLACEHOLDER, MessageRole.MESSAGES_PLACEHOLDER):
|
|
302
|
+
# Create a MessagesPlaceholder with the content as variable name
|
|
303
|
+
processed_messages.append(MessagesPlaceholder(variable_name=content))
|
|
304
|
+
return processed_messages
|
|
305
|
+
|
|
306
|
+
return processed_messages or None
|
|
307
|
+
|
|
308
|
+
def _format_message_with_input(self, msg: Any, input_dict: Dict[str, Any]) -> List[BaseMessage]:
|
|
309
|
+
"""Format a message with input."""
|
|
310
|
+
full_input_dict = dict(self.partial_variables or {})
|
|
311
|
+
full_input_dict.update(input_dict)
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
# Special handling for MessagesPlaceholder
|
|
315
|
+
if isinstance(msg, MessagesPlaceholder):
|
|
316
|
+
var_name = msg.variable_name
|
|
317
|
+
if var_name in full_input_dict:
|
|
318
|
+
messages_value = full_input_dict[var_name]
|
|
319
|
+
if isinstance(messages_value, list):
|
|
320
|
+
return messages_value if all(isinstance(m, BaseMessage) for m in messages_value) else [msg]
|
|
321
|
+
return [messages_value] if isinstance(messages_value, BaseMessage) else [msg]
|
|
322
|
+
return []
|
|
323
|
+
|
|
324
|
+
if hasattr(msg, "format") and callable(msg.format):
|
|
325
|
+
return [msg.format(**full_input_dict)]
|
|
326
|
+
elif hasattr(msg, "format_messages") and callable(msg.format_messages):
|
|
327
|
+
return msg.format_messages(**full_input_dict)
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.warning(f"Error formatting message: {e}")
|
|
330
|
+
|
|
331
|
+
return [msg]
|
|
332
|
+
|
|
333
|
+
async def _aformat_message_with_input(self, msg: Any, input_dict: Dict[str, Any]) -> List[BaseMessage]:
|
|
334
|
+
"""Asynchronously format a message with input."""
|
|
335
|
+
full_input_dict = dict(self.partial_variables or {})
|
|
336
|
+
full_input_dict.update(input_dict)
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
# Special handling for MessagesPlaceholder
|
|
340
|
+
if isinstance(msg, MessagesPlaceholder):
|
|
341
|
+
var_name = msg.variable_name
|
|
342
|
+
if var_name in full_input_dict:
|
|
343
|
+
messages_value = full_input_dict[var_name]
|
|
344
|
+
if isinstance(messages_value, list):
|
|
345
|
+
return messages_value if all(isinstance(m, BaseMessage) for m in messages_value) else [msg]
|
|
346
|
+
return [messages_value] if isinstance(messages_value, BaseMessage) else [msg]
|
|
347
|
+
return []
|
|
348
|
+
|
|
349
|
+
if hasattr(msg, "aformat") and callable(msg.aformat):
|
|
350
|
+
return [await msg.aformat(**full_input_dict)]
|
|
351
|
+
elif hasattr(msg, "aformat_messages") and callable(msg.aformat_messages):
|
|
352
|
+
return await msg.aformat_messages(**full_input_dict)
|
|
353
|
+
elif hasattr(msg, "format") and callable(msg.format):
|
|
354
|
+
return [msg.format(**full_input_dict)]
|
|
355
|
+
elif hasattr(msg, "format_messages") and callable(msg.format_messages):
|
|
356
|
+
return msg.format_messages(**full_input_dict)
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
return [msg]
|
|
361
|
+
|
|
362
|
+
def _ensure_messages_loaded(self) -> None:
|
|
363
|
+
"""Ensure messages are loaded from observability platform if needed."""
|
|
364
|
+
if not self.load_at_runtime or not self.prompt_name or not self._observability_platform:
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
current_time = time.time()
|
|
368
|
+
if self._loaded_prompt is None or current_time - self._last_load_time > self.cache_ttl_seconds:
|
|
369
|
+
try:
|
|
370
|
+
self._loaded_prompt = self._load_prompt_from_observability()
|
|
371
|
+
self._last_load_time = current_time
|
|
372
|
+
self._update_messages_from_loaded_prompt()
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.error(f"Failed to load prompt: {e}")
|
|
375
|
+
if not self.messages:
|
|
376
|
+
raise ValueError(f"Failed to load prompt and no fallback available: {e}")
|
|
377
|
+
|
|
378
|
+
def invoke(self, input: Any, config: Optional[Dict[str, Any]] = None, **kwargs: Any) -> PromptValue:
|
|
379
|
+
"""Invoke the prompt template."""
|
|
380
|
+
self._ensure_messages_loaded()
|
|
381
|
+
|
|
382
|
+
if isinstance(input, dict):
|
|
383
|
+
formatted_messages = []
|
|
384
|
+
for msg in self.messages:
|
|
385
|
+
formatted_messages.extend(self._format_message_with_input(msg, input))
|
|
386
|
+
return ChatPromptValue(messages=formatted_messages)
|
|
387
|
+
|
|
388
|
+
return super().invoke(input=input, config=config, **kwargs)
|
|
389
|
+
|
|
390
|
+
async def ainvoke(self, input: Any, config: Optional[Dict[str, Any]] = None, **kwargs: Any) -> PromptValue:
|
|
391
|
+
"""Asynchronously invoke the prompt template."""
|
|
392
|
+
self._ensure_messages_loaded()
|
|
393
|
+
|
|
394
|
+
if isinstance(input, dict):
|
|
395
|
+
formatted_messages = []
|
|
396
|
+
for msg in self.messages:
|
|
397
|
+
formatted_messages.extend(await self._aformat_message_with_input(msg, input))
|
|
398
|
+
return ChatPromptValue(messages=formatted_messages)
|
|
399
|
+
|
|
400
|
+
return await super().ainvoke(input=input, config=config, **kwargs)
|
|
401
|
+
|
|
402
|
+
@classmethod
|
|
403
|
+
def from_observability_platform(
|
|
404
|
+
cls,
|
|
405
|
+
prompt_name: str,
|
|
406
|
+
observability_platform: BaseObservabilityPlatform,
|
|
407
|
+
*,
|
|
408
|
+
prompt_version: Optional[int] = None,
|
|
409
|
+
prompt_label: Optional[str] = None,
|
|
410
|
+
load_at_runtime: bool = True,
|
|
411
|
+
**kwargs: Any,
|
|
412
|
+
) -> "ObservabilityChatPromptTemplate":
|
|
413
|
+
"""Create a chat prompt template from an observability platform."""
|
|
414
|
+
return cls(
|
|
415
|
+
prompt_name=prompt_name,
|
|
416
|
+
prompt_version=prompt_version,
|
|
417
|
+
prompt_label=prompt_label,
|
|
418
|
+
load_at_runtime=load_at_runtime,
|
|
419
|
+
observability_platform=observability_platform,
|
|
420
|
+
**kwargs,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
@classmethod
|
|
424
|
+
def from_observability_backend(
|
|
425
|
+
cls,
|
|
426
|
+
prompt_name: str,
|
|
427
|
+
observability_backend: Union[ObservabilityBackend, str],
|
|
428
|
+
*,
|
|
429
|
+
prompt_version: Optional[int] = None,
|
|
430
|
+
prompt_label: Optional[str] = None,
|
|
431
|
+
load_at_runtime: bool = True,
|
|
432
|
+
**kwargs: Any,
|
|
433
|
+
) -> "ObservabilityChatPromptTemplate":
|
|
434
|
+
"""Create a chat prompt template from an observability backend."""
|
|
435
|
+
backend = (
|
|
436
|
+
ObservabilityBackend(observability_backend)
|
|
437
|
+
if isinstance(observability_backend, str)
|
|
438
|
+
else observability_backend
|
|
439
|
+
)
|
|
440
|
+
platform = ObservabilityFactory.create(backend)
|
|
441
|
+
|
|
442
|
+
return cls(
|
|
443
|
+
prompt_name=prompt_name,
|
|
444
|
+
prompt_version=prompt_version,
|
|
445
|
+
prompt_label=prompt_label,
|
|
446
|
+
load_at_runtime=load_at_runtime,
|
|
447
|
+
observability_platform=platform,
|
|
448
|
+
observability_backend=backend,
|
|
449
|
+
**kwargs,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def __add__(self, other: Any) -> ChatPromptTemplate:
|
|
453
|
+
"""Combine two prompt templates."""
|
|
454
|
+
if isinstance(other, ChatPromptTemplate):
|
|
455
|
+
# Create a copy of messages from both templates
|
|
456
|
+
combined_messages = list(self.messages)
|
|
457
|
+
|
|
458
|
+
# Process messages from the other template
|
|
459
|
+
other_messages = []
|
|
460
|
+
for msg in other.messages:
|
|
461
|
+
# Special handling for MessagesPlaceholder
|
|
462
|
+
if isinstance(msg, MessagesPlaceholder):
|
|
463
|
+
other_messages.append(msg)
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
if isinstance(msg, BaseMessagePromptTemplate):
|
|
467
|
+
other_messages.append(msg)
|
|
468
|
+
elif isinstance(msg, BaseMessage):
|
|
469
|
+
content = msg.content
|
|
470
|
+
if isinstance(content, str):
|
|
471
|
+
template_vars = get_template_variables(content, self.template_format)
|
|
472
|
+
if template_vars:
|
|
473
|
+
template_class = {
|
|
474
|
+
MessageRole.SYSTEM: SystemMessagePromptTemplate,
|
|
475
|
+
MessageRole.HUMAN: HumanMessagePromptTemplate,
|
|
476
|
+
MessageRole.AI: AIMessagePromptTemplate,
|
|
477
|
+
MessageRole.ASSISTANT: AIMessagePromptTemplate,
|
|
478
|
+
}.get(MessageRole(msg.type))
|
|
479
|
+
|
|
480
|
+
if template_class:
|
|
481
|
+
other_messages.append(
|
|
482
|
+
template_class.from_template(content, template_format=self.template_format)
|
|
483
|
+
)
|
|
484
|
+
continue
|
|
485
|
+
|
|
486
|
+
other_messages.append(msg)
|
|
487
|
+
else:
|
|
488
|
+
other_messages.append(msg)
|
|
489
|
+
|
|
490
|
+
combined_messages.extend(other_messages)
|
|
491
|
+
|
|
492
|
+
# Collect all input variables
|
|
493
|
+
all_vars = set(self.input_variables or [])
|
|
494
|
+
other_vars = set(other.input_variables or [])
|
|
495
|
+
all_vars.update(other_vars)
|
|
496
|
+
|
|
497
|
+
# Get variables from MessagesPlaceholder
|
|
498
|
+
for msg in combined_messages:
|
|
499
|
+
if isinstance(msg, MessagesPlaceholder):
|
|
500
|
+
all_vars.add(msg.variable_name)
|
|
501
|
+
elif hasattr(msg, "input_variables"):
|
|
502
|
+
all_vars.update(msg.input_variables)
|
|
503
|
+
|
|
504
|
+
# Create new partial variables dict
|
|
505
|
+
combined_partial_vars = dict(self.partial_variables or {})
|
|
506
|
+
if hasattr(other, "partial_variables") and other.partial_variables:
|
|
507
|
+
for k, v in other.partial_variables.items():
|
|
508
|
+
if k not in combined_partial_vars:
|
|
509
|
+
combined_partial_vars[k] = v
|
|
510
|
+
|
|
511
|
+
# Create the combined template
|
|
512
|
+
return ChatPromptTemplate(
|
|
513
|
+
messages=combined_messages,
|
|
514
|
+
input_variables=list(all_vars),
|
|
515
|
+
partial_variables=combined_partial_vars,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
elif isinstance(other, (BaseMessagePromptTemplate, BaseMessage)):
|
|
519
|
+
return self + ChatPromptTemplate.from_messages([other])
|
|
520
|
+
elif isinstance(other, (list, tuple)):
|
|
521
|
+
return self + ChatPromptTemplate.from_messages(other)
|
|
522
|
+
elif isinstance(other, str):
|
|
523
|
+
return self + ChatPromptTemplate.from_template(other)
|
|
524
|
+
else:
|
|
525
|
+
raise NotImplementedError(f"Unsupported operand type for +: {type(other)}")
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
__all__ = ["ObservabilityChatPromptTemplate"]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
from dotenv import find_dotenv
|
|
6
|
+
from pydantic import (
|
|
7
|
+
BeforeValidator,
|
|
8
|
+
Field,
|
|
9
|
+
SecretStr,
|
|
10
|
+
computed_field,
|
|
11
|
+
)
|
|
12
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
|
+
|
|
14
|
+
from langgraph_agent_toolkit.core.memory.types import MemoryBackends
|
|
15
|
+
from langgraph_agent_toolkit.core.observability.types import ObservabilityBackend
|
|
16
|
+
from langgraph_agent_toolkit.helper.logging import logger
|
|
17
|
+
from langgraph_agent_toolkit.helper.utils import check_str_is_http
|
|
18
|
+
from langgraph_agent_toolkit.schema.models import (
|
|
19
|
+
AllModelEnum,
|
|
20
|
+
FakeModelName,
|
|
21
|
+
OpenAICompatibleName,
|
|
22
|
+
Provider,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Settings(BaseSettings):
|
|
27
|
+
model_config = SettingsConfigDict(
|
|
28
|
+
env_file=find_dotenv(),
|
|
29
|
+
env_file_encoding="utf-8",
|
|
30
|
+
env_ignore_empty=True,
|
|
31
|
+
extra="ignore",
|
|
32
|
+
validate_default=False,
|
|
33
|
+
)
|
|
34
|
+
ENV_MODE: str | None = None
|
|
35
|
+
|
|
36
|
+
HOST: str = "0.0.0.0"
|
|
37
|
+
PORT: int = 8080
|
|
38
|
+
|
|
39
|
+
AUTH_SECRET: SecretStr | None = None
|
|
40
|
+
USE_FAKE_MODEL: bool = False
|
|
41
|
+
|
|
42
|
+
# If DEFAULT_MODEL is None, it will be set in model_post_init
|
|
43
|
+
DEFAULT_MODEL: AllModelEnum | None = None
|
|
44
|
+
AVAILABLE_MODELS: set[AllModelEnum] = Field(
|
|
45
|
+
...,
|
|
46
|
+
description="Set of available models. If not set, all models will be available.",
|
|
47
|
+
default_factory=set,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Set openai compatible api, mainly used for proof of concept
|
|
51
|
+
COMPATIBLE_MODEL: str | None = None
|
|
52
|
+
COMPATIBLE_API_KEY: SecretStr | None = None
|
|
53
|
+
COMPATIBLE_BASE_URL: str | None = None
|
|
54
|
+
|
|
55
|
+
# Observability platform
|
|
56
|
+
OBSERVABILITY_BACKEND: ObservabilityBackend | None = None
|
|
57
|
+
|
|
58
|
+
# Agent configuration
|
|
59
|
+
AGENT_PATHS: list[str] = [
|
|
60
|
+
"langgraph_agent_toolkit.agents.blueprints.react.agent:react_agent",
|
|
61
|
+
"langgraph_agent_toolkit.agents.blueprints.chatbot.agent:chatbot_agent",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
LANGCHAIN_TRACING_V2: bool = False
|
|
65
|
+
LANGCHAIN_PROJECT: str = "default"
|
|
66
|
+
LANGCHAIN_ENDPOINT: Annotated[str, BeforeValidator(check_str_is_http)] = "https://api.smith.langchain.com"
|
|
67
|
+
LANGCHAIN_API_KEY: SecretStr | None = None
|
|
68
|
+
|
|
69
|
+
LANGFUSE_SECRET_KEY: SecretStr | None = None
|
|
70
|
+
LANGFUSE_PUBLIC_KEY: SecretStr | None = None
|
|
71
|
+
LANGFUSE_HOST: Annotated[str, BeforeValidator(check_str_is_http)] = "http://localhost:3000"
|
|
72
|
+
|
|
73
|
+
# Database Configuration
|
|
74
|
+
MEMORY_BACKEND: MemoryBackends = MemoryBackends.SQLITE
|
|
75
|
+
SQLITE_DB_PATH: str = "checkpoints.db"
|
|
76
|
+
|
|
77
|
+
# postgresql Configuration
|
|
78
|
+
POSTGRES_USER: str | None = None
|
|
79
|
+
POSTGRES_PASSWORD: SecretStr | None = None
|
|
80
|
+
POSTGRES_HOST: str | None = None
|
|
81
|
+
POSTGRES_PORT: int | None = None
|
|
82
|
+
POSTGRES_DB: str | None = None
|
|
83
|
+
POSTGRES_POOL_SIZE: int = Field(default=10, description="Maximum number of connections in the pool")
|
|
84
|
+
POSTGRES_MIN_SIZE: int = Field(default=3, description="Minimum number of connections in the pool")
|
|
85
|
+
POSTGRES_MAX_IDLE: int = Field(default=5, description="Maximum number of idle connections")
|
|
86
|
+
|
|
87
|
+
def model_post_init(self, __context: Any) -> None:
|
|
88
|
+
# Check for LANGGRAPH_ prefixed environment variables that might override settings
|
|
89
|
+
self._apply_langgraph_env_overrides()
|
|
90
|
+
|
|
91
|
+
api_keys = {
|
|
92
|
+
Provider.OPENAI_COMPATIBLE: self.COMPATIBLE_BASE_URL and self.COMPATIBLE_MODEL,
|
|
93
|
+
Provider.FAKE: self.USE_FAKE_MODEL,
|
|
94
|
+
}
|
|
95
|
+
active_keys = [k for k, v in api_keys.items() if v]
|
|
96
|
+
if not active_keys:
|
|
97
|
+
raise ValueError("At least one LLM API key must be provided.")
|
|
98
|
+
|
|
99
|
+
for provider in active_keys:
|
|
100
|
+
match provider:
|
|
101
|
+
case Provider.OPENAI_COMPATIBLE:
|
|
102
|
+
if self.DEFAULT_MODEL is None:
|
|
103
|
+
self.DEFAULT_MODEL = OpenAICompatibleName.OPENAI_COMPATIBLE
|
|
104
|
+
self.AVAILABLE_MODELS.update(set(OpenAICompatibleName))
|
|
105
|
+
case Provider.FAKE:
|
|
106
|
+
if self.DEFAULT_MODEL is None:
|
|
107
|
+
self.DEFAULT_MODEL = FakeModelName.FAKE
|
|
108
|
+
self.AVAILABLE_MODELS.update(set(FakeModelName))
|
|
109
|
+
case _:
|
|
110
|
+
raise ValueError(f"Unknown provider: {provider}")
|
|
111
|
+
|
|
112
|
+
def _apply_langgraph_env_overrides(self) -> None:
|
|
113
|
+
"""Apply any LANGGRAPH_ prefixed environment variables to override settings."""
|
|
114
|
+
for env_name, env_value in os.environ.items():
|
|
115
|
+
if env_name.startswith("LANGGRAPH_"):
|
|
116
|
+
setting_name = env_name[10:] # Remove the "LANGGRAPH_" prefix
|
|
117
|
+
if hasattr(self, setting_name):
|
|
118
|
+
try:
|
|
119
|
+
current_value = getattr(self, setting_name)
|
|
120
|
+
|
|
121
|
+
# Handle different types
|
|
122
|
+
if isinstance(current_value, list):
|
|
123
|
+
# Parse JSON array
|
|
124
|
+
try:
|
|
125
|
+
parsed_value = json.loads(env_value)
|
|
126
|
+
if isinstance(parsed_value, list):
|
|
127
|
+
setattr(self, setting_name, parsed_value)
|
|
128
|
+
logger.info(f"Applied environment override for {setting_name}")
|
|
129
|
+
except json.JSONDecodeError:
|
|
130
|
+
logger.warning(f"Failed to parse JSON for {setting_name}: {env_value}")
|
|
131
|
+
elif isinstance(current_value, bool):
|
|
132
|
+
# Convert string to boolean
|
|
133
|
+
if env_value.lower() in ("true", "1", "yes"):
|
|
134
|
+
setattr(self, setting_name, True)
|
|
135
|
+
logger.info(f"Applied environment override for {setting_name}")
|
|
136
|
+
elif env_value.lower() in ("false", "0", "no"):
|
|
137
|
+
setattr(self, setting_name, False)
|
|
138
|
+
logger.info(f"Applied environment override for {setting_name}")
|
|
139
|
+
elif current_value is None or isinstance(current_value, (str, int, float)):
|
|
140
|
+
# Convert to the appropriate type
|
|
141
|
+
if isinstance(current_value, int) or current_value is None and env_value.isdigit():
|
|
142
|
+
setattr(self, setting_name, int(env_value))
|
|
143
|
+
elif isinstance(current_value, float) or current_value is None and "." in env_value:
|
|
144
|
+
try:
|
|
145
|
+
setattr(self, setting_name, float(env_value))
|
|
146
|
+
except ValueError:
|
|
147
|
+
setattr(self, setting_name, env_value)
|
|
148
|
+
else:
|
|
149
|
+
setattr(self, setting_name, env_value)
|
|
150
|
+
logger.info(f"Applied environment override for {setting_name}")
|
|
151
|
+
# Add more type handling as needed
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.warning(f"Failed to apply environment override for {setting_name}: {e}")
|
|
154
|
+
|
|
155
|
+
@computed_field
|
|
156
|
+
@property
|
|
157
|
+
def BASE_URL(self) -> str:
|
|
158
|
+
return f"http://{self.HOST}:{self.PORT}"
|
|
159
|
+
|
|
160
|
+
def is_dev(self) -> bool:
|
|
161
|
+
return self.ENV_MODE == "development"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
settings = Settings()
|