Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ just test # Run all tests
just build # Build source and wheel distributions
```

## LLM Polling Support

SDK 0.9.0 adds support for polling-based LLM invocation. A model can now
declare the `polling` feature and implement polling methods, allowing plugins
to submit long-running provider jobs and return later checks through a short
request/response flow.

Polling results use three states:

- `running` returns plugin-owned state for the next check.
- `succeeded` returns the final LLM result.
- `failed` returns a terminal error.

## Version Management

This SDK follows Semantic Versioning (a.b.c):
Expand Down Expand Up @@ -73,3 +86,4 @@ For the manifest specification, we've introduced two versioning fields:
| 1.10.0 | 0.6.0 | Support Trigger functionality for plugins |
| 1.11.0 | 0.7.0 | Support Multimodal Reranking / Embeddings |
| 1.14.0 | 0.8.1 | Dependency and project structure cleanup |
| 1.14.2 | 0.9.0 | Support polling-based LLM plugin invocations |
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = 'dify_plugin'
version = '0.8.1'
version = '0.9.0'
description = 'Dify Plugin SDK'
authors = [{ name = 'langgenius', email = 'hello@dify.ai' }]
dependencies = [
Expand Down
23 changes: 21 additions & 2 deletions src/dify_plugin/core/entities/plugin/request.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from collections.abc import Mapping, Sequence
from enum import StrEnum
from typing import Any
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic import BaseModel, ConfigDict, Field, JsonValue, field_validator

from dify_plugin.entities.datasource import (
GetOnlineDocumentPageContentRequest,
Expand Down Expand Up @@ -58,6 +58,8 @@ class ModelActions(StrEnum):
ValidateProviderCredentials = "validate_provider_credentials"
ValidateModelCredentials = "validate_model_credentials"
InvokeLLM = "invoke_llm"
StartPolling = "start_polling"
CheckPolling = "check_polling"
GetLLMNumTokens = "get_llm_num_tokens"
InvokeTextEmbedding = "invoke_text_embedding"
InvokeMultimodalEmbedding = "invoke_multimodal_embedding"
Expand Down Expand Up @@ -193,11 +195,28 @@ class ModelInvokeLLMRequest(PluginAccessModelRequest, PromptMessageMixin):
model_parameters: dict[str, Any]
stop: list[str] | None
tools: list[PromptMessageTool] | None
json_schema: dict[str, JsonValue] | None = None
stream: bool = True

model_config = ConfigDict(protected_namespaces=())


class ModelStartPollingRequest(ModelInvokeLLMRequest):
action: ModelActions = ModelActions.StartPolling
stream: Literal[False] = False

workflow_run_id: str
node_id: str


class ModelCheckPollingRequest(PluginAccessModelRequest):
action: ModelActions = ModelActions.CheckPolling

workflow_run_id: str
node_id: str
plugin_state: dict[str, JsonValue]


class ModelGetLLMNumTokens(PluginAccessModelRequest, PromptMessageMixin):
action: ModelActions = ModelActions.GetLLMNumTokens

Expand Down
71 changes: 71 additions & 0 deletions src/dify_plugin/core/plugin_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
DatasourceValidateCredentialsRequest,
DynamicParameterFetchParameterOptionsRequest,
EndpointInvokeRequest,
ModelCheckPollingRequest,
ModelGetAIModelSchemas,
ModelGetLLMNumTokens,
ModelGetTextEmbeddingNumTokens,
Expand All @@ -29,6 +30,7 @@
ModelInvokeSpeech2TextRequest,
ModelInvokeTextEmbeddingRequest,
ModelInvokeTTSRequest,
ModelStartPollingRequest,
ModelValidateModelCredentialsRequest,
ModelValidateProviderCredentialsRequest,
OAuthGetAuthorizationUrlRequest,
Expand Down Expand Up @@ -261,6 +263,75 @@ def invoke_llm(self, session: Session, data: ModelInvokeLLMRequest) -> object:
msg,
)

def start_llm_polling(
self,
session: Session,
data: ModelStartPollingRequest,
) -> object:
del session
model_instance = self.registration.get_model_instance(
data.provider,
data.model_type,
)
if not isinstance(model_instance, LargeLanguageModel):
msg = f"Model `{data.model_type}` not found for provider `{data.provider}`"
raise TypeError(
msg,
)

if not model_instance.supports_polling(data.model, data.credentials):
msg = (
f"Model `{data.model}` for provider `{data.provider}` "
"does not support polling"
)
raise ValueError(msg)

return model_instance.start_polling(
model=data.model,
credentials=data.credentials,
prompt_messages=data.prompt_messages,
model_parameters=data.model_parameters,
tools=data.tools,
stop=data.stop,
stream=data.stream,
user=data.user_id,
json_schema=data.json_schema,
workflow_run_id=data.workflow_run_id,
node_id=data.node_id,
)

def check_llm_polling(
self,
session: Session,
data: ModelCheckPollingRequest,
) -> object:
del session
model_instance = self.registration.get_model_instance(
data.provider,
data.model_type,
)
if not isinstance(model_instance, LargeLanguageModel):
msg = f"Model `{data.model_type}` not found for provider `{data.provider}`"
raise TypeError(
msg,
)

if not model_instance.supports_polling(data.model, data.credentials):
msg = (
f"Model `{data.model}` for provider `{data.provider}` "
"does not support polling"
)
raise ValueError(msg)

return model_instance.check_polling(
model=data.model,
credentials=data.credentials,
plugin_state=data.plugin_state,
user=data.user_id,
workflow_run_id=data.workflow_run_id,
node_id=data.node_id,
)

def get_llm_num_tokens(
self,
session: Session,
Expand Down
48 changes: 45 additions & 3 deletions src/dify_plugin/entities/model/llm.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from collections.abc import Mapping
from decimal import Decimal
from enum import Enum

from pydantic import BaseModel, ConfigDict, Field, field_validator
from enum import Enum, StrEnum

from pydantic import (
BaseModel,
ConfigDict,
Field,
JsonValue,
PositiveInt,
field_validator,
model_validator,
)

from dify_plugin.entities.model import BaseModelConfig, ModelType, ModelUsage, PriceInfo
from dify_plugin.entities.model.message import (
Expand Down Expand Up @@ -37,6 +45,12 @@ def value_of(cls, value: str) -> "LLMMode":
raise ValueError(msg)


class LLMPollingStatus(StrEnum):
RUNNING = "running"
SUCCEEDED = "succeeded"
FAILED = "failed"


class LLMUsage(ModelUsage):
"""Model class for llm usage."""

Expand Down Expand Up @@ -174,6 +188,34 @@ def to_llm_result_chunk_with_structured_output(
)


class LLMPollingResult(BaseModel):
"""Model class for llm polling result."""

status: LLMPollingStatus
plugin_state: dict[str, JsonValue] | None = None
result: LLMResult | LLMResultWithStructuredOutput | None = None
error: str | None = None
next_check_after_seconds: PositiveInt | None = None
expires_after_seconds: PositiveInt | None = None
max_attempts: PositiveInt | None = None

@model_validator(mode="after")
def validate_status_payload(self) -> "LLMPollingResult":
if self.status == LLMPollingStatus.RUNNING and self.plugin_state is None:
msg = "plugin_state is required when polling status is running."
raise ValueError(msg)

if self.status == LLMPollingStatus.SUCCEEDED and self.result is None:
msg = "result is required when polling status is succeeded."
raise ValueError(msg)

if self.status == LLMPollingStatus.FAILED and not self.error:
msg = "error is required when polling status is failed."
raise ValueError(msg)

return self


class SummaryResult(BaseModel):
"""Model class for summary result."""

Expand Down
1 change: 1 addition & 0 deletions src/dify_plugin/entities/model/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ class ModelFeature(Enum):
VIDEO = "video"
AUDIO = "audio"
STRUCTURED_OUTPUT = "structured-output"
POLLING = "polling"


@docs(
Expand Down
Loading