Files
dify-plugin-sdks/python/dify_plugin/core/runtime/request.py
T
2024-08-27 17:09:38 +08:00

568 lines
16 KiB
Python

from binascii import hexlify, unhexlify
from collections.abc import Generator
import json
from typing import IO, Any, Literal, Optional, Type, overload
import uuid
import httpx
from pydantic import BaseModel
from yarl import URL
from dify_plugin.config.config import InstallMethod
from dify_plugin.core.entities.backwards_invocation.response_event import (
BackwardsInvocationResponseEvent,
)
from dify_plugin.core.entities.model_runtime.llm import (
LLMResult,
LLMResultChunk,
)
from dify_plugin.core.entities.model_runtime.message import (
PromptMessage,
PromptMessageTool,
)
from dify_plugin.core.entities.model_runtime.model_config import (
LLMModelConfig,
ModerationModelConfig,
RerankModelConfig,
Speech2TextModelConfig,
TTSModelConfig,
)
from dify_plugin.core.entities.model_runtime.moderation import ModerationResult
from dify_plugin.core.entities.model_runtime.rerank import RerankResult
from dify_plugin.core.entities.model_runtime.speech2text import (
Speech2TextResult,
)
from dify_plugin.core.entities.model_runtime.text_embedding import (
TextEmbeddingResult,
)
from dify_plugin.core.entities.model_runtime.tts import TTSResult
from dify_plugin.core.entities.plugin.dify import ToolProviderType
from dify_plugin.core.entities.plugin.invocation import InvokeType
from dify_plugin.core.entities.plugin.io import (
PluginInStream,
PluginInStreamBase,
PluginInStreamEvent,
)
from dify_plugin.core.entities.plugin.workflow import (
KnowledgeRetrievalNodeData,
NodeResponse,
ParameterExtractorNodeData,
QuestionClassifierNodeData,
)
from dify_plugin.core.runtime.abstract.request import AbstractRequestInterface
from dify_plugin.core.runtime.session import Session
from dify_plugin.tool.entities import ToolInvokeMessage
class RequestInterface(AbstractRequestInterface):
def __init__(
self,
session: Optional[Session] = None,
) -> None:
self.session = session
def _generate_backwards_request_id(self):
return uuid.uuid4().hex
def _backwards_invoke[T: BaseModel | dict](
self,
type: InvokeType,
data_type: Type[T],
data: dict,
) -> Generator[T, None, None]:
"""
backwards invoke dify depends on current runtime type
"""
backwards_request_id = self._generate_backwards_request_id()
if not self.session:
raise Exception("current tool runtime does not support backwards invoke")
if self.session.install_method in [InstallMethod.Local, InstallMethod.Remote]:
return self._full_duplex_backwards_invoke(
backwards_request_id, type, data_type, data
)
return self._http_backwards_invoke(backwards_request_id, type, data_type, data)
def _line_converter_wrapper[T: BaseModel | dict](
self,
generator: Generator[PluginInStreamBase | None, None, None],
data_type: Type[T],
) -> Generator[T, None, None]:
"""
convert string into type T
"""
empty_response_count = 0
for chunk in generator:
"""
accept response from input stream and wait for at most 60 seconds
"""
if chunk is None:
empty_response_count += 1
# if no response for 250 seconds, break
if empty_response_count >= 250:
raise Exception("invocation exited without response")
continue
event = BackwardsInvocationResponseEvent(**chunk.data)
if event.event == BackwardsInvocationResponseEvent.Event.End:
break
if event.event == BackwardsInvocationResponseEvent.Event.Error:
raise Exception(event.message)
if event.data is None:
break
empty_response_count = 0
try:
if data_type == dict:
event.data
else:
yield data_type(**event.data)
except Exception as e:
raise Exception(f"Failed to parse response: {str(e)}")
def _http_backwards_invoke[T: BaseModel | dict](
self,
backwards_request_id: str,
type: InvokeType,
data_type: Type[T],
data: dict,
) -> Generator[T, None, None]:
"""
http backwards invoke
"""
if not self.session or not self.session.dify_plugin_daemon_url:
raise Exception("current tool runtime does not support backwards invoke")
url = (
URL(self.session.dify_plugin_daemon_url)
/ "backwards-invocation"
/ "transaction"
)
headers = {
"Dify-Plugin-Session-ID": self.session.session_id,
}
payload = self.session.writer.session_message_text(
session_id=self.session.session_id,
data=self.session.writer.stream_invoke_object(
data={
"type": type.value,
"backwards_request_id": backwards_request_id,
"request": data,
}
),
)
with httpx.Client() as client:
with client.stream(
method="POST",
url=str(url),
headers=headers,
content=payload,
) as response:
def generator():
for line in response.iter_lines():
data = json.loads(line)
yield PluginInStreamBase(
session_id=data["session_id"],
event=PluginInStreamEvent.value_of(data["event"]),
data=data["data"],
)
return self._line_converter_wrapper(generator(), data_type)
def _full_duplex_backwards_invoke[T: BaseModel | dict](
self,
backwards_request_id: str,
type: InvokeType,
data_type: Type[T],
data: dict,
) -> Generator[T, None, None]:
if not self.session:
raise Exception("current tool runtime does not support backwards invoke")
self.session.writer.session_message(
session_id=self.session.session_id,
data=self.session.writer.stream_invoke_object(
data={
"type": type.value,
"backwards_request_id": backwards_request_id,
"request": data,
}
),
)
def filter(data: PluginInStream) -> bool:
return (
data.event == PluginInStreamEvent.BackwardInvocationResponse
and data.data.get("backwards_request_id") == backwards_request_id
)
with self.session.reader.read(filter) as reader:
return self._line_converter_wrapper(
reader.read(timeout_for_round=1), data_type
)
def invoke_tool(
self,
provider_type: ToolProviderType,
provider: str,
tool_name: str,
parameters: dict[str, Any],
) -> Generator[ToolInvokeMessage, None, None]:
"""
Invoke tool
"""
return self._backwards_invoke(
InvokeType.Tool,
ToolInvokeMessage,
{
"provider_type": provider_type.value,
"provider": provider,
"tool": tool_name,
"tool_parameters": parameters,
},
)
def invoke_builtin_tool(
self, provider: str, tool_name: str, parameters: dict[str, Any]
) -> Generator[ToolInvokeMessage, None, None]:
"""
Invoke builtin tool
"""
return self.invoke_tool(
ToolProviderType.BUILT_IN, provider, tool_name, parameters
)
def invoke_workflow_tool(
self, provider: str, tool_name: str, parameters: dict[str, Any]
) -> Generator[ToolInvokeMessage, None, None]:
"""
Invoke workflow tool
"""
return self.invoke_tool(
ToolProviderType.WORKFLOW, provider, tool_name, parameters
)
def invoke_api_tool(
self, provider: str, tool_name: str, parameters: dict[str, Any]
) -> Generator[ToolInvokeMessage, None, None]:
"""
Invoke api tool
"""
return self.invoke_tool(ToolProviderType.API, provider, tool_name, parameters)
@overload
def invoke_llm(
self,
model_config: LLMModelConfig,
prompt_messages: list[PromptMessage],
tools: list[PromptMessageTool] | None = None,
stop: list[str] | None = None,
stream: Literal[True] = True,
) -> Generator[LLMResultChunk, None, None]: ...
@overload
def invoke_llm(
self,
model_config: LLMModelConfig,
prompt_messages: list[PromptMessage],
tools: list[PromptMessageTool] | None = None,
stop: list[str] | None = None,
stream: Literal[False] = False,
) -> LLMResult: ...
def invoke_llm(
self,
model_config: LLMModelConfig,
prompt_messages: list[PromptMessage],
tools: list[PromptMessageTool] | None = None,
stop: list[str] | None = None,
stream: bool = True,
) -> Generator[LLMResultChunk, None, None] | LLMResult:
"""
Invoke llm
"""
data = {
**model_config.model_dump(),
"prompt_messages": [message.model_dump() for message in prompt_messages],
"tools": [tool.model_dump() for tool in tools] if tools else None,
"stop": stop,
"stream": stream,
}
if stream:
return self._backwards_invoke(
InvokeType.LLM,
LLMResultChunk,
data,
)
for data in self._backwards_invoke(
InvokeType.LLM,
LLMResult,
data,
):
return data
raise Exception("No response from llm")
def invoke_text_embedding(
self, model_config: TextEmbeddingResult, texts: list[str]
) -> TextEmbeddingResult:
"""
Invoke text embedding
"""
for data in self._backwards_invoke(
InvokeType.TextEmbedding,
TextEmbeddingResult,
{
**model_config.model_dump(),
"texts": texts,
},
):
return data
raise Exception("No response from text embedding")
def invoke_rerank(
self, model_config: RerankModelConfig, docs: list[str], query: str
) -> RerankResult:
"""
Invoke rerank
"""
for data in self._backwards_invoke(
InvokeType.Rerank,
RerankResult,
{
**model_config.model_dump(),
"docs": docs,
"query": query,
},
):
return data
raise Exception("No response from rerank")
def invoke_speech2text(
self, model_config: Speech2TextModelConfig, file: IO[bytes]
) -> str:
"""
Invoke speech2text
"""
for data in self._backwards_invoke(
InvokeType.Speech2Text,
Speech2TextResult,
{
**model_config.model_dump(),
"file": hexlify(file.read()),
},
):
return data.result
raise Exception("No response from speech2text")
def invoke_moderation(self, model_config: ModerationModelConfig, text: str) -> bool:
"""
Invoke moderation
"""
for data in self._backwards_invoke(
InvokeType.Moderation,
ModerationResult,
{
**model_config.model_dump(),
"text": text,
},
):
return data.result
raise Exception("No response from moderation")
def invoke_tts(
self, model_config: TTSModelConfig, content_text: str
) -> Generator[bytes, None, None]:
"""
Invoke tts
"""
for data in self._backwards_invoke(
InvokeType.TTS,
TTSResult,
{
**model_config.model_dump(),
"content_text": content_text,
},
):
yield unhexlify(data.result)
@overload
def invoke_chat(
self,
app_id: str,
inputs: dict,
response_mode: Literal["streaming"],
conversation_id: str,
files: list,
) -> Generator[dict, None, None]: ...
@overload
def invoke_chat(
self,
app_id: str,
inputs: dict,
response_mode: Literal["blocking"],
conversation_id: str,
files: list,
) -> dict: ...
def invoke_chat(
self,
app_id: str,
inputs: dict,
response_mode: Literal["streaming", "blocking"],
conversation_id: str,
files: list,
) -> Generator[dict, None, None] | dict:
"""
Invoke chat app
"""
response = self._backwards_invoke(
InvokeType.App,
dict,
{
"app_id": app_id,
"inputs": inputs,
"response_mode": response_mode,
"conversation_id": conversation_id,
"files": files,
},
)
if response_mode == "streaming":
return response
for data in response:
return data
raise Exception("No response from chat")
@overload
def invoke_completion(
self,
app_id: str,
inputs: dict,
response_mode: Literal["streaming"],
files: list,
) -> Generator[dict, None, None]: ...
@overload
def invoke_completion(
self,
app_id: str,
inputs: dict,
response_mode: Literal["blocking"],
files: list,
) -> dict: ...
def invoke_completion(
self,
app_id: str,
inputs: dict,
response_mode: Literal["streaming", "blocking"],
files: list,
) -> Generator[dict, None, None] | dict:
"""
Invoke completion app
"""
response = self._backwards_invoke(
InvokeType.App,
dict,
{
"app_id": app_id,
"inputs": inputs,
"response_mode": response_mode,
"files": files,
},
)
if response_mode == "streaming":
return response
for data in response:
return data
raise Exception("No response from completion")
@overload
def invoke_workflow(
self,
app_id: str,
inputs: dict,
response_mode: Literal["streaming"],
files: list,
) -> Generator[dict, None, None]: ...
@overload
def invoke_workflow(
self,
app_id: str,
inputs: dict,
response_mode: Literal["blocking"],
files: list,
) -> dict: ...
def invoke_workflow(
self,
app_id: str,
inputs: dict,
response_mode: Literal["streaming", "blocking"],
files: list,
) -> Generator[dict, None, None] | dict:
"""
Invoke workflow app
"""
response = self._backwards_invoke(
InvokeType.App,
dict,
{
"app_id": app_id,
"inputs": inputs,
"response_mode": response_mode,
"files": files,
},
)
if response_mode == "streaming":
return response
for data in response:
return data
raise Exception("No response from workflow")
def invoke_question_classifier(
self, node_data: QuestionClassifierNodeData, inputs: dict
) -> NodeResponse:
"""
Invoke question classifier
"""
return super().invoke_question_classifier(node_data, inputs)
def invoke_parameter_extractor(
self, node_data: ParameterExtractorNodeData, inputs: dict
) -> NodeResponse:
"""
Invoke parameter extractor
"""
return super().invoke_parameter_extractor(node_data, inputs)
def invoke_knowledge_retrieval(
self, node_data: KnowledgeRetrievalNodeData, inputs: dict
) -> NodeResponse:
"""
Invoke knowledge retrieval
"""
return super().invoke_knowledge_retrieval(node_data, inputs)