dify-player 0.3.8__tar.gz → 0.3.9__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.
- {dify_player-0.3.8 → dify_player-0.3.9}/PKG-INFO +1 -1
- {dify_player-0.3.8 → dify_player-0.3.9}/README.md +7 -1
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/__init__.py +1 -1
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/exceptions.py +21 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/models.py +8 -3
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/__init__.py +3 -1
- dify_player-0.3.9/dify_player/nodes/llm_groq_chat.py +52 -0
- dify_player-0.3.8/dify_player/nodes/llm_groq_chat.py → dify_player-0.3.9/dify_player/nodes/llm_openai_compatible_chat.py +135 -42
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/plan_loader.py +6 -6
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/runtime.py +24 -4
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player.egg-info/PKG-INFO +1 -1
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player.egg-info/SOURCES.txt +1 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/pyproject.toml +1 -1
- {dify_player-0.3.8 → dify_player-0.3.9}/MANIFEST.in +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/__main__.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/cli.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/__init__.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/graph_parser.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/http_body_converter.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/__init__.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/assigner.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/code.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/end.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/http_request.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/if_else.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/llm.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/loop.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/start.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/template_transform.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/variable_aggregator.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/plan_serializer.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/reference_converter.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/workflow_loader.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/workflow_normalizer.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_workflow_importer.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/event_logger.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/input_resolver.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/llm_cache.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/assigner.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/code.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/end.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/http.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/if_else.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/llm_azure_chat.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/llm_xai_chat.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/start.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/template.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/nodes/variable_aggregator.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/provider_config.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/value_renderer.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/workflow_engine.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player/workflow_executor.py +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player.egg-info/dependency_links.txt +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player.egg-info/requires.txt +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/dify_player.egg-info/top_level.txt +0 -0
- {dify_player-0.3.8 → dify_player-0.3.9}/setup.cfg +0 -0
|
@@ -99,7 +99,7 @@ async def run_workflow(payload: dict) -> dict:
|
|
|
99
99
|
|
|
100
100
|
### Provider Config
|
|
101
101
|
|
|
102
|
-
Azure OpenAI
|
|
102
|
+
Azure OpenAI、OpenAI-compatible endpoint、Groq の接続設定は、実行単位で `provider_config` として渡せます。
|
|
103
103
|
優先順位は `provider_config`、`DIFY_PLAYER_*` 環境変数、既存互換の環境変数の順です。
|
|
104
104
|
|
|
105
105
|
```python
|
|
@@ -110,6 +110,8 @@ result = await engine.run_compiled_plan(
|
|
|
110
110
|
"azure_openai_endpoint": "https://example.openai.azure.com",
|
|
111
111
|
"azure_openai_api_key": "...",
|
|
112
112
|
"azure_openai_api_version": "2024-10-21",
|
|
113
|
+
"openai_compatible_api_key": "...",
|
|
114
|
+
"openai_compatible_api_base_url": "http://localhost:8000/v1",
|
|
113
115
|
"groq_api_key": "...",
|
|
114
116
|
"groq_api_base_url": "https://api.groq.com/openai/v1",
|
|
115
117
|
},
|
|
@@ -120,9 +122,13 @@ result = await engine.run_compiled_plan(
|
|
|
120
122
|
|
|
121
123
|
- Azure OpenAI: `DIFY_PLAYER_AZURE_OPENAI_ENDPOINT`, `DIFY_PLAYER_AZURE_OPENAI_API_KEY`, `DIFY_PLAYER_AZURE_OPENAI_API_VERSION`
|
|
122
124
|
- Azure OpenAI 既存互換: `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_API_VERSION`
|
|
125
|
+
- OpenAI-compatible: `DIFY_PLAYER_OPENAI_COMPATIBLE_API_KEY`, `DIFY_PLAYER_OPENAI_COMPATIBLE_API_BASE_URL`
|
|
126
|
+
- OpenAI-compatible 既存互換: `OPENAI_COMPATIBLE_API_KEY`, `OPENAI_COMPATIBLE_API_BASE_URL`
|
|
123
127
|
- Groq: `DIFY_PLAYER_GROQ_API_KEY`, `DIFY_PLAYER_GROQ_API_BASE_URL`
|
|
124
128
|
- Groq 既存互換: `GROQ_API_KEY`, `GROQ_API_BASE_URL`
|
|
125
129
|
|
|
130
|
+
新規の OpenAI-compatible plan では `llm_openai_compatible_chat` を使ってください。`llm_groq_chat` は既存 plan 互換の alias として残しており、Groq の既定 base URL を引き続き使います。
|
|
131
|
+
|
|
126
132
|
### LLM Cache Mode
|
|
127
133
|
|
|
128
134
|
workflow 改善中だけ LLM 応答を再利用したい場合は、`llm_cache=True` と cache store を渡します。
|
|
@@ -84,3 +84,24 @@ class GroqLLMStructuredOutputError(RuntimeError):
|
|
|
84
84
|
def __init__(self, message: str, *, attempts: list[dict[str, str]]) -> None:
|
|
85
85
|
super().__init__(message)
|
|
86
86
|
self.attempts = attempts
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class OpenAICompatibleLLMRequestFailedError(RuntimeError):
|
|
90
|
+
"""Raised when an OpenAI-compatible LLM request fails before receiving a response."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class OpenAICompatibleLLMBadStatusError(RuntimeError):
|
|
94
|
+
"""Raised when an OpenAI-compatible LLM request returns a 4xx/5xx response."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, message: str, *, status_code: int, response_body: str | None = None) -> None:
|
|
97
|
+
super().__init__(message)
|
|
98
|
+
self.status_code = status_code
|
|
99
|
+
self.response_body = response_body
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class OpenAICompatibleLLMStructuredOutputError(RuntimeError):
|
|
103
|
+
"""Raised when OpenAI-compatible structured output remains invalid after repair retries."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, message: str, *, attempts: list[dict[str, str]]) -> None:
|
|
106
|
+
super().__init__(message)
|
|
107
|
+
self.attempts = attempts
|
|
@@ -3,13 +3,18 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import asdict, dataclass, field, is_dataclass
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
LLM_CHAT_NODE_KINDS = {
|
|
7
|
+
"llm_azure_chat",
|
|
8
|
+
"llm_xai_chat",
|
|
9
|
+
"llm_groq_chat",
|
|
10
|
+
"llm_openai_compatible_chat",
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
ALLOWED_NODE_KINDS = {
|
|
7
14
|
"start",
|
|
8
15
|
"template",
|
|
9
16
|
"http",
|
|
10
|
-
|
|
11
|
-
"llm_xai_chat",
|
|
12
|
-
"llm_groq_chat",
|
|
17
|
+
*LLM_CHAT_NODE_KINDS,
|
|
13
18
|
"if_else",
|
|
14
19
|
"variable_aggregator",
|
|
15
20
|
"code",
|
|
@@ -15,6 +15,7 @@ from dify_player.nodes.http import run as run_http
|
|
|
15
15
|
from dify_player.nodes.if_else import run as run_if_else
|
|
16
16
|
from dify_player.nodes.llm_azure_chat import run as run_llm_azure_chat
|
|
17
17
|
from dify_player.nodes.llm_groq_chat import run as run_llm_groq_chat
|
|
18
|
+
from dify_player.nodes.llm_openai_compatible_chat import run as run_llm_openai_compatible_chat
|
|
18
19
|
from dify_player.nodes.llm_xai_chat import run as run_llm_xai_chat
|
|
19
20
|
from dify_player.nodes.start import run as run_start
|
|
20
21
|
from dify_player.nodes.template import run as run_template
|
|
@@ -29,6 +30,7 @@ _EXECUTORS: dict[str, NodeExecutor] = {
|
|
|
29
30
|
"llm_azure_chat": run_llm_azure_chat,
|
|
30
31
|
"llm_xai_chat": run_llm_xai_chat,
|
|
31
32
|
"llm_groq_chat": run_llm_groq_chat,
|
|
33
|
+
"llm_openai_compatible_chat": run_llm_openai_compatible_chat,
|
|
32
34
|
"if_else": run_if_else,
|
|
33
35
|
"variable_aggregator": run_variable_aggregator,
|
|
34
36
|
"code": run_code,
|
|
@@ -56,6 +58,6 @@ async def run_node(
|
|
|
56
58
|
"resolved_inputs": resolved_inputs,
|
|
57
59
|
"http_client": http_client,
|
|
58
60
|
}
|
|
59
|
-
if node.kind in {"llm_azure_chat", "llm_groq_chat"}:
|
|
61
|
+
if node.kind in {"llm_azure_chat", "llm_groq_chat", "llm_openai_compatible_chat"}:
|
|
60
62
|
kwargs["provider_config"] = provider_config
|
|
61
63
|
return await executor(**kwargs)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""LLM node for Groq and other OpenAI-compatible Chat Completions endpoints.
|
|
2
|
+
|
|
3
|
+
Although named ``llm_groq_chat`` for historical reasons, this node speaks the
|
|
4
|
+
plain OpenAI ``/chat/completions`` protocol, so it can target any
|
|
5
|
+
OpenAI-compatible server (Groq, on-prem vLLM / TGI, etc.) simply by overriding
|
|
6
|
+
``groq_api_base_url`` / ``DIFY_PLAYER_GROQ_API_BASE_URL``. Structured output is
|
|
7
|
+
requested with the OpenAI-standard ``response_format: {"type": "json_schema",
|
|
8
|
+
...}`` shape, which recent vLLM builds accept, so OSS / on-prem deployments
|
|
9
|
+
reuse this node unchanged.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from dify_player.exceptions import GroqLLMBadStatusError, GroqLLMRequestFailedError, GroqLLMStructuredOutputError
|
|
19
|
+
from dify_player.models import Node
|
|
20
|
+
from dify_player.nodes.llm_openai_compatible_chat import run_openai_compatible_chat
|
|
21
|
+
from dify_player.provider_config import ProviderConfig
|
|
22
|
+
|
|
23
|
+
_DEFAULT_BASE_URL = "https://api.groq.com/openai/v1"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def run(
|
|
27
|
+
*,
|
|
28
|
+
node: Node,
|
|
29
|
+
workflow_inputs: dict[str, Any],
|
|
30
|
+
node_outputs: dict[str, dict[str, Any]],
|
|
31
|
+
resolved_inputs: dict[str, Any],
|
|
32
|
+
http_client: httpx.AsyncClient | None = None,
|
|
33
|
+
provider_config: ProviderConfig | None = None,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
return await run_openai_compatible_chat(
|
|
36
|
+
node=node,
|
|
37
|
+
workflow_inputs=workflow_inputs,
|
|
38
|
+
node_outputs=node_outputs,
|
|
39
|
+
resolved_inputs=resolved_inputs,
|
|
40
|
+
http_client=http_client,
|
|
41
|
+
provider_config=provider_config,
|
|
42
|
+
api_key_config_key="groq_api_key",
|
|
43
|
+
api_key_env_names=("DIFY_PLAYER_GROQ_API_KEY", "GROQ_API_KEY"),
|
|
44
|
+
base_url_config_key="groq_api_base_url",
|
|
45
|
+
base_url_env_names=("DIFY_PLAYER_GROQ_API_BASE_URL", "GROQ_API_BASE_URL"),
|
|
46
|
+
base_url_default=_DEFAULT_BASE_URL,
|
|
47
|
+
provider_name="groq",
|
|
48
|
+
bad_status_error_cls=GroqLLMBadStatusError,
|
|
49
|
+
request_failed_error_cls=GroqLLMRequestFailedError,
|
|
50
|
+
structured_output_error_cls=GroqLLMStructuredOutputError,
|
|
51
|
+
json_validate_failed_default_message="Groq json_validate_failed",
|
|
52
|
+
)
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
"""LLM node for Groq and other OpenAI-compatible Chat Completions endpoints.
|
|
2
|
-
|
|
3
|
-
Although named ``llm_groq_chat`` for historical reasons, this node speaks the
|
|
4
|
-
plain OpenAI ``/chat/completions`` protocol, so it can target any
|
|
5
|
-
OpenAI-compatible server (Groq, on-prem vLLM / TGI, etc.) simply by overriding
|
|
6
|
-
``groq_api_base_url`` / ``DIFY_PLAYER_GROQ_API_BASE_URL``. Structured output is
|
|
7
|
-
requested with the OpenAI-standard ``response_format: {"type": "json_schema",
|
|
8
|
-
...}`` shape, which recent vLLM builds accept, so OSS / on-prem deployments
|
|
9
|
-
reuse this node unchanged.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
1
|
from __future__ import annotations
|
|
13
2
|
|
|
14
3
|
import json
|
|
@@ -16,7 +5,11 @@ from typing import Any
|
|
|
16
5
|
|
|
17
6
|
import httpx
|
|
18
7
|
|
|
19
|
-
from dify_player.exceptions import
|
|
8
|
+
from dify_player.exceptions import (
|
|
9
|
+
OpenAICompatibleLLMBadStatusError,
|
|
10
|
+
OpenAICompatibleLLMRequestFailedError,
|
|
11
|
+
OpenAICompatibleLLMStructuredOutputError,
|
|
12
|
+
)
|
|
20
13
|
from dify_player.models import Node
|
|
21
14
|
from dify_player.nodes.llm_azure_chat import (
|
|
22
15
|
_build_bad_status_message,
|
|
@@ -32,7 +25,6 @@ from dify_player.nodes.llm_azure_chat import (
|
|
|
32
25
|
from dify_player.provider_config import ProviderConfig, resolve_provider_config_value
|
|
33
26
|
from dify_player.value_renderer import render_value
|
|
34
27
|
|
|
35
|
-
_DEFAULT_BASE_URL = "https://api.groq.com/openai/v1"
|
|
36
28
|
_STRUCTURED_OUTPUT_REPAIR_RETRIES = 1
|
|
37
29
|
|
|
38
30
|
|
|
@@ -44,22 +36,69 @@ async def run(
|
|
|
44
36
|
resolved_inputs: dict[str, Any],
|
|
45
37
|
http_client: httpx.AsyncClient | None = None,
|
|
46
38
|
provider_config: ProviderConfig | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
return await run_openai_compatible_chat(
|
|
41
|
+
node=node,
|
|
42
|
+
workflow_inputs=workflow_inputs,
|
|
43
|
+
node_outputs=node_outputs,
|
|
44
|
+
resolved_inputs=resolved_inputs,
|
|
45
|
+
http_client=http_client,
|
|
46
|
+
provider_config=provider_config,
|
|
47
|
+
api_key_config_key="openai_compatible_api_key",
|
|
48
|
+
api_key_env_names=(
|
|
49
|
+
"DIFY_PLAYER_OPENAI_COMPATIBLE_API_KEY",
|
|
50
|
+
"OPENAI_COMPATIBLE_API_KEY",
|
|
51
|
+
),
|
|
52
|
+
base_url_config_key="openai_compatible_api_base_url",
|
|
53
|
+
base_url_env_names=(
|
|
54
|
+
"DIFY_PLAYER_OPENAI_COMPATIBLE_API_BASE_URL",
|
|
55
|
+
"OPENAI_COMPATIBLE_API_BASE_URL",
|
|
56
|
+
),
|
|
57
|
+
base_url_default=None,
|
|
58
|
+
provider_name="openai_compatible",
|
|
59
|
+
bad_status_error_cls=OpenAICompatibleLLMBadStatusError,
|
|
60
|
+
request_failed_error_cls=OpenAICompatibleLLMRequestFailedError,
|
|
61
|
+
structured_output_error_cls=OpenAICompatibleLLMStructuredOutputError,
|
|
62
|
+
json_validate_failed_default_message=(
|
|
63
|
+
"OpenAI-compatible json_validate_failed"
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def run_openai_compatible_chat(
|
|
69
|
+
*,
|
|
70
|
+
node: Node,
|
|
71
|
+
workflow_inputs: dict[str, Any],
|
|
72
|
+
node_outputs: dict[str, dict[str, Any]],
|
|
73
|
+
resolved_inputs: dict[str, Any],
|
|
74
|
+
http_client: httpx.AsyncClient | None,
|
|
75
|
+
provider_config: ProviderConfig | None,
|
|
76
|
+
api_key_config_key: str,
|
|
77
|
+
api_key_env_names: tuple[str, ...],
|
|
78
|
+
base_url_config_key: str,
|
|
79
|
+
base_url_env_names: tuple[str, ...],
|
|
80
|
+
base_url_default: str | None,
|
|
81
|
+
provider_name: str,
|
|
82
|
+
bad_status_error_cls: type[OpenAICompatibleLLMBadStatusError],
|
|
83
|
+
request_failed_error_cls: type[OpenAICompatibleLLMRequestFailedError],
|
|
84
|
+
structured_output_error_cls: type[OpenAICompatibleLLMStructuredOutputError],
|
|
85
|
+
json_validate_failed_default_message: str,
|
|
47
86
|
) -> dict[str, Any]:
|
|
48
87
|
_ = resolved_inputs
|
|
49
88
|
api_key = resolve_provider_config_value(
|
|
50
89
|
provider_config=provider_config,
|
|
51
|
-
config_key=
|
|
52
|
-
env_names=
|
|
90
|
+
config_key=api_key_config_key,
|
|
91
|
+
env_names=api_key_env_names,
|
|
53
92
|
node=node,
|
|
54
93
|
required=True,
|
|
55
94
|
)
|
|
56
95
|
base_url = resolve_provider_config_value(
|
|
57
96
|
provider_config=provider_config,
|
|
58
|
-
config_key=
|
|
59
|
-
env_names=
|
|
97
|
+
config_key=base_url_config_key,
|
|
98
|
+
env_names=base_url_env_names,
|
|
60
99
|
node=node,
|
|
61
|
-
required=
|
|
62
|
-
default=
|
|
100
|
+
required=base_url_default is None,
|
|
101
|
+
default=base_url_default,
|
|
63
102
|
)
|
|
64
103
|
|
|
65
104
|
messages = render_value(
|
|
@@ -74,6 +113,11 @@ async def run(
|
|
|
74
113
|
api_key=api_key,
|
|
75
114
|
messages=messages,
|
|
76
115
|
http_client=http_client,
|
|
116
|
+
provider_name=provider_name,
|
|
117
|
+
bad_status_error_cls=bad_status_error_cls,
|
|
118
|
+
request_failed_error_cls=request_failed_error_cls,
|
|
119
|
+
structured_output_error_cls=structured_output_error_cls,
|
|
120
|
+
json_validate_failed_default_message=json_validate_failed_default_message,
|
|
77
121
|
)
|
|
78
122
|
|
|
79
123
|
raw_text, metadata = await _request_raw_text(
|
|
@@ -82,6 +126,8 @@ async def run(
|
|
|
82
126
|
api_key=api_key,
|
|
83
127
|
messages=messages,
|
|
84
128
|
http_client=http_client,
|
|
129
|
+
bad_status_error_cls=bad_status_error_cls,
|
|
130
|
+
request_failed_error_cls=request_failed_error_cls,
|
|
85
131
|
)
|
|
86
132
|
return _build_llm_output(
|
|
87
133
|
text=raw_text,
|
|
@@ -89,23 +135,32 @@ async def run(
|
|
|
89
135
|
usage=metadata["usage"],
|
|
90
136
|
finish_reason=metadata["finish_reason"],
|
|
91
137
|
attempt_count=1,
|
|
138
|
+
provider_name=provider_name,
|
|
92
139
|
)
|
|
93
140
|
|
|
94
141
|
|
|
95
142
|
def _build_request_body(*, node: Node, messages: Any) -> dict[str, Any]:
|
|
96
143
|
if not isinstance(messages, list):
|
|
97
|
-
raise ValueError(
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"llm node {node.label!r} config.messages must render to an array"
|
|
146
|
+
)
|
|
98
147
|
|
|
99
148
|
request_messages: list[dict[str, str]] = []
|
|
100
149
|
for index, message in enumerate(messages):
|
|
101
150
|
if not isinstance(message, dict):
|
|
102
|
-
raise ValueError(
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"llm node {node.label!r} config.messages[{index}] must be an object"
|
|
153
|
+
)
|
|
103
154
|
role = message.get("role")
|
|
104
155
|
content = message.get("content")
|
|
105
156
|
if not isinstance(role, str) or not role:
|
|
106
|
-
raise ValueError(
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"llm node {node.label!r} config.messages[{index}].role must be a non-empty string"
|
|
159
|
+
)
|
|
107
160
|
if not isinstance(content, str):
|
|
108
|
-
raise ValueError(
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"llm node {node.label!r} config.messages[{index}].content must be a string"
|
|
163
|
+
)
|
|
109
164
|
request_messages.append({"role": role, "content": content})
|
|
110
165
|
|
|
111
166
|
body: dict[str, Any] = {
|
|
@@ -135,6 +190,8 @@ async def _request_raw_text(
|
|
|
135
190
|
api_key: str,
|
|
136
191
|
messages: list[dict[str, str]],
|
|
137
192
|
http_client: httpx.AsyncClient | None,
|
|
193
|
+
bad_status_error_cls: type[OpenAICompatibleLLMBadStatusError],
|
|
194
|
+
request_failed_error_cls: type[OpenAICompatibleLLMRequestFailedError],
|
|
138
195
|
) -> tuple[str, dict[str, Any]]:
|
|
139
196
|
request_body = _build_request_body(node=node, messages=messages)
|
|
140
197
|
url = f"{base_url.rstrip('/')}/chat/completions"
|
|
@@ -145,22 +202,33 @@ async def _request_raw_text(
|
|
|
145
202
|
try:
|
|
146
203
|
response = await http_client.post(
|
|
147
204
|
url,
|
|
148
|
-
headers={
|
|
205
|
+
headers={
|
|
206
|
+
"Authorization": f"Bearer {api_key}",
|
|
207
|
+
"Content-Type": "application/json",
|
|
208
|
+
},
|
|
149
209
|
json=request_body,
|
|
150
210
|
timeout=node.config.get("timeout_sec", 60),
|
|
151
211
|
)
|
|
152
212
|
except httpx.RequestError as exc:
|
|
153
|
-
raise
|
|
213
|
+
raise request_failed_error_cls(
|
|
214
|
+
f"llm node {node.label!r} request failed: {exc}"
|
|
215
|
+
) from exc
|
|
154
216
|
|
|
155
217
|
if response.status_code >= 400:
|
|
156
|
-
raise
|
|
157
|
-
_build_bad_status_message(
|
|
218
|
+
raise bad_status_error_cls(
|
|
219
|
+
_build_bad_status_message(
|
|
220
|
+
node=node,
|
|
221
|
+
status_code=response.status_code,
|
|
222
|
+
response_body=response.text,
|
|
223
|
+
),
|
|
158
224
|
status_code=response.status_code,
|
|
159
225
|
response_body=response.text,
|
|
160
226
|
)
|
|
161
227
|
|
|
162
228
|
payload = _parse_response_json(node=node, body=response.text)
|
|
163
|
-
return _extract_response_text(
|
|
229
|
+
return _extract_response_text(
|
|
230
|
+
node=node, payload=payload
|
|
231
|
+
), _extract_response_metadata(payload)
|
|
164
232
|
|
|
165
233
|
|
|
166
234
|
async def _run_structured_output_with_repair(
|
|
@@ -170,6 +238,11 @@ async def _run_structured_output_with_repair(
|
|
|
170
238
|
api_key: str,
|
|
171
239
|
messages: list[dict[str, str]],
|
|
172
240
|
http_client: httpx.AsyncClient | None,
|
|
241
|
+
provider_name: str,
|
|
242
|
+
bad_status_error_cls: type[OpenAICompatibleLLMBadStatusError],
|
|
243
|
+
request_failed_error_cls: type[OpenAICompatibleLLMRequestFailedError],
|
|
244
|
+
structured_output_error_cls: type[OpenAICompatibleLLMStructuredOutputError],
|
|
245
|
+
json_validate_failed_default_message: str,
|
|
173
246
|
) -> dict[str, Any]:
|
|
174
247
|
current_messages = list(messages)
|
|
175
248
|
attempts: list[dict[str, str]] = []
|
|
@@ -184,9 +257,14 @@ async def _run_structured_output_with_repair(
|
|
|
184
257
|
api_key=api_key,
|
|
185
258
|
messages=current_messages,
|
|
186
259
|
http_client=http_client,
|
|
260
|
+
bad_status_error_cls=bad_status_error_cls,
|
|
261
|
+
request_failed_error_cls=request_failed_error_cls,
|
|
262
|
+
)
|
|
263
|
+
except bad_status_error_cls as exc:
|
|
264
|
+
failed_generation = _extract_json_validate_failed_generation(
|
|
265
|
+
exc,
|
|
266
|
+
default_message=json_validate_failed_default_message,
|
|
187
267
|
)
|
|
188
|
-
except GroqLLMBadStatusError as exc:
|
|
189
|
-
failed_generation = _extract_groq_failed_generation(exc)
|
|
190
268
|
if failed_generation is None:
|
|
191
269
|
raise
|
|
192
270
|
|
|
@@ -200,12 +278,15 @@ async def _run_structured_output_with_repair(
|
|
|
200
278
|
}
|
|
201
279
|
)
|
|
202
280
|
if attempt_index >= _STRUCTURED_OUTPUT_REPAIR_RETRIES:
|
|
203
|
-
raise
|
|
281
|
+
raise structured_output_error_cls(
|
|
204
282
|
f"{validation_error} after {attempt_index + 1} attempts",
|
|
205
283
|
attempts=attempts,
|
|
206
284
|
) from exc
|
|
207
285
|
current_messages = current_messages + [
|
|
208
|
-
_build_repair_message(
|
|
286
|
+
_build_repair_message(
|
|
287
|
+
validation_error=validation_error,
|
|
288
|
+
previous_output=raw_text,
|
|
289
|
+
)
|
|
209
290
|
]
|
|
210
291
|
continue
|
|
211
292
|
|
|
@@ -213,13 +294,19 @@ async def _run_structured_output_with_repair(
|
|
|
213
294
|
latest_metadata = metadata
|
|
214
295
|
try:
|
|
215
296
|
structured_output = _parse_structured_output(node=node, raw_text=raw_text)
|
|
216
|
-
_validate_schema(
|
|
297
|
+
_validate_schema(
|
|
298
|
+
node=node,
|
|
299
|
+
schema=node.config["json_schema"],
|
|
300
|
+
value=structured_output,
|
|
301
|
+
path="$",
|
|
302
|
+
)
|
|
217
303
|
return _build_llm_output(
|
|
218
304
|
text=raw_text,
|
|
219
305
|
structured_output=structured_output,
|
|
220
306
|
usage=aggregated_usage,
|
|
221
307
|
finish_reason=latest_metadata["finish_reason"],
|
|
222
308
|
attempt_count=attempt_index + 1,
|
|
309
|
+
provider_name=provider_name,
|
|
223
310
|
)
|
|
224
311
|
except ValueError as exc:
|
|
225
312
|
attempts.append(
|
|
@@ -230,20 +317,25 @@ async def _run_structured_output_with_repair(
|
|
|
230
317
|
}
|
|
231
318
|
)
|
|
232
319
|
if attempt_index >= _STRUCTURED_OUTPUT_REPAIR_RETRIES:
|
|
233
|
-
raise
|
|
320
|
+
raise structured_output_error_cls(
|
|
234
321
|
f"{str(exc)} after {attempt_index + 1} attempts",
|
|
235
322
|
attempts=attempts,
|
|
236
323
|
) from exc
|
|
237
|
-
current_messages = current_messages + [
|
|
324
|
+
current_messages = current_messages + [
|
|
325
|
+
_build_repair_message(
|
|
326
|
+
validation_error=str(exc),
|
|
327
|
+
previous_output=raw_text,
|
|
328
|
+
)
|
|
329
|
+
]
|
|
238
330
|
|
|
239
331
|
raise AssertionError("structured output repair loop must return or raise")
|
|
240
332
|
|
|
241
333
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
334
|
+
def _extract_json_validate_failed_generation(
|
|
335
|
+
exc: OpenAICompatibleLLMBadStatusError,
|
|
336
|
+
*,
|
|
337
|
+
default_message: str,
|
|
338
|
+
) -> tuple[str, str] | None:
|
|
247
339
|
if exc.status_code != 400 or exc.response_body is None:
|
|
248
340
|
return None
|
|
249
341
|
|
|
@@ -266,7 +358,7 @@ def _extract_groq_failed_generation(exc: GroqLLMBadStatusError) -> tuple[str, st
|
|
|
266
358
|
|
|
267
359
|
message = error.get("message")
|
|
268
360
|
if not isinstance(message, str) or not message:
|
|
269
|
-
message =
|
|
361
|
+
message = default_message
|
|
270
362
|
return message, failed_generation
|
|
271
363
|
|
|
272
364
|
|
|
@@ -277,6 +369,7 @@ def _build_llm_output(
|
|
|
277
369
|
usage: dict[str, int],
|
|
278
370
|
finish_reason: str | None,
|
|
279
371
|
attempt_count: int,
|
|
372
|
+
provider_name: str,
|
|
280
373
|
) -> dict[str, Any]:
|
|
281
374
|
output = {
|
|
282
375
|
"text": text,
|
|
@@ -284,7 +377,7 @@ def _build_llm_output(
|
|
|
284
377
|
"finish_reason": finish_reason,
|
|
285
378
|
"attempt_count": attempt_count,
|
|
286
379
|
"safety": None,
|
|
287
|
-
"provider_details": {"provider":
|
|
380
|
+
"provider_details": {"provider": provider_name},
|
|
288
381
|
}
|
|
289
382
|
if structured_output is not None:
|
|
290
383
|
output["structured_output"] = structured_output
|
|
@@ -8,10 +8,10 @@ from typing import Any
|
|
|
8
8
|
from jinja2 import meta
|
|
9
9
|
|
|
10
10
|
from dify_player.exceptions import PlanValidationError
|
|
11
|
-
from dify_player.models import ALLOWED_NODE_KINDS, CompiledLoop, CompiledPlan, Edge, Node, Plan, format_node_label
|
|
11
|
+
from dify_player.models import ALLOWED_NODE_KINDS, LLM_CHAT_NODE_KINDS, CompiledLoop, CompiledPlan, Edge, Node, Plan, format_node_label
|
|
12
12
|
from dify_player.value_renderer import get_native_environment
|
|
13
13
|
|
|
14
|
-
LOOP_BODY_ALLOWED_NODE_KINDS = {
|
|
14
|
+
LOOP_BODY_ALLOWED_NODE_KINDS = {*LLM_CHAT_NODE_KINDS, "code", "if_else", "template", "assigner"}
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _load_json(path: Path) -> Any:
|
|
@@ -92,7 +92,7 @@ def parse_plan(data: Any) -> Plan:
|
|
|
92
92
|
_validate_code_config(node_id=node_id, node_name=name, config=normalized_config)
|
|
93
93
|
if kind == "loop":
|
|
94
94
|
_validate_loop_config(node_id=node_id, node_name=name, config=normalized_config)
|
|
95
|
-
if kind in
|
|
95
|
+
if kind in LLM_CHAT_NODE_KINDS:
|
|
96
96
|
_validate_llm_chat_config(node_id=node_id, node_name=name, kind=kind, config=normalized_config)
|
|
97
97
|
if kind == "end" and not isinstance(normalized_config.get("outputs"), dict):
|
|
98
98
|
raise PlanValidationError(f"end node {node_label!r} must define config.outputs as an object")
|
|
@@ -265,7 +265,7 @@ def _validate_node_definition(node: Node) -> None:
|
|
|
265
265
|
elif node.kind == "loop":
|
|
266
266
|
if node.inputs:
|
|
267
267
|
raise PlanValidationError(f"loop node {node.label!r} must not define inputs")
|
|
268
|
-
elif node.kind in
|
|
268
|
+
elif node.kind in LLM_CHAT_NODE_KINDS:
|
|
269
269
|
_validate_allowed_roots(
|
|
270
270
|
node.config["messages"],
|
|
271
271
|
allowed_roots={"inputs", "nodes"},
|
|
@@ -619,7 +619,7 @@ def _parse_loop_body_nodes(raw_body_nodes: list[Any], *, node_label: str) -> lis
|
|
|
619
619
|
_validate_if_else_config(node_id=node_id, node_name=name, config=normalized_config)
|
|
620
620
|
if kind == "code":
|
|
621
621
|
_validate_code_config(node_id=node_id, node_name=name, config=normalized_config)
|
|
622
|
-
if kind in
|
|
622
|
+
if kind in LLM_CHAT_NODE_KINDS:
|
|
623
623
|
_validate_llm_chat_config(node_id=node_id, node_name=name, kind=kind, config=normalized_config)
|
|
624
624
|
if kind == "assigner":
|
|
625
625
|
_validate_assigner_config(node_id=node_id, node_name=name, config=normalized_config)
|
|
@@ -677,7 +677,7 @@ def _validate_loop_body_node_definition(node: Node) -> None:
|
|
|
677
677
|
elif node.kind == "code":
|
|
678
678
|
if not isinstance(node.config.get("source"), str):
|
|
679
679
|
raise PlanValidationError(f"loop body code node {node.label!r} must define config.source as a string")
|
|
680
|
-
elif node.kind in
|
|
680
|
+
elif node.kind in LLM_CHAT_NODE_KINDS:
|
|
681
681
|
_validate_allowed_roots(
|
|
682
682
|
node.config["messages"],
|
|
683
683
|
allowed_roots={"inputs", "nodes"},
|
|
@@ -17,6 +17,9 @@ from dify_player.exceptions import (
|
|
|
17
17
|
GroqLLMStructuredOutputError,
|
|
18
18
|
HTTPBadStatusError,
|
|
19
19
|
HTTPRequestFailedError,
|
|
20
|
+
OpenAICompatibleLLMBadStatusError,
|
|
21
|
+
OpenAICompatibleLLMRequestFailedError,
|
|
22
|
+
OpenAICompatibleLLMStructuredOutputError,
|
|
20
23
|
PlanValidationError,
|
|
21
24
|
UnsupportedNodeError,
|
|
22
25
|
XAILLMBadStatusError,
|
|
@@ -477,7 +480,7 @@ class WorkflowRuntime:
|
|
|
477
480
|
return cached_output
|
|
478
481
|
|
|
479
482
|
def _should_use_llm_cache(self, *, node: Node) -> bool:
|
|
480
|
-
return self.llm_cache_enabled and node.kind
|
|
483
|
+
return self.llm_cache_enabled and node.kind in {"llm_azure_chat", "llm_openai_compatible_chat"}
|
|
481
484
|
|
|
482
485
|
def _load_cached_llm_output(
|
|
483
486
|
self,
|
|
@@ -536,6 +539,15 @@ class WorkflowRuntime:
|
|
|
536
539
|
),
|
|
537
540
|
"provider_config": _cacheable_provider_config(self.provider_config),
|
|
538
541
|
}
|
|
542
|
+
elif node.kind == "llm_openai_compatible_chat":
|
|
543
|
+
extra = {
|
|
544
|
+
"rendered_messages": render_value(
|
|
545
|
+
node.config["messages"],
|
|
546
|
+
{"inputs": workflow_inputs, "nodes": node_outputs},
|
|
547
|
+
location=f"llm cache key for node {node.label!r} config.messages",
|
|
548
|
+
),
|
|
549
|
+
"provider_config": _cacheable_provider_config(self.provider_config),
|
|
550
|
+
}
|
|
539
551
|
return build_llm_cache_key(
|
|
540
552
|
node_kind=node.kind,
|
|
541
553
|
node_config=node.config,
|
|
@@ -674,7 +686,7 @@ class WorkflowRuntime:
|
|
|
674
686
|
**({"response_body": exc.response_body} if exc.response_body is not None else {}),
|
|
675
687
|
},
|
|
676
688
|
)
|
|
677
|
-
if isinstance(exc, (AzureLLMRequestFailedError, XAILLMRequestFailedError, GroqLLMRequestFailedError)):
|
|
689
|
+
if isinstance(exc, (AzureLLMRequestFailedError, XAILLMRequestFailedError, GroqLLMRequestFailedError, OpenAICompatibleLLMRequestFailedError)):
|
|
678
690
|
return WorkflowError(
|
|
679
691
|
code="LLM_REQUEST_FAILED",
|
|
680
692
|
message=str(exc),
|
|
@@ -683,7 +695,7 @@ class WorkflowRuntime:
|
|
|
683
695
|
node_name=node.name,
|
|
684
696
|
retryable=True,
|
|
685
697
|
)
|
|
686
|
-
if isinstance(exc, (AzureLLMBadStatusError, XAILLMBadStatusError, GroqLLMBadStatusError)):
|
|
698
|
+
if isinstance(exc, (AzureLLMBadStatusError, XAILLMBadStatusError, GroqLLMBadStatusError, OpenAICompatibleLLMBadStatusError)):
|
|
687
699
|
return WorkflowError(
|
|
688
700
|
code="LLM_BAD_STATUS",
|
|
689
701
|
message=str(exc),
|
|
@@ -696,7 +708,7 @@ class WorkflowRuntime:
|
|
|
696
708
|
**({"response_body": exc.response_body} if exc.response_body is not None else {}),
|
|
697
709
|
},
|
|
698
710
|
)
|
|
699
|
-
if isinstance(exc, (AzureLLMStructuredOutputError, XAILLMStructuredOutputError, GroqLLMStructuredOutputError)):
|
|
711
|
+
if isinstance(exc, (AzureLLMStructuredOutputError, XAILLMStructuredOutputError, GroqLLMStructuredOutputError, OpenAICompatibleLLMStructuredOutputError)):
|
|
700
712
|
return WorkflowError(
|
|
701
713
|
code="NODE_EXECUTION_FAILED",
|
|
702
714
|
message=str(exc),
|
|
@@ -790,6 +802,14 @@ def _cacheable_provider_config(provider_config: ProviderConfig | None) -> dict[s
|
|
|
790
802
|
config_key="azure_openai_api_version",
|
|
791
803
|
env_names=("DIFY_PLAYER_AZURE_OPENAI_API_VERSION", "AZURE_OPENAI_API_VERSION"),
|
|
792
804
|
),
|
|
805
|
+
"openai_compatible_api_base_url": _cacheable_provider_config_value(
|
|
806
|
+
provider_config=provider_config,
|
|
807
|
+
config_key="openai_compatible_api_base_url",
|
|
808
|
+
env_names=(
|
|
809
|
+
"DIFY_PLAYER_OPENAI_COMPATIBLE_API_BASE_URL",
|
|
810
|
+
"OPENAI_COMPATIBLE_API_BASE_URL",
|
|
811
|
+
),
|
|
812
|
+
),
|
|
793
813
|
}
|
|
794
814
|
return {key: value for key, value in values.items() if value}
|
|
795
815
|
|
|
@@ -47,6 +47,7 @@ dify_player/nodes/http.py
|
|
|
47
47
|
dify_player/nodes/if_else.py
|
|
48
48
|
dify_player/nodes/llm_azure_chat.py
|
|
49
49
|
dify_player/nodes/llm_groq_chat.py
|
|
50
|
+
dify_player/nodes/llm_openai_compatible_chat.py
|
|
50
51
|
dify_player/nodes/llm_xai_chat.py
|
|
51
52
|
dify_player/nodes/start.py
|
|
52
53
|
dify_player/nodes/template.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/__init__.py
RENAMED
|
File without changes
|
{dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/assigner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/http_request.py
RENAMED
|
File without changes
|
{dify_player-0.3.8 → dify_player-0.3.9}/dify_player/dify_importer/node_converters/if_else.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|