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.
Files changed (74) hide show
  1. langgraph_agent_toolkit/__init__.py +7 -0
  2. langgraph_agent_toolkit/agents/__init__.py +0 -0
  3. langgraph_agent_toolkit/agents/agent.py +14 -0
  4. langgraph_agent_toolkit/agents/agent_executor.py +415 -0
  5. langgraph_agent_toolkit/agents/blueprints/__init__.py +0 -0
  6. langgraph_agent_toolkit/agents/blueprints/bg_task_agent/__init__.py +0 -0
  7. langgraph_agent_toolkit/agents/blueprints/bg_task_agent/agent.py +69 -0
  8. langgraph_agent_toolkit/agents/blueprints/bg_task_agent/task.py +52 -0
  9. langgraph_agent_toolkit/agents/blueprints/bg_task_agent/utils.py +17 -0
  10. langgraph_agent_toolkit/agents/blueprints/chatbot/__init__.py +0 -0
  11. langgraph_agent_toolkit/agents/blueprints/chatbot/agent.py +34 -0
  12. langgraph_agent_toolkit/agents/blueprints/command_agent/__init__.py +0 -0
  13. langgraph_agent_toolkit/agents/blueprints/command_agent/agent.py +54 -0
  14. langgraph_agent_toolkit/agents/blueprints/interrupt_agent/__init__.py +0 -0
  15. langgraph_agent_toolkit/agents/blueprints/interrupt_agent/agent.py +140 -0
  16. langgraph_agent_toolkit/agents/blueprints/react/__init__.py +0 -0
  17. langgraph_agent_toolkit/agents/blueprints/react/agent.py +67 -0
  18. langgraph_agent_toolkit/agents/blueprints/react_so/__init__.py +0 -0
  19. langgraph_agent_toolkit/agents/blueprints/react_so/agent.py +39 -0
  20. langgraph_agent_toolkit/agents/blueprints/supervisor_agent/__init__.py +0 -0
  21. langgraph_agent_toolkit/agents/blueprints/supervisor_agent/agent.py +44 -0
  22. langgraph_agent_toolkit/agents/components/__init__.py +0 -0
  23. langgraph_agent_toolkit/agents/components/creators/__init__.py +4 -0
  24. langgraph_agent_toolkit/agents/components/creators/create_react_agent.py +459 -0
  25. langgraph_agent_toolkit/agents/components/tools.py +13 -0
  26. langgraph_agent_toolkit/agents/components/utils.py +42 -0
  27. langgraph_agent_toolkit/client/__init__.py +4 -0
  28. langgraph_agent_toolkit/client/client.py +344 -0
  29. langgraph_agent_toolkit/core/__init__.py +5 -0
  30. langgraph_agent_toolkit/core/memory/__init__.py +0 -0
  31. langgraph_agent_toolkit/core/memory/base.py +33 -0
  32. langgraph_agent_toolkit/core/memory/factory.py +30 -0
  33. langgraph_agent_toolkit/core/memory/postgres.py +76 -0
  34. langgraph_agent_toolkit/core/memory/sqlite.py +21 -0
  35. langgraph_agent_toolkit/core/memory/types.py +6 -0
  36. langgraph_agent_toolkit/core/models/__init__.py +5 -0
  37. langgraph_agent_toolkit/core/models/chat_openai.py +21 -0
  38. langgraph_agent_toolkit/core/models/factory.py +118 -0
  39. langgraph_agent_toolkit/core/models/fake.py +25 -0
  40. langgraph_agent_toolkit/core/observability/__init__.py +10 -0
  41. langgraph_agent_toolkit/core/observability/base.py +331 -0
  42. langgraph_agent_toolkit/core/observability/empty.py +67 -0
  43. langgraph_agent_toolkit/core/observability/factory.py +43 -0
  44. langgraph_agent_toolkit/core/observability/langfuse.py +118 -0
  45. langgraph_agent_toolkit/core/observability/langsmith.py +131 -0
  46. langgraph_agent_toolkit/core/observability/types.py +34 -0
  47. langgraph_agent_toolkit/core/prompts/__init__.py +0 -0
  48. langgraph_agent_toolkit/core/prompts/chat_prompt_template.py +528 -0
  49. langgraph_agent_toolkit/core/settings.py +164 -0
  50. langgraph_agent_toolkit/helper/__init__.py +0 -0
  51. langgraph_agent_toolkit/helper/constants.py +10 -0
  52. langgraph_agent_toolkit/helper/logging.py +111 -0
  53. langgraph_agent_toolkit/helper/types.py +7 -0
  54. langgraph_agent_toolkit/helper/utils.py +80 -0
  55. langgraph_agent_toolkit/run_agent.py +68 -0
  56. langgraph_agent_toolkit/run_client.py +55 -0
  57. langgraph_agent_toolkit/run_service.py +19 -0
  58. langgraph_agent_toolkit/schema/__init__.py +28 -0
  59. langgraph_agent_toolkit/schema/models.py +25 -0
  60. langgraph_agent_toolkit/schema/schema.py +210 -0
  61. langgraph_agent_toolkit/schema/task_data.py +72 -0
  62. langgraph_agent_toolkit/service/__init__.py +0 -0
  63. langgraph_agent_toolkit/service/exception_handlers.py +46 -0
  64. langgraph_agent_toolkit/service/factory.py +213 -0
  65. langgraph_agent_toolkit/service/handler.py +122 -0
  66. langgraph_agent_toolkit/service/middleware.py +18 -0
  67. langgraph_agent_toolkit/service/routes.py +169 -0
  68. langgraph_agent_toolkit/service/types.py +8 -0
  69. langgraph_agent_toolkit/service/utils.py +136 -0
  70. langgraph_agent_toolkit/streamlit_app.py +368 -0
  71. langgraph_agent_toolkit-0.1.0.dist-info/METADATA +424 -0
  72. langgraph_agent_toolkit-0.1.0.dist-info/RECORD +74 -0
  73. langgraph_agent_toolkit-0.1.0.dist-info/WHEEL +4 -0
  74. 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()