livekit-plugins-difyai 1.0.22__tar.gz

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,182 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
175
+
176
+ # tests
177
+ test.py
178
+
179
+ .python-version
180
+
181
+ *.ipynb
182
+ config.yaml
@@ -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,42 @@
1
+ # LiveKit Plugins Dify
2
+
3
+ Agent Framework plugin for Dify.
4
+
5
+ ## Installation
6
+ ```python
7
+ pip install livekit-plugins-dify
8
+ ```
9
+
10
+ ## Pre-requisites
11
+
12
+ - Dify API Key environment variables: `DIFY_API_KEY`
13
+
14
+ ## Usage
15
+
16
+
17
+ This example shows how to use the Dify plugin.
18
+
19
+ ```python
20
+ from livekit.agents import Agent, AgentSession, JobContext, cli, WorkerOptions
21
+ from livekit.plugins import dify
22
+ from dotenv import load_dotenv
23
+
24
+
25
+ async def entry_point(ctx: JobContext):
26
+
27
+ await ctx.connect()
28
+
29
+ agent = Agent(instructions="You are a helpful assistant.")
30
+
31
+ session = AgentSession(
32
+ llm = dify.LLM(user="xxx")
33
+ )
34
+
35
+ await session.start(agent=agent, room=ctx.room)
36
+
37
+
38
+ if __name__ == "__main__":
39
+ load_dotenv()
40
+ cli.run_app(WorkerOptions(entrypoint_fnc=entry_point))
41
+ ```
42
+
@@ -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")
@@ -0,0 +1 @@
1
+ __version__ = "1.0.22"
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "livekit-plugins-difyai"
3
+ dynamic = ["version"]
4
+ description = "LiveKit Agent Plugins for Dify"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "wangmengdi", email = "790990241@qq.com" }
8
+ ]
9
+ keywords = ["webrtc", "realtime", "audio", "video", "livekit"]
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "livekit-agents~=1.0.22",
13
+ ]
14
+
15
+ classifiers = [
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Topic :: Multimedia :: Sound/Audio",
19
+ "Topic :: Multimedia :: Video",
20
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ ]
26
+
27
+
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.version]
34
+ path = "livekit/plugins/dify/version.py"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["livekit"]
38
+
39
+ [tool.hatch.build.targets.sdist]
40
+ include = ["/livekit"]