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,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)
|