livekit-plugins-difyai 1.0.22__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.
@@ -0,0 +1,25 @@
1
+ from .llm import LLM
2
+ from .version import __version__
3
+
4
+ __all__ = ["LLM", "__version__"]
5
+
6
+ from livekit.agents import Plugin
7
+
8
+ from .log import logger
9
+
10
+
11
+ class DifyPlugin(Plugin):
12
+ def __init__(self):
13
+ super().__init__(__name__, __version__, __package__, logger)
14
+
15
+
16
+ Plugin.register_plugin(DifyPlugin())
17
+
18
+ # Cleanup docs of unexported modules
19
+ _module = dir()
20
+ NOT_IN_ALL = [m for m in _module if m not in __all__]
21
+
22
+ __pdoc__ = {}
23
+
24
+ for n in NOT_IN_ALL:
25
+ __pdoc__[n] = False
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Optional
7
+
8
+ import aiohttp
9
+ from livekit.agents import (
10
+ APIConnectionError,
11
+ APIStatusError,
12
+ llm,
13
+ )
14
+ from livekit.agents.types import (
15
+ DEFAULT_API_CONNECT_OPTIONS,
16
+ APIConnectOptions,
17
+ NotGivenOr,
18
+ NOT_GIVEN,
19
+ )
20
+ from livekit.agents.llm import FunctionTool, ToolChoice
21
+
22
+ from .log import logger
23
+
24
+
25
+ @dataclass
26
+ class LLMOptions:
27
+ api_key: str
28
+ api_base: str = "https://api.dify.ai/v1"
29
+ user: str | None = None
30
+ _conversation_id: str = ""
31
+
32
+ def get_parameters_url(self) -> str:
33
+ return f"{self.api_base}/parameters"
34
+
35
+ def get_message_history_url(self) -> str:
36
+ return f"{self.api_base}/messages"
37
+
38
+ def get_conversation_history_url(self) -> str:
39
+ return f"{self.api_base}/conversations"
40
+
41
+ def get_headers(self) -> Dict[str, str]:
42
+ return {
43
+ "Authorization": f"Bearer {self.api_key}",
44
+ "Content-Type": "application/json",
45
+ }
46
+
47
+
48
+ class LLM(llm.LLM):
49
+ def __init__(
50
+ self,
51
+ *,
52
+ api_key: str | None = None,
53
+ api_base: str | None = None,
54
+ user: str | None = None,
55
+ ) -> None:
56
+ """
57
+ Create a new instance of Dify LLM.
58
+
59
+ api_key (str | None): The Dify API key. Defaults to DIFY_API_KEY env var.
60
+ api_base (str | None): The base URL for the Dify API. Defaults to https://api.dify.ai/v1.
61
+ temperature (float | None): The temperature for generation. Defaults to None.
62
+ conversation_id (str | None): The conversation ID to continue. Defaults to None.
63
+ """
64
+ super().__init__()
65
+ self._session: Optional[aiohttp.ClientSession] = None
66
+
67
+ api_key = api_key or os.environ.get("DIFY_API_KEY")
68
+ if api_key is None:
69
+ raise ValueError("Dify API key is required")
70
+
71
+ api_base = api_base or os.environ.get("DIFY_API_BASE", "https://api.dify.ai/v1")
72
+
73
+ if user is None:
74
+ user = os.environ.get("DIFY_USER", "test_user")
75
+
76
+ self._opts = LLMOptions(
77
+ api_key=api_key,
78
+ api_base=api_base,
79
+ user=user,
80
+ )
81
+
82
+ def chat(
83
+ self,
84
+ *,
85
+ chat_ctx: llm.ChatContext,
86
+ conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
87
+ tools: list[FunctionTool] | None = None,
88
+ parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
89
+ tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN,
90
+ extra_kwargs: NotGivenOr[dict[str, Any]] = NOT_GIVEN,
91
+ ) -> "LLMStream":
92
+ """Start a chat completion stream"""
93
+ # Extract the last user message
94
+ last_message = next(
95
+ (msg for msg in reversed(chat_ctx.items) if msg.role == "user"), None
96
+ )
97
+ if not last_message:
98
+ raise ValueError("No user message found in chat context")
99
+
100
+ # Prepare the request payload
101
+ payload = {
102
+ "inputs": {},
103
+ "query": last_message.content[0],
104
+ "response_mode": "streaming",
105
+ "conversation_id": self._opts._conversation_id,
106
+ "user": self._opts.user,
107
+ }
108
+ # Create headers
109
+ headers = {
110
+ "Authorization": f"Bearer {self._opts.api_key}",
111
+ "Content-Type": "application/json",
112
+ }
113
+
114
+ # Create or reuse the session
115
+ if self._session is None:
116
+ self._session = aiohttp.ClientSession()
117
+ logger.info("llm start", extra={"query": last_message.content[0]})
118
+ # Create the stream
119
+ stream = self._session.post(
120
+ f"{self._opts.api_base}/chat-messages", headers=headers, json=payload
121
+ )
122
+
123
+ return LLMStream(
124
+ self,
125
+ dify_stream=stream,
126
+ chat_ctx=chat_ctx,
127
+ conn_options=conn_options,
128
+ tools=tools,
129
+ )
130
+
131
+ def ensure_session(self) -> None:
132
+ """Ensure that the session is created"""
133
+ if self._session is None:
134
+ self._session = aiohttp.ClientSession()
135
+
136
+ async def get_parameters(self) -> Dict:
137
+ """Get the parameters for the LLM"""
138
+ self.ensure_session()
139
+ res = await self._session.get(
140
+ url=self._opts.get_parameters_url(), headers=self._opts.get_headers()
141
+ )
142
+ return await res.json()
143
+
144
+ async def get_opening_words(self) -> str:
145
+ """Get the introduction text for the LLM"""
146
+ params = await self.get_parameters()
147
+ return params.get("opening_statement", "")
148
+
149
+ async def is_chatable(self) -> bool:
150
+ """Check if the LLM is chatable"""
151
+ url = self._opts.get_conversation_history_url()
152
+ url = f"{url}?&user={self._opts.user}"
153
+ self.ensure_session()
154
+ res = await self._session.get(url=url, headers=self._opts.get_headers())
155
+ if res.status != 200:
156
+ return False
157
+ return True
158
+
159
+ async def required_inputs(self) -> bool:
160
+ """Check if the LLM requires inputs"""
161
+ paras = await self.get_parameters()
162
+ user_input_form = paras.get("user_input_form", [])
163
+ if len(user_input_form) == 0:
164
+ return False
165
+ else:
166
+ required = False
167
+ for item in user_input_form:
168
+ key = list(item.items())[0][0]
169
+ _item = item.get(key, {})
170
+ required = _item.get("required", False)
171
+ break
172
+ return required
173
+
174
+ async def close(self) -> None:
175
+ """Close the LLM client and cleanup resources"""
176
+ if self._session is not None:
177
+ await self._session.close()
178
+ self._session = None
179
+
180
+ @classmethod
181
+ def from_env(cls) -> "LLM":
182
+ """Create a DifyLLM instance from environment variables"""
183
+ api_key = os.getenv("DIFY_API_KEY")
184
+ if not api_key:
185
+ raise ValueError("DIFY_API_KEY environment variable is required")
186
+
187
+ api_base = os.getenv("DIFY_API_BASE", "app-BbmsdDbay9Js28Ku780EDZRV")
188
+
189
+ return cls(
190
+ api_key=api_key,
191
+ api_base=api_base,
192
+ )
193
+
194
+
195
+ class LLMStream(llm.LLMStream):
196
+ def __init__(
197
+ self,
198
+ llm: LLM,
199
+ *,
200
+ dify_stream: aiohttp.ClientResponse,
201
+ chat_ctx: llm.ChatContext,
202
+ conn_options: APIConnectOptions,
203
+ tools: list[FunctionTool] | None = None,
204
+ ) -> None:
205
+ super().__init__(llm, chat_ctx=chat_ctx, conn_options=conn_options, tools=tools)
206
+ self._awaitable_dify_stream = dify_stream
207
+ self._dify_stream: aiohttp.ClientResponse | None = None
208
+ self._request_id: str = ""
209
+ self._input_tokens = 0
210
+ self._output_tokens = 0
211
+
212
+ async def _run(self) -> None:
213
+ """Run the LLM stream"""
214
+ is_chatable = await self._llm.is_chatable()
215
+ required_inputs = await self._llm.required_inputs()
216
+ is_available = is_chatable and (not required_inputs)
217
+ if not is_available:
218
+ error_text = "Dify is not available"
219
+ raise APIConnectionError(error_text, retryable=False)
220
+
221
+ retryable = True
222
+ first_response = True
223
+ try:
224
+ if not self._dify_stream:
225
+ self._dify_stream = await self._awaitable_dify_stream
226
+ async with self._dify_stream as response:
227
+ if response.status != 200:
228
+ error_text = await response.text()
229
+ raise APIStatusError(
230
+ f"Dify API error: {error_text}",
231
+ status_code=response.status,
232
+ body=error_text,
233
+ )
234
+ async for line in response.content:
235
+ if line:
236
+ try:
237
+ line = line.decode("utf-8").strip()
238
+ if line.startswith("data: "):
239
+ data = json.loads(line[6:])
240
+ chat_chunk = self._parse_event(data, self._llm._opts)
241
+ if chat_chunk is not None:
242
+ self._event_ch.send_nowait(chat_chunk)
243
+ retryable = False
244
+ if first_response:
245
+ logger.info("llm first response")
246
+ first_response = False
247
+ except Exception as e:
248
+ logger.error(f"Error processing stream: {e}")
249
+ continue
250
+
251
+ # Send final usage stats
252
+ self._event_ch.send_nowait(
253
+ llm.ChatChunk(
254
+ id=self._request_id,
255
+ usage=llm.CompletionUsage(
256
+ completion_tokens=self._output_tokens,
257
+ prompt_tokens=self._input_tokens,
258
+ total_tokens=self._input_tokens + self._output_tokens,
259
+ ),
260
+ )
261
+ )
262
+ logger.info("llm end")
263
+
264
+ except aiohttp.ClientError as e:
265
+ raise APIConnectionError(retryable=retryable) from e
266
+ except Exception as e:
267
+ raise APIConnectionError(retryable=retryable) from e
268
+
269
+ def _parse_event(
270
+ self, event: Dict[str, Any], ops: LLMOptions
271
+ ) -> llm.ChatChunk | None:
272
+ """Parse a Dify event into a ChatChunk"""
273
+ event_type = event.get("event")
274
+ if event_type == "message_end":
275
+ # Update usage statistics
276
+ if "metadata" in event and "usage" in event["metadata"]:
277
+ usage = event["metadata"]["usage"]
278
+ self._input_tokens = usage.get("prompt_tokens", 0)
279
+ self._output_tokens = usage.get("completion_tokens", 0)
280
+ ops._conversation_id = event.get("conversation_id", "")
281
+ return None
282
+
283
+ elif event_type == "agent_message" or event_type == "message":
284
+ # Extract message content
285
+ answer = event.get("answer", "")
286
+ if not answer:
287
+ return None
288
+
289
+ return llm.ChatChunk(
290
+ id=event.get("message_id", ""),
291
+ delta=llm.ChoiceDelta(
292
+ role="assistant",
293
+ content=answer,
294
+ ),
295
+ )
296
+
297
+ return None
298
+
@@ -0,0 +1,3 @@
1
+ from logging import getLogger
2
+
3
+ logger = getLogger("livekit.plugins.dify")
File without changes
@@ -0,0 +1 @@
1
+ __version__ = "1.0.22"
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: livekit-plugins-difyai
3
+ Version: 1.0.22
4
+ Summary: LiveKit Agent Plugins for Dify
5
+ Author-email: wangmengdi <790990241@qq.com>
6
+ Keywords: audio,livekit,realtime,video,webrtc
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: License :: OSI Approved :: Apache Software License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Topic :: Multimedia :: Sound/Audio
14
+ Classifier: Topic :: Multimedia :: Video
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Requires-Python: >=3.9
17
+ Requires-Dist: livekit-agents~=1.0.22
18
+ Description-Content-Type: text/markdown
19
+
20
+ # LiveKit Plugins Dify
21
+
22
+ Agent Framework plugin for Dify.
23
+
24
+ ## Installation
25
+ ```python
26
+ pip install livekit-plugins-dify
27
+ ```
28
+
29
+ ## Pre-requisites
30
+
31
+ - Dify API Key environment variables: `DIFY_API_KEY`
32
+
33
+ ## Usage
34
+
35
+
36
+ This example shows how to use the Dify plugin.
37
+
38
+ ```python
39
+ from livekit.agents import Agent, AgentSession, JobContext, cli, WorkerOptions
40
+ from livekit.plugins import dify
41
+ from dotenv import load_dotenv
42
+
43
+
44
+ async def entry_point(ctx: JobContext):
45
+
46
+ await ctx.connect()
47
+
48
+ agent = Agent(instructions="You are a helpful assistant.")
49
+
50
+ session = AgentSession(
51
+ llm = dify.LLM(user="xxx")
52
+ )
53
+
54
+ await session.start(agent=agent, room=ctx.room)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ load_dotenv()
59
+ cli.run_app(WorkerOptions(entrypoint_fnc=entry_point))
60
+ ```
61
+
@@ -0,0 +1,8 @@
1
+ livekit/plugins/dify/__init__.py,sha256=sItl-nm33GTWN3M7Oavv8Ybe2l83U75_w4TQI0ubU74,477
2
+ livekit/plugins/dify/llm.py,sha256=a-SL2fdrfAp2xKi9JqhO_O-_N1J5LmuRQ9iDIcYCG3Q,10331
3
+ livekit/plugins/dify/log.py,sha256=Ok6vysHtAP2pArHHRCH0VMYMzjvMq_GxkvVhBT-p8Kg,74
4
+ livekit/plugins/dify/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ livekit/plugins/dify/version.py,sha256=3qPJsI_cs0SlJn5DWM459KiP-4DSaTWmW-7lyF1jPMo,23
6
+ livekit_plugins_difyai-1.0.22.dist-info/METADATA,sha256=zwnX4ioW8EwTtVDWXhFfk5HwXe5LdUfYeK-4i69Xgic,1554
7
+ livekit_plugins_difyai-1.0.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ livekit_plugins_difyai-1.0.22.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any