mirror of
https://github.com/langgenius/dify.git
synced 2026-10-24 12:04:30 +08:00
feat(api): LLM polling support (#37462)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: WH-2099 <wh2099@pm.me>
This commit is contained in:
co-authored by
autofix-ci[bot]
WH-2099
parent
19838972dc
commit
f0b34bdeb4
@@ -7,6 +7,7 @@ from pytest_mock import MockerFixture
|
||||
from core.plugin.endpoint.exc import EndpointSetupFailedError
|
||||
from core.plugin.entities.plugin_daemon import PluginDaemonInnerError
|
||||
from core.plugin.impl.base import PLUGIN_DAEMON_MAX_PATH_LENGTH, BasePluginClient
|
||||
from core.plugin.impl.exc import PluginLLMPollingUnsupportedError
|
||||
from core.trigger.errors import (
|
||||
EventIgnoreError,
|
||||
TriggerInvokeError,
|
||||
@@ -167,3 +168,10 @@ class TestBasePluginClientImpl:
|
||||
|
||||
with pytest.raises(expected):
|
||||
client._handle_plugin_daemon_error("PluginInvokeError", message)
|
||||
|
||||
def test_handle_plugin_daemon_error_maps_unsupported_polling_to_typed_exception(self):
|
||||
client = BasePluginClient()
|
||||
message = json.dumps({"error_type": PluginLLMPollingUnsupportedError.__name__, "message": "m"})
|
||||
|
||||
with pytest.raises(PluginLLMPollingUnsupportedError):
|
||||
client._handle_plugin_daemon_error("PluginInvokeError", message)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from core.plugin.entities.plugin_daemon import PluginDaemonInnerError
|
||||
from core.plugin.impl.exc import PluginInvokeError, PluginLLMPollingUnsupportedError
|
||||
from core.plugin.impl.model import PluginModelClient
|
||||
from graphon.model_runtime.entities.llm_entities import LLMPollingResult, LLMPollingStatus, LLMResult, LLMUsage
|
||||
from graphon.model_runtime.entities.message_entities import AssistantPromptMessage
|
||||
|
||||
|
||||
class TestPluginModelClient:
|
||||
@@ -183,6 +187,113 @@ class TestPluginModelClient:
|
||||
)
|
||||
)
|
||||
|
||||
def test_start_llm_polling(self, mocker: MockerFixture):
|
||||
client = PluginModelClient()
|
||||
polling_result = LLMPollingResult(
|
||||
status=LLMPollingStatus.RUNNING,
|
||||
plugin_state={"task_id": "poll-1"},
|
||||
next_check_after_seconds=3,
|
||||
)
|
||||
request_mock = mocker.patch.object(
|
||||
client,
|
||||
"_request_with_plugin_daemon_response",
|
||||
return_value=polling_result,
|
||||
)
|
||||
|
||||
result = client.start_llm_polling(
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
plugin_id="org/plugin:1",
|
||||
provider="provider-a",
|
||||
model="gpt-test",
|
||||
credentials={"api_key": "key"},
|
||||
prompt_messages=[],
|
||||
model_parameters={"temperature": 0.1},
|
||||
tools=[],
|
||||
stop=["STOP"],
|
||||
json_schema={"type": "object"},
|
||||
)
|
||||
|
||||
assert result == polling_result
|
||||
call_kwargs = request_mock.call_args.kwargs
|
||||
assert call_kwargs["path"] == "plugin/tenant-1/dispatch/model/polling/start"
|
||||
assert call_kwargs["data"]["data"] == {
|
||||
"provider": "provider-a",
|
||||
"model_type": "llm",
|
||||
"model": "gpt-test",
|
||||
"credentials": {"api_key": "key"},
|
||||
"prompt_messages": [],
|
||||
"model_parameters": {"temperature": 0.1},
|
||||
"tools": [],
|
||||
"stop": ["STOP"],
|
||||
"stream": False,
|
||||
"json_schema": {"type": "object"},
|
||||
}
|
||||
|
||||
def test_check_llm_polling(self, mocker: MockerFixture):
|
||||
client = PluginModelClient()
|
||||
polling_result = LLMPollingResult(
|
||||
status=LLMPollingStatus.SUCCEEDED,
|
||||
result=LLMResult(
|
||||
model="gpt-test",
|
||||
prompt_messages=[],
|
||||
message=AssistantPromptMessage(content="done"),
|
||||
usage=LLMUsage.empty_usage(),
|
||||
),
|
||||
)
|
||||
request_mock = mocker.patch.object(
|
||||
client,
|
||||
"_request_with_plugin_daemon_response",
|
||||
return_value=polling_result,
|
||||
)
|
||||
|
||||
result = client.check_llm_polling(
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
plugin_id="org/plugin:1",
|
||||
provider="provider-a",
|
||||
model="gpt-test",
|
||||
credentials={"api_key": "key"},
|
||||
plugin_state={"task_id": "poll-1"},
|
||||
)
|
||||
|
||||
assert result == polling_result
|
||||
call_kwargs = request_mock.call_args.kwargs
|
||||
assert call_kwargs["path"] == "plugin/tenant-1/dispatch/model/polling/check"
|
||||
assert call_kwargs["data"]["data"] == {
|
||||
"provider": "provider-a",
|
||||
"model_type": "llm",
|
||||
"model": "gpt-test",
|
||||
"credentials": {"api_key": "key"},
|
||||
"plugin_state": {"task_id": "poll-1"},
|
||||
}
|
||||
|
||||
def test_start_llm_polling_maps_unsupported_polling_invoke_error(self, mocker: MockerFixture):
|
||||
client = PluginModelClient()
|
||||
mocker.patch.object(
|
||||
client,
|
||||
"_request_with_plugin_daemon_response",
|
||||
side_effect=PluginInvokeError(
|
||||
json.dumps(
|
||||
{
|
||||
"error_type": PluginLLMPollingUnsupportedError.__name__,
|
||||
"message": "Model `gpt-test` does not support polling.",
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(PluginLLMPollingUnsupportedError):
|
||||
client.start_llm_polling(
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
plugin_id="org/plugin:1",
|
||||
provider="provider-a",
|
||||
model="gpt-test",
|
||||
credentials={"api_key": "key"},
|
||||
prompt_messages=[],
|
||||
)
|
||||
|
||||
def test_get_llm_num_tokens(self, mocker: MockerFixture):
|
||||
client = PluginModelClient()
|
||||
mocker.patch.object(
|
||||
|
||||
@@ -14,7 +14,13 @@ from core.plugin.impl.model_runtime import TENANT_SCOPE_SCHEMA_CACHE_USER_ID, Pl
|
||||
from core.plugin.impl.model_runtime_factory import create_plugin_model_runtime
|
||||
from core.plugin.plugin_service import PluginService
|
||||
from graphon.model_runtime.entities.common_entities import I18nObject
|
||||
from graphon.model_runtime.entities.llm_entities import LLMResultChunk, LLMResultChunkDelta, LLMUsage
|
||||
from graphon.model_runtime.entities.llm_entities import (
|
||||
LLMPollingResult,
|
||||
LLMPollingStatus,
|
||||
LLMResultChunk,
|
||||
LLMResultChunkDelta,
|
||||
LLMUsage,
|
||||
)
|
||||
from graphon.model_runtime.entities.message_entities import AssistantPromptMessage
|
||||
from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType
|
||||
from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity
|
||||
@@ -282,6 +288,74 @@ class TestPluginModelRuntime:
|
||||
stream=True,
|
||||
)
|
||||
|
||||
def test_start_llm_polling_resolves_plugin_fields(self) -> None:
|
||||
client = Mock(spec=PluginModelClient)
|
||||
polling_result = LLMPollingResult(
|
||||
status=LLMPollingStatus.RUNNING,
|
||||
plugin_state={"task_id": "poll-1"},
|
||||
next_check_after_seconds=2,
|
||||
)
|
||||
client.start_llm_polling.return_value = polling_result
|
||||
runtime = PluginModelRuntime(tenant_id="tenant", user_id="user", client=client, plugin_service=PluginService)
|
||||
|
||||
result = runtime.start_llm_polling(
|
||||
provider="langgenius/openai/openai",
|
||||
model="gpt-4o-mini",
|
||||
credentials={"api_key": "secret"},
|
||||
model_parameters={"temperature": 0.2},
|
||||
prompt_messages=[],
|
||||
tools=None,
|
||||
stop=("END",),
|
||||
json_schema={"type": "object"},
|
||||
)
|
||||
|
||||
assert result == polling_result
|
||||
client.start_llm_polling.assert_called_once_with(
|
||||
tenant_id="tenant",
|
||||
user_id="user",
|
||||
plugin_id="langgenius/openai",
|
||||
provider="openai",
|
||||
model="gpt-4o-mini",
|
||||
credentials={"api_key": "secret"},
|
||||
prompt_messages=[],
|
||||
model_parameters={"temperature": 0.2},
|
||||
tools=None,
|
||||
stop=["END"],
|
||||
json_schema={"type": "object"},
|
||||
)
|
||||
|
||||
def test_check_llm_polling_resolves_plugin_fields(self) -> None:
|
||||
client = Mock(spec=PluginModelClient)
|
||||
polling_result = LLMPollingResult(
|
||||
status=LLMPollingStatus.SUCCEEDED,
|
||||
result=model_runtime_module.LLMResult(
|
||||
model="gpt-4o-mini",
|
||||
prompt_messages=[],
|
||||
message=AssistantPromptMessage(content="done"),
|
||||
usage=LLMUsage.empty_usage(),
|
||||
),
|
||||
)
|
||||
client.check_llm_polling.return_value = polling_result
|
||||
runtime = PluginModelRuntime(tenant_id="tenant", user_id="user", client=client, plugin_service=PluginService)
|
||||
|
||||
result = runtime.check_llm_polling(
|
||||
provider="langgenius/openai/openai",
|
||||
model="gpt-4o-mini",
|
||||
credentials={"api_key": "secret"},
|
||||
plugin_state={"task_id": "poll-1"},
|
||||
)
|
||||
|
||||
assert result == polling_result
|
||||
client.check_llm_polling.assert_called_once_with(
|
||||
tenant_id="tenant",
|
||||
user_id="user",
|
||||
plugin_id="langgenius/openai",
|
||||
provider="openai",
|
||||
model="gpt-4o-mini",
|
||||
credentials={"api_key": "secret"},
|
||||
plugin_state={"task_id": "poll-1"},
|
||||
)
|
||||
|
||||
def test_invoke_llm_rejects_per_call_user_override(self) -> None:
|
||||
client = Mock(spec=PluginModelClient)
|
||||
client.invoke_llm.return_value = sentinel.result
|
||||
|
||||
Reference in New Issue
Block a user