dify_plugin 0.2.0__tar.gz → 0.2.2__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_plugin-0.2.0 → dify_plugin-0.2.2}/PKG-INFO +2 -2
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/README.md +1 -1
- dify_plugin-0.2.2/dify_plugin/cli.py +16 -0
- dify_plugin-0.2.2/dify_plugin/commands/__init__.py +3 -0
- dify_plugin-0.2.2/dify_plugin/commands/generate_docs.py +5 -0
- dify_plugin-0.2.2/dify_plugin/core/documentation/generator.py +416 -0
- dify_plugin-0.2.2/dify_plugin/core/documentation/schema_doc.py +78 -0
- dify_plugin-0.2.2/dify_plugin/core/entities/plugin/setup.py +169 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/plugin_executor.py +7 -1
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/stdio/request_reader.py +14 -6
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/tcp/request_reader.py +1 -2
- dify_plugin-0.2.2/dify_plugin/core/utils/http_parser.py +54 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/__init__.py +5 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/agent.py +47 -3
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/endpoint.py +27 -2
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/__init__.py +57 -26
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/provider.py +64 -6
- dify_plugin-0.2.2/dify_plugin/entities/oauth.py +17 -0
- dify_plugin-0.2.2/dify_plugin/entities/provider_config.py +116 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/tool.py +83 -67
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/file/file.py +14 -3
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/agent/__init__.py +3 -1
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/llm.py +56 -42
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/tool/__init__.py +17 -1
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/pyproject.toml +1 -1
- dify_plugin-0.2.2/tests/__init__.py +0 -0
- dify_plugin-0.2.2/tests/entities/endpoint/test_endpoint_group.py +53 -0
- dify_plugin-0.2.2/tests/entities/plugin/test_declaration.py +24 -0
- dify_plugin-0.2.2/tests/interfaces/model/__init__.py +0 -0
- dify_plugin-0.2.2/tests/interfaces/model/openai_compatible/__init__.py +0 -0
- dify_plugin-0.2.2/tests/interfaces/model/openai_compatible/test_increase_tool_call.py +99 -0
- dify_plugin-0.2.2/tests/servers/test_stdio.py +76 -0
- dify_plugin-0.2.2/tests/utils/test_http_parser.py +54 -0
- dify_plugin-0.2.0/dify_plugin/core/entities/plugin/setup.py +0 -101
- dify_plugin-0.2.0/dify_plugin/core/utils/http_parser.py +0 -14
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/LICENSE +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/config/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/config/config.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/config/logger_format.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/__init__.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/core/entities → dify_plugin-0.2.2/dify_plugin/core/documentation}/__init__.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/core/entities/plugin → dify_plugin-0.2.2/dify_plugin/core/entities}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/entities/invocation.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/entities/message.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/core/server/__base → dify_plugin-0.2.2/dify_plugin/core/entities/plugin}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/entities/plugin/io.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/entities/plugin/parameter_type.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/entities/plugin/request.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/plugin_registration.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/runtime.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/core/server → dify_plugin-0.2.2/dify_plugin/core/server/__base}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/__base/filter_reader.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/__base/request_reader.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/__base/response_writer.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/__base/writer_entities.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/core/server/serverless → dify_plugin-0.2.2/dify_plugin/core/server}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/io_server.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/router.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/core/server/stdio → dify_plugin-0.2.2/dify_plugin/core/server/serverless}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/serverless/request_reader.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/serverless/response_writer.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/core/server/tcp → dify_plugin-0.2.2/dify_plugin/core/server/stdio}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/stdio/response_writer.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/core/utils → dify_plugin-0.2.2/dify_plugin/core/server/tcp}/__init__.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/errors → dify_plugin-0.2.2/dify_plugin/core/utils}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/utils/class_loader.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/utils/position_helper.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/utils/yaml_loader.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/llm.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/message.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/moderation.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/rerank.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/speech2text.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/text_embedding.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/tts.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/workflow_node.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/file → dify_plugin-0.2.2/dify_plugin/errors}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/errors/model.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/errors/tool.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/interfaces → dify_plugin-0.2.2/dify_plugin/file}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/file/constants.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/file/entities.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/interfaces/model/openai_compatible → dify_plugin-0.2.2/dify_plugin/interfaces}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/endpoint/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/ai_model.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/audio.mp3 +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/large_language_model.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/moderation_model.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/invocations → dify_plugin-0.2.2/dify_plugin/interfaces/model/openai_compatible}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/common.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/provider.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/rerank.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/speech2text.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/text_embedding.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/tts.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/rerank_model.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/speech2text_model.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/text_embedding_model.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/tts_model.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/invocations/model → dify_plugin-0.2.2/dify_plugin/invocations}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/app/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/app/chat.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/app/completion.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/app/workflow.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/file.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/invocations/workflow_node → dify_plugin-0.2.2/dify_plugin/invocations/model}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/llm.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/moderation.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/rerank.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/speech2text.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/text_embedding.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/tts.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/storage.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/tool.py +0 -0
- {dify_plugin-0.2.0/dify_plugin/tool → dify_plugin-0.2.2/dify_plugin/invocations/workflow_node}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/workflow_node/parameter_extractor.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/workflow_node/question_classifier.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/plugin.py +0 -0
- {dify_plugin-0.2.0/tests → dify_plugin-0.2.2/dify_plugin/tool}/__init__.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/tool/entities.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/entities/models/test_llm.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/interfaces/agent/test_agent.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/invocations/test_storage.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/test_llm_result.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/test_prompt_message.py +0 -0
- {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/test_tool_call_model_init.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dify_plugin
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Dify Plugin SDK
|
|
5
5
|
Keywords: dify,plugin,sdk
|
|
6
6
|
Author-Email: langgenius <hello@dify.ai>
|
|
@@ -43,6 +43,6 @@ When depending on this SDK, it's recommended to specify version constraints that
|
|
|
43
43
|
Example in your project's dependency management:
|
|
44
44
|
|
|
45
45
|
```
|
|
46
|
-
dify_plugin>=0.
|
|
46
|
+
dify_plugin>=0.2.0,<0.3.0
|
|
47
47
|
```
|
|
48
48
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from dify_plugin.commands.generate_docs import generate_docs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(description="Dify Plugin SDK Documentation Generator")
|
|
8
|
+
parser.add_argument("command", choices=["generate-docs"], help="Command to run")
|
|
9
|
+
args = parser.parse_args()
|
|
10
|
+
|
|
11
|
+
if args.command == "generate-docs":
|
|
12
|
+
generate_docs()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from dify_plugin.core.documentation.schema_doc import list_schema_docs
|
|
8
|
+
from dify_plugin.core.entities import * # noqa: F403
|
|
9
|
+
from dify_plugin.core.entities.plugin import * # noqa: F403
|
|
10
|
+
from dify_plugin.core.entities.plugin.setup import * # noqa: F403
|
|
11
|
+
from dify_plugin.entities import * # noqa: F403
|
|
12
|
+
from dify_plugin.entities.agent import * # noqa: F403
|
|
13
|
+
from dify_plugin.entities.endpoint import * # noqa: F403
|
|
14
|
+
from dify_plugin.entities.model import * # noqa: F403
|
|
15
|
+
from dify_plugin.entities.model.llm import * # noqa: F403
|
|
16
|
+
from dify_plugin.entities.model.moderation import * # noqa: F403
|
|
17
|
+
from dify_plugin.entities.model.provider import * # noqa: F403
|
|
18
|
+
from dify_plugin.entities.model.rerank import * # noqa: F403
|
|
19
|
+
from dify_plugin.entities.model.speech2text import * # noqa: F403
|
|
20
|
+
from dify_plugin.entities.model.text_embedding import * # noqa: F403
|
|
21
|
+
from dify_plugin.entities.model.tts import * # noqa: F403
|
|
22
|
+
from dify_plugin.entities.tool import * # noqa: F403
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SchemaDocumentationGenerator:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._reference_counts: dict[type, int] = {}
|
|
28
|
+
self._reference_graph: dict[type, set[type]] = defaultdict(set)
|
|
29
|
+
self._processed_types: set[type] = set()
|
|
30
|
+
self._field_descriptions: dict[tuple[type, str], str] = {}
|
|
31
|
+
self._schema_descriptions: dict[type, str] = {}
|
|
32
|
+
self._processed_field_types: set[type] = set()
|
|
33
|
+
self._type_to_schema: dict[type, Any] = {}
|
|
34
|
+
self._type_blocks: dict[type, int] = {}
|
|
35
|
+
self._blocks: list[list] = []
|
|
36
|
+
self._types: set[type] = set()
|
|
37
|
+
|
|
38
|
+
def _organize_toc(self) -> list[tuple[type, list[Any]]]:
|
|
39
|
+
"""Organize types into a hierarchical structure for table of contents.
|
|
40
|
+
|
|
41
|
+
The hierarchy is built based on the following rules:
|
|
42
|
+
1. Types marked with top=True are placed at the root level first
|
|
43
|
+
2. Types referenced by multiple other types are placed at the root level
|
|
44
|
+
3. Types not referenced by any other type are placed at the root level
|
|
45
|
+
4. Types referenced by exactly one other type are placed as children of their parent type
|
|
46
|
+
5. This process continues recursively for each child type
|
|
47
|
+
|
|
48
|
+
This ensures that:
|
|
49
|
+
- Important types (marked with top=True) are easily accessible
|
|
50
|
+
- Types that are part of multiple other types are at root for easy access
|
|
51
|
+
- Types that belong to a single parent are properly nested
|
|
52
|
+
- The hierarchy reflects the actual reference relationships in the code
|
|
53
|
+
- Deep reference chains are properly represented (A -> B -> C shown as nested structure)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List[Tuple[Type, List[Any]]]: A list of tuples, where each tuple contains:
|
|
57
|
+
- A parent type
|
|
58
|
+
- A list of its child nodes, each being a tuple of (Type, List[Any])
|
|
59
|
+
"""
|
|
60
|
+
# Build a reverse reference map: type -> set of types that reference it
|
|
61
|
+
referenced_by = {t: set() for t in self._types}
|
|
62
|
+
for t, refs in self._reference_graph.items():
|
|
63
|
+
for ref in refs:
|
|
64
|
+
if ref in referenced_by:
|
|
65
|
+
referenced_by[ref].add(t)
|
|
66
|
+
|
|
67
|
+
def build_subtree(type_: type, processed: set[type]) -> tuple[type, list[Any]]:
|
|
68
|
+
"""Recursively build a subtree for a type and its references.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
type_: The type to build a subtree for
|
|
72
|
+
processed: Set of already processed types to avoid cycles
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Tuple[Type, List[Any]]: The type and its nested children
|
|
76
|
+
"""
|
|
77
|
+
if type_ in processed:
|
|
78
|
+
return type_, []
|
|
79
|
+
|
|
80
|
+
processed.add(type_)
|
|
81
|
+
children = []
|
|
82
|
+
|
|
83
|
+
# Find all types that are only referenced by this type
|
|
84
|
+
for ref_type in self._reference_graph.get(type_, set()):
|
|
85
|
+
# If this is the only reference to ref_type
|
|
86
|
+
refs = referenced_by.get(ref_type, set())
|
|
87
|
+
if len(refs) == 1 and next(iter(refs)) == type_:
|
|
88
|
+
subtree = build_subtree(ref_type, processed)
|
|
89
|
+
children.append(subtree)
|
|
90
|
+
|
|
91
|
+
return type_, children
|
|
92
|
+
|
|
93
|
+
# Start building the hierarchy
|
|
94
|
+
hierarchy = []
|
|
95
|
+
processed = set()
|
|
96
|
+
|
|
97
|
+
# Phase 1: Add types marked with top=True at the root level
|
|
98
|
+
for t in self._types:
|
|
99
|
+
if t not in processed and hasattr(t, "__schema_docs__") and any(doc.top for doc in t.__schema_docs__):
|
|
100
|
+
subtree = build_subtree(t, processed)
|
|
101
|
+
hierarchy.append(subtree)
|
|
102
|
+
|
|
103
|
+
# Phase 2: Add types that are not referenced by any other type
|
|
104
|
+
# or are referenced by multiple types
|
|
105
|
+
remaining = [t for t in self._types if len(referenced_by[t]) != 1]
|
|
106
|
+
for t in remaining:
|
|
107
|
+
if t not in processed:
|
|
108
|
+
subtree = build_subtree(t, processed)
|
|
109
|
+
hierarchy.append(subtree)
|
|
110
|
+
|
|
111
|
+
# Phase 3: Add any remaining types that weren't processed
|
|
112
|
+
for t in self._types:
|
|
113
|
+
if t not in processed:
|
|
114
|
+
subtree = build_subtree(t, processed)
|
|
115
|
+
hierarchy.append(subtree)
|
|
116
|
+
|
|
117
|
+
return hierarchy
|
|
118
|
+
|
|
119
|
+
def generate_docs(self, output_file: str):
|
|
120
|
+
with open(output_file, "w") as f:
|
|
121
|
+
# Write header
|
|
122
|
+
f.write("# Dify Plugin SDK Schema Documentation\n\n")
|
|
123
|
+
|
|
124
|
+
schemas = list_schema_docs()
|
|
125
|
+
|
|
126
|
+
# Build type to schema mapping
|
|
127
|
+
for schema in schemas:
|
|
128
|
+
self._type_to_schema[schema.cls] = schema
|
|
129
|
+
self._types.add(schema.cls)
|
|
130
|
+
|
|
131
|
+
# Pre-process schemas to collect field descriptions
|
|
132
|
+
self._preprocess_schemas(schemas)
|
|
133
|
+
|
|
134
|
+
# Count references and build reference graph
|
|
135
|
+
self._build_reference_graph(schemas)
|
|
136
|
+
|
|
137
|
+
# Create blocks
|
|
138
|
+
self._create_blocks()
|
|
139
|
+
|
|
140
|
+
# Generate table of contents
|
|
141
|
+
f.write("## Table of Contents\n\n")
|
|
142
|
+
hierarchy = self._organize_toc()
|
|
143
|
+
|
|
144
|
+
def write_toc_item(node: tuple[type, list[Any]], indent: int = 0):
|
|
145
|
+
type_, children = node
|
|
146
|
+
schema = self._type_to_schema[type_]
|
|
147
|
+
name = schema.name or type_.__name__
|
|
148
|
+
f.write(f"{' ' * (indent * 2)}- [{name}](#{name.lower()})\n")
|
|
149
|
+
for child in children:
|
|
150
|
+
write_toc_item(child, indent + 1)
|
|
151
|
+
|
|
152
|
+
for node in hierarchy:
|
|
153
|
+
write_toc_item(node)
|
|
154
|
+
f.write("\n")
|
|
155
|
+
|
|
156
|
+
# Generate documentation for each block
|
|
157
|
+
for block in self._blocks:
|
|
158
|
+
for type_ in block:
|
|
159
|
+
self._write_schema_doc(f, type_)
|
|
160
|
+
|
|
161
|
+
def _preprocess_schemas(self, schemas: list) -> None:
|
|
162
|
+
"""Pre-process schemas to collect field descriptions and merge duplicates."""
|
|
163
|
+
# First pass: collect all field descriptions
|
|
164
|
+
for schema in schemas:
|
|
165
|
+
cls = schema.cls
|
|
166
|
+
if not issubclass(cls, BaseModel):
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
# Store schema description
|
|
170
|
+
if cls not in self._schema_descriptions or len(schema.description) > len(self._schema_descriptions[cls]):
|
|
171
|
+
self._schema_descriptions[cls] = schema.description
|
|
172
|
+
|
|
173
|
+
# Store field descriptions
|
|
174
|
+
outside_reference_fields = getattr(schema, "outside_reference_fields", {}) or {}
|
|
175
|
+
for field_name, field_info in cls.model_fields.items():
|
|
176
|
+
field_type = field_info.annotation
|
|
177
|
+
if field_type is None:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# For BaseModel types that are not outside references, we'll document them separately
|
|
181
|
+
if (
|
|
182
|
+
isinstance(field_type, type)
|
|
183
|
+
and issubclass(field_type, BaseModel)
|
|
184
|
+
and field_name not in outside_reference_fields
|
|
185
|
+
):
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
key = (cls, field_name)
|
|
189
|
+
description = field_info.description or ""
|
|
190
|
+
|
|
191
|
+
# Handle dynamic fields
|
|
192
|
+
if hasattr(schema, "dynamic_fields") and schema.dynamic_fields and field_name in schema.dynamic_fields:
|
|
193
|
+
description = schema.dynamic_fields[field_name]
|
|
194
|
+
|
|
195
|
+
# For outside reference fields, append reference information to description
|
|
196
|
+
if field_name in outside_reference_fields:
|
|
197
|
+
referenced_type = outside_reference_fields[field_name]
|
|
198
|
+
referenced_schema = self._type_to_schema.get(referenced_type)
|
|
199
|
+
schema_name = referenced_schema.name if referenced_schema else referenced_type.__name__
|
|
200
|
+
if description:
|
|
201
|
+
description = f"{description} "
|
|
202
|
+
f"(Paths to yaml files that will be loaded as [{schema_name}](#{schema_name.lower()}))"
|
|
203
|
+
else:
|
|
204
|
+
description = (
|
|
205
|
+
f"Paths to yaml files that will be loaded as [{schema_name}](#{schema_name.lower()})"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Store the most detailed description
|
|
209
|
+
if key not in self._field_descriptions or len(description) > len(self._field_descriptions[key]):
|
|
210
|
+
self._field_descriptions[key] = description
|
|
211
|
+
|
|
212
|
+
def _extract_referenced_types(self, field_type):
|
|
213
|
+
"""Recursively extract all referenced BaseModel and Enum types from a field type."""
|
|
214
|
+
referenced = set()
|
|
215
|
+
if field_type is None:
|
|
216
|
+
return referenced
|
|
217
|
+
|
|
218
|
+
# Handle direct type references (BaseModel and Enum)
|
|
219
|
+
if isinstance(field_type, type):
|
|
220
|
+
if issubclass(field_type, (BaseModel, Enum)):
|
|
221
|
+
referenced.add(field_type)
|
|
222
|
+
# Handle generic types (List, Dict, Union, etc)
|
|
223
|
+
elif (hasattr(field_type, "__origin__") and field_type.__origin__ == Union) or hasattr(field_type, "__args__"):
|
|
224
|
+
# Handle Union types
|
|
225
|
+
for arg in field_type.__args__:
|
|
226
|
+
referenced.update(self._extract_referenced_types(arg))
|
|
227
|
+
|
|
228
|
+
return referenced
|
|
229
|
+
|
|
230
|
+
def _build_reference_graph(self, schemas: list) -> None:
|
|
231
|
+
"""Build a graph of references between types (recursively for all nested types)."""
|
|
232
|
+
for schema in schemas:
|
|
233
|
+
cls = schema.cls
|
|
234
|
+
if not issubclass(cls, BaseModel):
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
# Count references in fields
|
|
238
|
+
for field_name, field_info in cls.model_fields.items():
|
|
239
|
+
field_type = field_info.annotation
|
|
240
|
+
if field_type is None:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
# Handle outside reference fields
|
|
244
|
+
outside_reference_fields = getattr(schema, "outside_reference_fields", {}) or {}
|
|
245
|
+
if field_name in outside_reference_fields:
|
|
246
|
+
referenced_type = outside_reference_fields[field_name]
|
|
247
|
+
# Add the reference to the graph
|
|
248
|
+
self._reference_graph[cls].add(referenced_type)
|
|
249
|
+
self._reference_counts[referenced_type] = self._reference_counts.get(referenced_type, 0) + 1
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
for ref_type in self._extract_referenced_types(field_type):
|
|
253
|
+
if ref_type != cls: # Avoid self-references
|
|
254
|
+
self._reference_graph[cls].add(ref_type)
|
|
255
|
+
self._reference_counts[ref_type] = self._reference_counts.get(ref_type, 0) + 1
|
|
256
|
+
|
|
257
|
+
def _create_blocks(self) -> None:
|
|
258
|
+
"""Create documentation blocks for all types"""
|
|
259
|
+
# First pass: assign each type to a block index
|
|
260
|
+
for type_ in self._types:
|
|
261
|
+
if type_ not in self._type_blocks:
|
|
262
|
+
# If type has top=True, assign it to block 0
|
|
263
|
+
if hasattr(type_, "__schema_docs__") and any(doc.top for doc in type_.__schema_docs__):
|
|
264
|
+
self._type_blocks[type_] = 0
|
|
265
|
+
else:
|
|
266
|
+
# Assign to a new block, starting from 1
|
|
267
|
+
self._type_blocks[type_] = len(self._type_blocks) + 1
|
|
268
|
+
|
|
269
|
+
# Second pass: create actual blocks
|
|
270
|
+
# Initialize blocks list with enough empty lists
|
|
271
|
+
max_block_index = max(self._type_blocks.values()) if self._type_blocks else 0
|
|
272
|
+
self._blocks = [[] for _ in range(max_block_index + 1)]
|
|
273
|
+
|
|
274
|
+
for type_, block_index in self._type_blocks.items():
|
|
275
|
+
self._blocks[block_index].append(type_)
|
|
276
|
+
|
|
277
|
+
# Sort blocks to ensure top types are first
|
|
278
|
+
# Only move block 0 to the front if it contains top types
|
|
279
|
+
if (
|
|
280
|
+
self._blocks
|
|
281
|
+
and self._blocks[0]
|
|
282
|
+
and any(
|
|
283
|
+
hasattr(t, "__schema_docs__") and any(doc.top for doc in t.__schema_docs__) for t in self._blocks[0]
|
|
284
|
+
)
|
|
285
|
+
):
|
|
286
|
+
top_block = self._blocks[0]
|
|
287
|
+
self._blocks.sort(key=lambda block: 0 if block is top_block else 1)
|
|
288
|
+
|
|
289
|
+
def _is_container_type(self, field_type: Any, container_types=(list, set)) -> bool:
|
|
290
|
+
"""Check if a field type is a container type (list, set, etc)."""
|
|
291
|
+
try:
|
|
292
|
+
return (
|
|
293
|
+
hasattr(field_type, "__origin__")
|
|
294
|
+
and isinstance(getattr(field_type, "__origin__", None), type)
|
|
295
|
+
and getattr(field_type, "__origin__", None) in container_types
|
|
296
|
+
)
|
|
297
|
+
except Exception:
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
def _get_container_name(self, field_type: Any) -> str:
|
|
301
|
+
"""Get the name of a container type."""
|
|
302
|
+
try:
|
|
303
|
+
origin = getattr(field_type, "__origin__", None)
|
|
304
|
+
return origin.__name__ if origin else str(field_type)
|
|
305
|
+
except Exception:
|
|
306
|
+
return str(field_type)
|
|
307
|
+
|
|
308
|
+
def _write_schema_doc(self, f, type_) -> None:
|
|
309
|
+
"""Write documentation for a single schema."""
|
|
310
|
+
schema = self._type_to_schema[type_]
|
|
311
|
+
name = schema.name or type_.__name__
|
|
312
|
+
|
|
313
|
+
f.write(f"## {name}\n\n")
|
|
314
|
+
|
|
315
|
+
# Write description
|
|
316
|
+
description = self._schema_descriptions.get(type_, "")
|
|
317
|
+
f.write(f"{description}\n\n")
|
|
318
|
+
|
|
319
|
+
if issubclass(type_, BaseModel):
|
|
320
|
+
f.write("### Fields\n\n")
|
|
321
|
+
f.write("| Name | Type | Description | Default | Extra |\n")
|
|
322
|
+
f.write("|------|------|-------------|---------|---------|\n")
|
|
323
|
+
|
|
324
|
+
# Track processed fields to avoid duplicates
|
|
325
|
+
processed_fields = set()
|
|
326
|
+
ignore_fields = set(getattr(schema, "ignore_fields", []) or [])
|
|
327
|
+
outside_reference_fields = getattr(schema, "outside_reference_fields", {}) or {}
|
|
328
|
+
|
|
329
|
+
for field_name, field_info in type_.model_fields.items():
|
|
330
|
+
if field_name in ignore_fields:
|
|
331
|
+
continue
|
|
332
|
+
field_type = field_info.annotation
|
|
333
|
+
if field_type is None:
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
# Skip if we've already processed this field type
|
|
337
|
+
if isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
|
338
|
+
if field_type in self._processed_field_types:
|
|
339
|
+
continue
|
|
340
|
+
self._processed_field_types.add(field_type)
|
|
341
|
+
|
|
342
|
+
# Skip if we've already processed this field
|
|
343
|
+
field_key = (field_type, field_name)
|
|
344
|
+
if field_key in processed_fields:
|
|
345
|
+
continue
|
|
346
|
+
processed_fields.add(field_key)
|
|
347
|
+
|
|
348
|
+
# Get the most detailed description
|
|
349
|
+
description = self._field_descriptions.get((type_, field_name), field_info.description or "")
|
|
350
|
+
|
|
351
|
+
# Format type name
|
|
352
|
+
type_name = self._format_type_name(field_type)
|
|
353
|
+
|
|
354
|
+
# Handle outside reference fields
|
|
355
|
+
if field_name in outside_reference_fields:
|
|
356
|
+
if self._is_container_type(field_type):
|
|
357
|
+
type_name = f"{self._get_container_name(field_type)}[str]"
|
|
358
|
+
else:
|
|
359
|
+
type_name = "str"
|
|
360
|
+
|
|
361
|
+
# Get field metadata
|
|
362
|
+
default = field_info.default
|
|
363
|
+
# User-friendly default value
|
|
364
|
+
if str(default) == "PydanticUndefined":
|
|
365
|
+
default = ""
|
|
366
|
+
|
|
367
|
+
# Get pattern if exists (robust)
|
|
368
|
+
extra = ""
|
|
369
|
+
if hasattr(field_info, "metadata"):
|
|
370
|
+
for value in field_info.metadata:
|
|
371
|
+
extra += f"{value} "
|
|
372
|
+
|
|
373
|
+
f.write(f"| {field_name} | {type_name} | {description} | {default} | {extra} |\n")
|
|
374
|
+
|
|
375
|
+
f.write("\n")
|
|
376
|
+
|
|
377
|
+
elif issubclass(type_, Enum):
|
|
378
|
+
f.write("### Values\n\n")
|
|
379
|
+
for member in type_:
|
|
380
|
+
f.write(f"- `{member.name}`: {member.value}\n")
|
|
381
|
+
f.write("\n")
|
|
382
|
+
|
|
383
|
+
def _format_type_name(self, field_type: Any) -> str:
|
|
384
|
+
"""Format the type name for display, handling complex types and references.
|
|
385
|
+
|
|
386
|
+
For BaseModel and Enum types, use their schema name if available.
|
|
387
|
+
For container types (list, dict, etc), recursively format their type arguments.
|
|
388
|
+
"""
|
|
389
|
+
if field_type is None:
|
|
390
|
+
return "Any"
|
|
391
|
+
|
|
392
|
+
if isinstance(field_type, type):
|
|
393
|
+
if issubclass(field_type, (BaseModel, Enum)):
|
|
394
|
+
# Use schema name if available
|
|
395
|
+
schema = self._type_to_schema.get(field_type)
|
|
396
|
+
name = schema.name if schema else field_type.__name__
|
|
397
|
+
return f"[{name}](#{name.lower()})"
|
|
398
|
+
return field_type.__name__
|
|
399
|
+
|
|
400
|
+
if hasattr(field_type, "__origin__") and hasattr(field_type, "__args__"):
|
|
401
|
+
origin = field_type.__origin__
|
|
402
|
+
if origin in (list, set):
|
|
403
|
+
inner_type = self._format_type_name(field_type.__args__[0])
|
|
404
|
+
return f"{origin.__name__}[{inner_type}]"
|
|
405
|
+
elif origin is dict:
|
|
406
|
+
key_type = self._format_type_name(field_type.__args__[0])
|
|
407
|
+
value_type = self._format_type_name(field_type.__args__[1])
|
|
408
|
+
return f"dict[{key_type}, {value_type}]"
|
|
409
|
+
elif origin is tuple:
|
|
410
|
+
types = [self._format_type_name(arg) for arg in field_type.__args__]
|
|
411
|
+
return f"tuple[{', '.join(types)}]"
|
|
412
|
+
elif origin is Union:
|
|
413
|
+
types = [self._format_type_name(arg) for arg in field_type.__args__]
|
|
414
|
+
return f"Union[{', '.join(types)}]"
|
|
415
|
+
|
|
416
|
+
return str(field_type)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from collections.abc import Callable, Mapping
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SchemaDoc:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
cls: type[BaseModel],
|
|
11
|
+
description: str,
|
|
12
|
+
name: Optional[str] = None,
|
|
13
|
+
top: bool = False,
|
|
14
|
+
ignore_fields: Optional[list[str]] = None,
|
|
15
|
+
outside_reference_fields: Optional[Mapping[str, type[BaseModel]]] = None,
|
|
16
|
+
):
|
|
17
|
+
self.cls = cls
|
|
18
|
+
self.description = description
|
|
19
|
+
self.name = name
|
|
20
|
+
self.top = top
|
|
21
|
+
self.ignore_fields = ignore_fields or []
|
|
22
|
+
self.outside_reference_fields = outside_reference_fields or {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__cls_mapping__: dict[type[BaseModel], SchemaDoc] = {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def docs(
|
|
29
|
+
description: str,
|
|
30
|
+
name: Optional[str] = None,
|
|
31
|
+
top: bool = False,
|
|
32
|
+
ignore_fields: Optional[list[str]] = None,
|
|
33
|
+
outside_reference_fields: Optional[Mapping[str, type[BaseModel]]] = None,
|
|
34
|
+
) -> Callable:
|
|
35
|
+
"""
|
|
36
|
+
Decorator to add schema documentation to a class
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
description: Description of the schema
|
|
40
|
+
name: Optional name override for the schema
|
|
41
|
+
example: Optional example instance
|
|
42
|
+
reference: Optional reference to another schema
|
|
43
|
+
dynamic_fields: Optional dynamic field descriptions
|
|
44
|
+
top: Whether this schema should be placed at the top of the documentation
|
|
45
|
+
ignore_fields: List of field names to ignore in documentation
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def decorator(cls_or_func: Any) -> Any:
|
|
49
|
+
# check if cls_or_func is a class
|
|
50
|
+
if isinstance(cls_or_func, type):
|
|
51
|
+
nonlocal name
|
|
52
|
+
name = name or cls_or_func.__name__
|
|
53
|
+
|
|
54
|
+
if cls_or_func not in __cls_mapping__:
|
|
55
|
+
__cls_mapping__[cls_or_func] = SchemaDoc(
|
|
56
|
+
cls_or_func, description, name, top, ignore_fields, outside_reference_fields
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if not hasattr(cls_or_func, "__schema_docs__"):
|
|
60
|
+
cls_or_func.__schema_docs__ = []
|
|
61
|
+
cls_or_func.__schema_docs__.append(__cls_mapping__[cls_or_func])
|
|
62
|
+
return cls_or_func
|
|
63
|
+
|
|
64
|
+
return decorator
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_schema_doc(cls: type[BaseModel]) -> SchemaDoc | None:
|
|
68
|
+
"""
|
|
69
|
+
Get the schema documentation for a class
|
|
70
|
+
"""
|
|
71
|
+
return __cls_mapping__.get(cls)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list_schema_docs() -> list[SchemaDoc]:
|
|
75
|
+
"""
|
|
76
|
+
List all schema documentation
|
|
77
|
+
"""
|
|
78
|
+
return list(__cls_mapping__.values())
|