Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

streamable_http.py 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import json
  2. import logging
  3. from collections.abc import Mapping
  4. from typing import Any, cast
  5. from configs import dify_config
  6. from core.app.app_config.entities import VariableEntity, VariableEntityType
  7. from core.app.entities.app_invoke_entities import InvokeFrom
  8. from core.app.features.rate_limiting.rate_limit import RateLimitGenerator
  9. from core.mcp import types as mcp_types
  10. from models.model import App, AppMCPServer, AppMode, EndUser
  11. from services.app_generate_service import AppGenerateService
  12. logger = logging.getLogger(__name__)
  13. def handle_mcp_request(
  14. app: App,
  15. request: mcp_types.ClientRequest,
  16. user_input_form: list[VariableEntity],
  17. mcp_server: AppMCPServer,
  18. end_user: EndUser | None = None,
  19. request_id: int | str = 1,
  20. ) -> mcp_types.JSONRPCResponse | mcp_types.JSONRPCError:
  21. """
  22. Handle MCP request and return JSON-RPC response
  23. Args:
  24. app: The Dify app instance
  25. request: The JSON-RPC request message
  26. user_input_form: List of variable entities for the app
  27. mcp_server: The MCP server configuration
  28. end_user: Optional end user
  29. request_id: The request ID
  30. Returns:
  31. JSON-RPC response or error
  32. """
  33. request_type = type(request.root)
  34. def create_success_response(result_data: mcp_types.Result) -> mcp_types.JSONRPCResponse:
  35. """Create success response with business result data"""
  36. return mcp_types.JSONRPCResponse(
  37. jsonrpc="2.0",
  38. id=request_id,
  39. result=result_data.model_dump(by_alias=True, mode="json", exclude_none=True),
  40. )
  41. def create_error_response(code: int, message: str) -> mcp_types.JSONRPCError:
  42. """Create error response with error code and message"""
  43. from core.mcp.types import ErrorData
  44. error_data = ErrorData(code=code, message=message)
  45. return mcp_types.JSONRPCError(
  46. jsonrpc="2.0",
  47. id=request_id,
  48. error=error_data,
  49. )
  50. # Request handler mapping using functional approach
  51. request_handlers = {
  52. mcp_types.InitializeRequest: lambda: handle_initialize(mcp_server.description),
  53. mcp_types.ListToolsRequest: lambda: handle_list_tools(
  54. app.name, app.mode, user_input_form, mcp_server.description, mcp_server.parameters_dict
  55. ),
  56. mcp_types.CallToolRequest: lambda: handle_call_tool(app, request, user_input_form, end_user),
  57. mcp_types.PingRequest: lambda: handle_ping(),
  58. }
  59. try:
  60. # Dispatch request to appropriate handler
  61. handler = request_handlers.get(request_type)
  62. if handler:
  63. return create_success_response(handler())
  64. else:
  65. return create_error_response(mcp_types.METHOD_NOT_FOUND, f"Method not found: {request_type.__name__}")
  66. except ValueError as e:
  67. logger.exception("Invalid params")
  68. return create_error_response(mcp_types.INVALID_PARAMS, str(e))
  69. except Exception as e:
  70. logger.exception("Internal server error")
  71. return create_error_response(mcp_types.INTERNAL_ERROR, "Internal server error: " + str(e))
  72. def handle_ping() -> mcp_types.EmptyResult:
  73. """Handle ping request"""
  74. return mcp_types.EmptyResult()
  75. def handle_initialize(description: str) -> mcp_types.InitializeResult:
  76. """Handle initialize request"""
  77. capabilities = mcp_types.ServerCapabilities(
  78. tools=mcp_types.ToolsCapability(listChanged=False),
  79. )
  80. return mcp_types.InitializeResult(
  81. protocolVersion=mcp_types.SERVER_LATEST_PROTOCOL_VERSION,
  82. capabilities=capabilities,
  83. serverInfo=mcp_types.Implementation(name="Dify", version=dify_config.project.version),
  84. instructions=description,
  85. )
  86. def handle_list_tools(
  87. app_name: str,
  88. app_mode: str,
  89. user_input_form: list[VariableEntity],
  90. description: str,
  91. parameters_dict: dict[str, str],
  92. ) -> mcp_types.ListToolsResult:
  93. """Handle list tools request"""
  94. parameter_schema = build_parameter_schema(app_mode, user_input_form, parameters_dict)
  95. return mcp_types.ListToolsResult(
  96. tools=[
  97. mcp_types.Tool(
  98. name=app_name,
  99. description=description,
  100. inputSchema=parameter_schema,
  101. )
  102. ],
  103. )
  104. def handle_call_tool(
  105. app: App,
  106. request: mcp_types.ClientRequest,
  107. user_input_form: list[VariableEntity],
  108. end_user: EndUser | None,
  109. ) -> mcp_types.CallToolResult:
  110. """Handle call tool request"""
  111. request_obj = cast(mcp_types.CallToolRequest, request.root)
  112. args = prepare_tool_arguments(app, request_obj.params.arguments or {})
  113. if not end_user:
  114. raise ValueError("End user not found")
  115. response = AppGenerateService.generate(
  116. app,
  117. end_user,
  118. args,
  119. InvokeFrom.SERVICE_API,
  120. streaming=app.mode == AppMode.AGENT_CHAT.value,
  121. )
  122. answer = extract_answer_from_response(app, response)
  123. return mcp_types.CallToolResult(content=[mcp_types.TextContent(text=answer, type="text")])
  124. def build_parameter_schema(
  125. app_mode: str,
  126. user_input_form: list[VariableEntity],
  127. parameters_dict: dict[str, str],
  128. ) -> dict[str, Any]:
  129. """Build parameter schema for the tool"""
  130. parameters, required = convert_input_form_to_parameters(user_input_form, parameters_dict)
  131. if app_mode in {AppMode.COMPLETION.value, AppMode.WORKFLOW.value}:
  132. return {
  133. "type": "object",
  134. "properties": parameters,
  135. "required": required,
  136. }
  137. return {
  138. "type": "object",
  139. "properties": {
  140. "query": {"type": "string", "description": "User Input/Question content"},
  141. **parameters,
  142. },
  143. "required": ["query", *required],
  144. }
  145. def prepare_tool_arguments(app: App, arguments: dict[str, Any]) -> dict[str, Any]:
  146. """Prepare arguments based on app mode"""
  147. if app.mode == AppMode.WORKFLOW.value:
  148. return {"inputs": arguments}
  149. elif app.mode == AppMode.COMPLETION.value:
  150. return {"query": "", "inputs": arguments}
  151. else:
  152. # Chat modes - create a copy to avoid modifying original dict
  153. args_copy = arguments.copy()
  154. query = args_copy.pop("query", "")
  155. return {"query": query, "inputs": args_copy}
  156. def extract_answer_from_response(app: App, response: Any) -> str:
  157. """Extract answer from app generate response"""
  158. answer = ""
  159. if isinstance(response, RateLimitGenerator):
  160. answer = process_streaming_response(response)
  161. elif isinstance(response, Mapping):
  162. answer = process_mapping_response(app, response)
  163. else:
  164. logger.warning("Unexpected response type: %s", type(response))
  165. return answer
  166. def process_streaming_response(response: RateLimitGenerator) -> str:
  167. """Process streaming response for agent chat mode"""
  168. answer = ""
  169. for item in response.generator:
  170. if isinstance(item, str) and item.startswith("data: "):
  171. try:
  172. json_str = item[6:].strip()
  173. parsed_data = json.loads(json_str)
  174. if parsed_data.get("event") == "agent_thought":
  175. answer += parsed_data.get("thought", "")
  176. except json.JSONDecodeError:
  177. continue
  178. return answer
  179. def process_mapping_response(app: App, response: Mapping) -> str:
  180. """Process mapping response based on app mode"""
  181. if app.mode in {
  182. AppMode.ADVANCED_CHAT.value,
  183. AppMode.COMPLETION.value,
  184. AppMode.CHAT.value,
  185. AppMode.AGENT_CHAT.value,
  186. }:
  187. return response.get("answer", "")
  188. elif app.mode == AppMode.WORKFLOW.value:
  189. return json.dumps(response["data"]["outputs"], ensure_ascii=False)
  190. else:
  191. raise ValueError("Invalid app mode: " + str(app.mode))
  192. def convert_input_form_to_parameters(
  193. user_input_form: list[VariableEntity],
  194. parameters_dict: dict[str, str],
  195. ) -> tuple[dict[str, dict[str, Any]], list[str]]:
  196. """Convert user input form to parameter schema"""
  197. parameters: dict[str, dict[str, Any]] = {}
  198. required = []
  199. for item in user_input_form:
  200. if item.type in (
  201. VariableEntityType.FILE,
  202. VariableEntityType.FILE_LIST,
  203. VariableEntityType.EXTERNAL_DATA_TOOL,
  204. ):
  205. continue
  206. parameters[item.variable] = {}
  207. if item.required:
  208. required.append(item.variable)
  209. # if the workflow republished, the parameters not changed
  210. # we should not raise error here
  211. description = parameters_dict.get(item.variable, "")
  212. parameters[item.variable]["description"] = description
  213. if item.type in (VariableEntityType.TEXT_INPUT, VariableEntityType.PARAGRAPH):
  214. parameters[item.variable]["type"] = "string"
  215. elif item.type == VariableEntityType.SELECT:
  216. parameters[item.variable]["type"] = "string"
  217. parameters[item.variable]["enum"] = item.options
  218. elif item.type == VariableEntityType.NUMBER:
  219. parameters[item.variable]["type"] = "number"
  220. return parameters, required