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,25 @@
1
+ from typing import Any, Callable, Dict, Literal, Optional, Sequence, Union
2
+
3
+ from langchain_community.chat_models import FakeListChatModel
4
+ from langchain_core.language_models.base import LanguageModelInput
5
+ from langchain_core.messages import BaseMessage
6
+ from langchain_core.runnables import Runnable
7
+ from langchain_core.tools import BaseTool
8
+
9
+
10
+ class FakeToolModel(FakeListChatModel):
11
+ """A fake model that returns a fixed response for testing purposes."""
12
+
13
+ def __init__(self, responses: list[str]):
14
+ super().__init__(responses=responses)
15
+
16
+ def bind_tools(
17
+ self,
18
+ tools: Sequence[
19
+ Union[Dict[str, Any], type, Callable, BaseTool] # noqa: UP006
20
+ ],
21
+ *,
22
+ tool_choice: Optional[Union[str, Literal["any"]]] = None,
23
+ **kwargs: Any,
24
+ ) -> Runnable[LanguageModelInput, BaseMessage]:
25
+ return self
@@ -0,0 +1,10 @@
1
+ from langgraph_agent_toolkit.core.observability.base import BaseObservabilityPlatform
2
+ from langgraph_agent_toolkit.core.observability.empty import EmptyObservability
3
+ from langgraph_agent_toolkit.core.observability.factory import ObservabilityFactory
4
+ from langgraph_agent_toolkit.core.observability.langfuse import LangfuseObservability
5
+ from langgraph_agent_toolkit.core.observability.langsmith import LangsmithObservability
6
+ from langgraph_agent_toolkit.core.observability.types import ObservabilityBackend
7
+
8
+
9
+ # For backward compatibility
10
+ create_observability_platform = ObservabilityFactory.create
@@ -0,0 +1,331 @@
1
+ import functools
2
+ import os
3
+ import tempfile
4
+ from abc import ABC, abstractmethod
5
+ from pathlib import Path
6
+ from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, TypeVar, cast
7
+
8
+ import joblib
9
+ from jinja2 import Template
10
+ from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
11
+ from langchain_core.prompts import ChatPromptTemplate
12
+ from langchain_core.prompts.chat import (
13
+ AIMessagePromptTemplate,
14
+ BaseMessage,
15
+ HumanMessagePromptTemplate,
16
+ MessagesPlaceholder,
17
+ SystemMessagePromptTemplate,
18
+ )
19
+
20
+ from langgraph_agent_toolkit.core.observability.types import MessageRole, PromptReturnType, PromptTemplateType
21
+ from langgraph_agent_toolkit.helper.logging import logger
22
+
23
+
24
+ T = TypeVar("T")
25
+
26
+
27
+ class BaseObservabilityPlatform(ABC):
28
+ """Base class for observability platforms."""
29
+
30
+ __default_required_vars = []
31
+
32
+ def __init__(self, prompts_dir: Optional[str] = None):
33
+ self._required_vars = self.__default_required_vars.copy()
34
+
35
+ if prompts_dir:
36
+ self._prompts_dir = Path(prompts_dir)
37
+ else:
38
+ temp_base = Path(tempfile.gettempdir())
39
+ self._prompts_dir = temp_base / "langgraph_prompts"
40
+
41
+ self._prompts_dir.mkdir(exist_ok=True, parents=True)
42
+
43
+ @property
44
+ def prompts_dir(self) -> Path:
45
+ return self._prompts_dir
46
+
47
+ @prompts_dir.setter
48
+ def prompts_dir(self, path: str) -> None:
49
+ self._prompts_dir = Path(path)
50
+ self._prompts_dir.mkdir(exist_ok=True, parents=True)
51
+
52
+ @property
53
+ def required_vars(self) -> List[str]:
54
+ return self._required_vars
55
+
56
+ @required_vars.setter
57
+ def required_vars(self, value: List[str]) -> None:
58
+ self._required_vars = value
59
+
60
+ def validate_environment(self) -> bool:
61
+ missing_vars = [var for var in self._required_vars if not os.environ.get(var)]
62
+
63
+ if missing_vars:
64
+ raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")
65
+
66
+ return True
67
+
68
+ @staticmethod
69
+ def requires_env_vars(func: Callable[..., T]) -> Callable[..., T]:
70
+ @functools.wraps(func)
71
+ def wrapper(self, *args, **kwargs):
72
+ self.validate_environment()
73
+ return func(self, *args, **kwargs)
74
+
75
+ return wrapper
76
+
77
+ @abstractmethod
78
+ def get_callback_handler(self, **kwargs) -> Any:
79
+ pass
80
+
81
+ @abstractmethod
82
+ def before_shutdown(self) -> None:
83
+ pass
84
+
85
+ @abstractmethod
86
+ def record_feedback(self, run_id: str, key: str, score: float, **kwargs) -> None:
87
+ pass
88
+
89
+ def _handle_existing_prompt(
90
+ self,
91
+ name: str,
92
+ create_new_version: bool = True,
93
+ client: Any = None,
94
+ client_pull_method: Optional[str] = None,
95
+ client_delete_method: Optional[str] = None,
96
+ ) -> Tuple[Any, Any]:
97
+ existing_prompt = None
98
+ url = None
99
+
100
+ if not client or not client_pull_method or not client_delete_method:
101
+ return (existing_prompt, url)
102
+
103
+ pull_method = getattr(client, client_pull_method, None)
104
+ delete_method = getattr(client, client_delete_method, None)
105
+
106
+ if not pull_method or not delete_method:
107
+ return (existing_prompt, url)
108
+
109
+ if not create_new_version:
110
+ try:
111
+ existing_prompt = pull_method(name=name)
112
+ url = getattr(existing_prompt, "url", None)
113
+ logger.debug(f"Using existing prompt '{name}' as create_new_version is False")
114
+ except Exception:
115
+ logger.debug(f"Existing prompt '{name}' not found, will create a new one")
116
+ else:
117
+ try:
118
+ pull_method(name=name)
119
+ delete_method(name=name)
120
+ logger.debug(f"Deleted existing prompt '{name}' to create new version")
121
+ except Exception:
122
+ pass
123
+
124
+ return (existing_prompt, url)
125
+
126
+ def _convert_to_chat_prompt(self, prompt_template: PromptTemplateType) -> ChatPromptTemplate:
127
+ if isinstance(prompt_template, str):
128
+ return ChatPromptTemplate.from_template(prompt_template)
129
+ elif isinstance(prompt_template, list) and all(isinstance(msg, dict) for msg in prompt_template):
130
+ messages = []
131
+ for msg in prompt_template:
132
+ role = msg.get("role", "")
133
+ content = msg.get("content", "")
134
+
135
+ match role.lower():
136
+ case MessageRole.SYSTEM:
137
+ messages.append(SystemMessage(content=content))
138
+ case MessageRole.HUMAN | MessageRole.USER:
139
+ messages.append(HumanMessage(content=content))
140
+ case MessageRole.AI | MessageRole.ASSISTANT:
141
+ messages.append(AIMessage(content=content))
142
+ case MessageRole.PLACEHOLDER | MessageRole.MESSAGES_PLACEHOLDER:
143
+ messages.append(MessagesPlaceholder(variable_name=content))
144
+ case _:
145
+ raise ValueError(f"Unknown message role: {role}")
146
+
147
+ return ChatPromptTemplate.from_messages(messages)
148
+ else:
149
+ return cast(ChatPromptTemplate, prompt_template)
150
+
151
+ def _process_messages_from_prompt(
152
+ self, messages: List[Any], template_format: Literal["f-string", "mustache", "jinja2"] = "f-string"
153
+ ) -> List[Any]:
154
+ MESSAGE_TYPE_MAP = {
155
+ MessageRole.SYSTEM: SystemMessagePromptTemplate,
156
+ MessageRole.HUMAN: HumanMessagePromptTemplate,
157
+ MessageRole.USER: HumanMessagePromptTemplate,
158
+ MessageRole.AI: AIMessagePromptTemplate,
159
+ MessageRole.ASSISTANT: AIMessagePromptTemplate,
160
+ }
161
+
162
+ processed_messages = []
163
+
164
+ for msg in messages:
165
+ if isinstance(msg, MessagesPlaceholder):
166
+ processed_messages.append(msg)
167
+ continue
168
+
169
+ if isinstance(msg, BaseMessage):
170
+ msg_type = MessageRole(msg.type)
171
+ if msg_type in MESSAGE_TYPE_MAP:
172
+ template_class = MESSAGE_TYPE_MAP[msg_type]
173
+ processed_messages.append(
174
+ template_class.from_template(msg.content, template_format=template_format)
175
+ )
176
+ continue
177
+
178
+ if isinstance(msg, dict) and "role" in msg and "content" in msg:
179
+ role, content = msg["role"], msg["content"]
180
+
181
+ if role in MESSAGE_TYPE_MAP:
182
+ template_class = MESSAGE_TYPE_MAP[role]
183
+ processed_messages.append(template_class.from_template(content, template_format=template_format))
184
+ continue
185
+
186
+ if role.lower() in (MessageRole.PLACEHOLDER, MessageRole.MESSAGES_PLACEHOLDER):
187
+ processed_messages.append(MessagesPlaceholder(variable_name=content))
188
+ continue
189
+
190
+ if isinstance(msg, tuple) and len(msg) == 2:
191
+ role, content = msg
192
+ if role in MESSAGE_TYPE_MAP:
193
+ template_class = MESSAGE_TYPE_MAP[role]
194
+ processed_messages.append(template_class.from_template(content, template_format=template_format))
195
+ continue
196
+
197
+ if role.lower() in (MessageRole.PLACEHOLDER, MessageRole.MESSAGES_PLACEHOLDER):
198
+ processed_messages.append(MessagesPlaceholder(variable_name=content))
199
+ continue
200
+
201
+ processed_messages.append(msg)
202
+
203
+ return processed_messages
204
+
205
+ def _process_prompt_object(
206
+ self, prompt_obj: Any, template_format: Literal["f-string", "mustache", "jinja2"] = "f-string"
207
+ ) -> ChatPromptTemplate:
208
+ if isinstance(prompt_obj, ChatPromptTemplate):
209
+ return prompt_obj
210
+
211
+ if hasattr(prompt_obj, "messages") and isinstance(prompt_obj.messages, list):
212
+ processed_messages = self._process_messages_from_prompt(
213
+ prompt_obj.messages, template_format=template_format
214
+ )
215
+ if processed_messages:
216
+ return ChatPromptTemplate.from_messages(processed_messages)
217
+
218
+ elif isinstance(prompt_obj, list):
219
+ if all(isinstance(item, dict) and "role" in item and "content" in item for item in prompt_obj):
220
+ processed_messages = self._process_messages_from_prompt(prompt_obj, template_format=template_format)
221
+ if processed_messages:
222
+ return ChatPromptTemplate.from_messages(processed_messages)
223
+
224
+ elif isinstance(prompt_obj, str):
225
+ return ChatPromptTemplate.from_template(prompt_obj, template_format=template_format)
226
+
227
+ else:
228
+ raise ValueError(f"Could not process prompt object of type {type(prompt_obj)}")
229
+
230
+ def _extract_template_string(self, prompt_template: PromptTemplateType, prompt_obj: Any) -> str:
231
+ if isinstance(prompt_template, str):
232
+ return prompt_template
233
+ elif isinstance(prompt_template, list) and all(isinstance(msg, dict) for msg in prompt_template):
234
+ template_str = ""
235
+ for msg in prompt_template:
236
+ template_str += f"[{msg['role']}]: {msg['content']}\n\n"
237
+ return template_str
238
+ else:
239
+ if hasattr(prompt_obj, "template"):
240
+ return prompt_obj.template
241
+ return str(prompt_obj)
242
+
243
+ def _local_pull_prompt(self, name: str, template_format: str = "f-string", **kwargs) -> PromptReturnType:
244
+ """Local implementation of pull_prompt that reads from the file system."""
245
+ file_path = self._prompts_dir / f"{name}.jinja2"
246
+
247
+ if not file_path.exists():
248
+ raise ValueError(f"Prompt '{name}' not found at {file_path}")
249
+
250
+ with open(file_path, "r", encoding="utf-8") as f:
251
+ template_content = f.read()
252
+
253
+ metadata_path = self._prompts_dir / f"{name}.metadata.joblib"
254
+
255
+ if metadata_path.exists():
256
+ try:
257
+ metadata = joblib.load(metadata_path)
258
+ original_prompt = metadata.get("original_prompt")
259
+ if original_prompt:
260
+ return original_prompt
261
+ except Exception:
262
+ pass
263
+
264
+ return ChatPromptTemplate.from_template(template_content, template_format=template_format)
265
+
266
+ def pull_prompt(
267
+ self, name: str, template_format: Literal["f-string", "mustache", "jinja2"] = "f-string", **kwargs
268
+ ) -> PromptReturnType:
269
+ """Pull a prompt from the observability platform."""
270
+ # Use the local implementation
271
+ return self._local_pull_prompt(name, template_format=template_format, **kwargs)
272
+
273
+ def push_prompt(
274
+ self,
275
+ name: str,
276
+ prompt_template: PromptTemplateType,
277
+ metadata: Optional[Dict[str, Any]] = None,
278
+ create_new_version: bool = True,
279
+ ) -> None:
280
+ self._prompts_dir.mkdir(exist_ok=True, parents=True)
281
+
282
+ file_path = self._prompts_dir / f"{name}.jinja2"
283
+ metadata_path = self._prompts_dir / f"{name}.metadata.joblib"
284
+
285
+ if create_new_version:
286
+ if file_path.exists():
287
+ file_path.unlink()
288
+ if metadata_path.exists():
289
+ metadata_path.unlink()
290
+
291
+ chat_prompt = self._convert_to_chat_prompt(prompt_template)
292
+ template_str = self._extract_template_string(prompt_template, chat_prompt)
293
+
294
+ with open(file_path, "w", encoding="utf-8") as f:
295
+ f.write(str(template_str))
296
+
297
+ full_metadata = metadata.copy() if metadata else {}
298
+ if not isinstance(prompt_template, str):
299
+ full_metadata["original_prompt"] = chat_prompt
300
+ full_metadata["original_format"] = "chat_message_dict" if isinstance(prompt_template, list) else "other"
301
+ joblib.dump(full_metadata, metadata_path)
302
+ elif metadata:
303
+ joblib.dump(full_metadata, metadata_path)
304
+
305
+ def get_template(self, name: str) -> str:
306
+ file_path = self._prompts_dir / f"{name}.jinja2"
307
+
308
+ if not file_path.exists():
309
+ raise ValueError(f"Prompt '{name}' not found at {file_path}")
310
+
311
+ with open(file_path, "r", encoding="utf-8") as f:
312
+ return f.read()
313
+
314
+ def render_prompt(self, prompt_name: str, **variables) -> str:
315
+ template_content = self.get_template(prompt_name)
316
+ template = Template(template_content)
317
+ return template.render(**variables)
318
+
319
+ def delete_prompt(self, name: str) -> None:
320
+ file_path = self._prompts_dir / f"{name}.jinja2"
321
+ metadata_path = self._prompts_dir / f"{name}.metadata.joblib"
322
+ json_metadata_path = self._prompts_dir / f"{name}.metadata.json"
323
+
324
+ if file_path.exists():
325
+ file_path.unlink()
326
+
327
+ if metadata_path.exists():
328
+ metadata_path.unlink()
329
+
330
+ if json_metadata_path.exists():
331
+ json_metadata_path.unlink()
@@ -0,0 +1,67 @@
1
+ from typing import Any, Dict, Literal, Optional
2
+
3
+ from langgraph_agent_toolkit.core.observability.base import BaseObservabilityPlatform
4
+ from langgraph_agent_toolkit.core.observability.types import PromptReturnType, PromptTemplateType
5
+
6
+
7
+ class EmptyObservability(BaseObservabilityPlatform):
8
+ """Empty implementation of observability platform."""
9
+
10
+ __default_required_vars = []
11
+
12
+ def __init__(self, prompts_dir: Optional[str] = None):
13
+ """Initialize EmptyObservability.
14
+
15
+ Args:
16
+ prompts_dir: Optional directory to store prompts locally. If None, a system temp directory is used.
17
+
18
+ """
19
+ super().__init__(prompts_dir)
20
+
21
+ def get_callback_handler(self, **kwargs) -> None:
22
+ """Get the callback handler for the observability platform."""
23
+ return None
24
+
25
+ def before_shutdown(self) -> None:
26
+ """Perform any necessary cleanup before shutdown."""
27
+ pass
28
+
29
+ def record_feedback(self, run_id: str, key: str, score: float, **kwargs) -> None:
30
+ """Record feedback for a run with Empty observability platform."""
31
+ raise ValueError("Cannot record feedback: No observability platform is configured.")
32
+
33
+ def push_prompt(
34
+ self,
35
+ name: str,
36
+ prompt_template: PromptTemplateType,
37
+ metadata: Optional[Dict[str, Any]] = None,
38
+ create_new_version: bool = True,
39
+ ) -> None:
40
+ """Push a prompt using local storage.
41
+
42
+ Args:
43
+ name: Name of the prompt
44
+ prompt_template: String template, list of message dicts, or prompt object
45
+ metadata: Additional metadata for the prompt
46
+ create_new_version: If True, overwrite existing prompt with new version
47
+
48
+ """
49
+ super().push_prompt(name, prompt_template, metadata, create_new_version)
50
+
51
+ def pull_prompt(
52
+ self,
53
+ name: str,
54
+ template_format: Literal["f-string", "mustache", "jinja2"] = "f-string",
55
+ **kwargs,
56
+ ) -> PromptReturnType:
57
+ """Pull a prompt from local storage."""
58
+ return super().pull_prompt(name, template_format=template_format, **kwargs)
59
+
60
+ def delete_prompt(self, name: str) -> None:
61
+ """Delete a prompt from local storage.
62
+
63
+ Args:
64
+ name: Name of the prompt to delete
65
+
66
+ """
67
+ super().delete_prompt(name)
@@ -0,0 +1,43 @@
1
+ from typing import Optional, Union
2
+
3
+ from langgraph_agent_toolkit.core.observability.base import BaseObservabilityPlatform
4
+ from langgraph_agent_toolkit.core.observability.empty import EmptyObservability
5
+ from langgraph_agent_toolkit.core.observability.langfuse import LangfuseObservability
6
+ from langgraph_agent_toolkit.core.observability.langsmith import LangsmithObservability
7
+ from langgraph_agent_toolkit.core.observability.types import ObservabilityBackend
8
+
9
+
10
+ class ObservabilityFactory:
11
+ """Factory for creating observability platform instances."""
12
+
13
+ @staticmethod
14
+ def create(
15
+ platform: Union[ObservabilityBackend, str], prompts_dir: Optional[str] = None
16
+ ) -> BaseObservabilityPlatform:
17
+ """Create and return an observability platform instance.
18
+
19
+ Args:
20
+ platform: The observability platform to create
21
+ prompts_dir: Optional directory to store prompts locally
22
+
23
+ Returns:
24
+ An instance of the requested observability platform
25
+
26
+ Raises:
27
+ ValueError: If the requested platform is not supported
28
+
29
+ """
30
+ platform = ObservabilityBackend(platform)
31
+
32
+ match platform:
33
+ case ObservabilityBackend.LANGFUSE:
34
+ return LangfuseObservability(prompts_dir=prompts_dir)
35
+
36
+ case ObservabilityBackend.LANGSMITH:
37
+ return LangsmithObservability(prompts_dir=prompts_dir)
38
+
39
+ case ObservabilityBackend.EMPTY:
40
+ return EmptyObservability(prompts_dir=prompts_dir)
41
+
42
+ case _:
43
+ raise ValueError(f"Unsupported ObservabilityBackend: {platform}")
@@ -0,0 +1,118 @@
1
+ from typing import Any, Dict, Literal, Optional, Tuple, Union
2
+
3
+ from langfuse import Langfuse
4
+ from langfuse.callback import CallbackHandler
5
+
6
+ from langgraph_agent_toolkit.core.observability.base import BaseObservabilityPlatform
7
+ from langgraph_agent_toolkit.core.observability.types import PromptReturnType, PromptTemplateType
8
+ from langgraph_agent_toolkit.helper.constants import DEFAULT_CACHE_TTL_SECOND
9
+ from langgraph_agent_toolkit.helper.logging import logger
10
+
11
+
12
+ class LangfuseObservability(BaseObservabilityPlatform):
13
+ """Langfuse implementation of observability platform."""
14
+
15
+ def __init__(self, prompts_dir: Optional[str] = None):
16
+ super().__init__(prompts_dir)
17
+ self.required_vars = ["LANGFUSE_SECRET_KEY", "LANGFUSE_PUBLIC_KEY", "LANGFUSE_HOST"]
18
+
19
+ @BaseObservabilityPlatform.requires_env_vars
20
+ def get_callback_handler(self, **kwargs) -> CallbackHandler:
21
+ return CallbackHandler(**kwargs)
22
+
23
+ def before_shutdown(self) -> None:
24
+ Langfuse().flush()
25
+
26
+ @BaseObservabilityPlatform.requires_env_vars
27
+ def record_feedback(self, run_id: str, key: str, score: float, **kwargs) -> None:
28
+ Langfuse().score(
29
+ trace_id=run_id,
30
+ name=key,
31
+ value=score,
32
+ **kwargs,
33
+ )
34
+
35
+ @BaseObservabilityPlatform.requires_env_vars
36
+ def push_prompt(
37
+ self,
38
+ name: str,
39
+ prompt_template: PromptTemplateType,
40
+ metadata: Optional[Dict[str, Any]] = None,
41
+ create_new_version: bool = True,
42
+ ) -> None:
43
+ langfuse = Langfuse()
44
+ labels = metadata.get("labels", ["production"]) if metadata else ["production"]
45
+
46
+ # Handle existing prompt versions - custom implementation for Langfuse
47
+ existing_prompt = None
48
+ if not create_new_version:
49
+ try:
50
+ existing_prompt = langfuse.get_prompt(name=name)
51
+ logger.debug(f"Using existing prompt '{name}' as create_new_version is False")
52
+ except Exception:
53
+ logger.debug(f"Existing prompt '{name}' not found, will create a new one")
54
+
55
+ prompt_obj = self._convert_to_chat_prompt(prompt_template)
56
+ type_prompt = "text" if isinstance(prompt_template, str) else "chat"
57
+
58
+ if existing_prompt:
59
+ langfuse_prompt = existing_prompt
60
+ else:
61
+ langfuse_prompt = langfuse.create_prompt(
62
+ name=name,
63
+ prompt=prompt_template,
64
+ labels=labels,
65
+ type=type_prompt,
66
+ )
67
+
68
+ full_metadata = metadata.copy() if metadata else {}
69
+ full_metadata["langfuse_prompt"] = langfuse_prompt
70
+ full_metadata["original_prompt"] = prompt_obj
71
+
72
+ super().push_prompt(name, prompt_template, full_metadata)
73
+
74
+ @BaseObservabilityPlatform.requires_env_vars
75
+ def pull_prompt(
76
+ self,
77
+ name: str,
78
+ return_with_prompt_object: bool = False,
79
+ cache_ttl_seconds: Optional[int] = DEFAULT_CACHE_TTL_SECOND,
80
+ template_format: Literal["f-string", "mustache", "jinja2"] = "f-string",
81
+ label: Optional[str] = None,
82
+ version: Optional[int] = None,
83
+ **kwargs,
84
+ ) -> Union[PromptReturnType, Tuple[PromptReturnType, Any]]:
85
+ try:
86
+ langfuse = Langfuse()
87
+ get_prompt_kwargs = {"name": name, "cache_ttl_seconds": cache_ttl_seconds}
88
+
89
+ if label:
90
+ get_prompt_kwargs["label"] = label
91
+ elif kwargs.get("prompt_label"):
92
+ get_prompt_kwargs["label"] = kwargs.get("prompt_label")
93
+
94
+ if version is not None:
95
+ get_prompt_kwargs["version"] = version
96
+ elif kwargs.get("prompt_version"):
97
+ get_prompt_kwargs["version"] = kwargs.get("prompt_version")
98
+
99
+ try:
100
+ langfuse_prompt = langfuse.get_prompt(**get_prompt_kwargs)
101
+ except Exception as e:
102
+ logger.debug(f"Prompt not found with parameters: {e}")
103
+ langfuse_prompt = langfuse.get_prompt(name=name, cache_ttl_seconds=cache_ttl_seconds)
104
+
105
+ # Process the prompt object using the base class helper
106
+ prompt = self._process_prompt_object(langfuse_prompt.prompt, template_format=template_format)
107
+
108
+ return (prompt, langfuse_prompt) if return_with_prompt_object else prompt
109
+
110
+ except Exception as e:
111
+ logger.warning(f"Failed to pull prompt from Langfuse: {e}")
112
+ local_prompt = super().pull_prompt(name, template_format=template_format, **kwargs)
113
+ return (local_prompt, None) if return_with_prompt_object else local_prompt
114
+
115
+ @BaseObservabilityPlatform.requires_env_vars
116
+ def delete_prompt(self, name: str) -> None:
117
+ logger.warning(f"Skipping deletion of prompt '{name}' from Langfuse")
118
+ super().delete_prompt(name)