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.
- livekit_plugins_difyai-1.0.22/.gitignore +182 -0
- livekit_plugins_difyai-1.0.22/PKG-INFO +61 -0
- livekit_plugins_difyai-1.0.22/README.md +42 -0
- livekit_plugins_difyai-1.0.22/livekit/plugins/dify/__init__.py +25 -0
- livekit_plugins_difyai-1.0.22/livekit/plugins/dify/llm.py +298 -0
- livekit_plugins_difyai-1.0.22/livekit/plugins/dify/log.py +3 -0
- livekit_plugins_difyai-1.0.22/livekit/plugins/dify/py.typed +0 -0
- livekit_plugins_difyai-1.0.22/livekit/plugins/dify/version.py +1 -0
- livekit_plugins_difyai-1.0.22/pyproject.toml +40 -0
|
@@ -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
|
+
|
|
File without changes
|
|
@@ -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"]
|