mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
refactor(skill): transition from artifact set to bundle structure
- Replaced SkillArtifactSet with SkillBundle across various components, enhancing the organization of skill dependencies and references. - Updated SkillManager methods to load and save bundles instead of artifacts, improving clarity in asset management. - Refactored SkillCompiler to compile skills into bundles, streamlining the dependency resolution process. - Adjusted DifyCli and SandboxBashSession to utilize ToolDependencies, ensuring consistent handling of tool references. - Introduced AssetReferences for better management of file dependencies within skill bundles.
This commit is contained in:
@ -54,8 +54,7 @@ class SkillBuilder:
|
|||||||
documents = [SkillDocument(skill_id=s.node.id, content=s.content, metadata=s.metadata) for s in loaded]
|
documents = [SkillDocument(skill_id=s.node.id, content=s.content, metadata=s.metadata) for s in loaded]
|
||||||
artifact_set = SkillCompiler().compile_all(documents, tree, ctx.build_id)
|
artifact_set = SkillCompiler().compile_all(documents, tree, ctx.build_id)
|
||||||
|
|
||||||
# 3. Save tool artifact
|
SkillManager.save_bundle(ctx.tenant_id, ctx.app_id, ctx.build_id, artifact_set)
|
||||||
SkillManager.save_artifact(ctx.tenant_id, ctx.app_id, ctx.build_id, artifact_set)
|
|
||||||
|
|
||||||
# 4. Prepare compiled skills for upload
|
# 4. Prepare compiled skills for upload
|
||||||
to_upload: list[_CompiledSkill] = []
|
to_upload: list[_CompiledSkill] = []
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
|||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
from core.session.cli_api import CliApiSession
|
from core.session.cli_api import CliApiSession
|
||||||
from core.skill.entities import ToolArtifact, ToolReference
|
from core.skill.entities import ToolDependencies, ToolReference
|
||||||
from core.tools.entities.tool_entities import ToolParameter, ToolProviderType
|
from core.tools.entities.tool_entities import ToolParameter, ToolProviderType
|
||||||
from core.tools.tool_manager import ToolManager
|
from core.tools.tool_manager import ToolManager
|
||||||
from core.virtual_environment.__base.entities import Arch, OperatingSystem
|
from core.virtual_environment.__base.entities import Arch, OperatingSystem
|
||||||
@ -131,14 +131,14 @@ class DifyCliConfig(BaseModel):
|
|||||||
cls,
|
cls,
|
||||||
session: CliApiSession,
|
session: CliApiSession,
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
artifact: ToolArtifact,
|
tool_deps: ToolDependencies,
|
||||||
) -> DifyCliConfig:
|
) -> DifyCliConfig:
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
|
|
||||||
cli_api_url = dify_config.CLI_API_URL
|
cli_api_url = dify_config.CLI_API_URL
|
||||||
|
|
||||||
tools: list[Tool] = []
|
tools: list[Tool] = []
|
||||||
for dependency in artifact.dependencies:
|
for dependency in tool_deps.dependencies:
|
||||||
tool = ToolManager.get_tool_runtime(
|
tool = ToolManager.get_tool_runtime(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
provider_type=dependency.type,
|
provider_type=dependency.type,
|
||||||
@ -155,7 +155,7 @@ class DifyCliConfig(BaseModel):
|
|||||||
cli_api_session_id=session.id,
|
cli_api_session_id=session.id,
|
||||||
cli_api_secret=session.secret,
|
cli_api_secret=session.secret,
|
||||||
),
|
),
|
||||||
tool_references=[DifyCliToolReference.create_from_tool_reference(ref) for ref in artifact.references],
|
tool_references=[DifyCliToolReference.create_from_tool_reference(ref) for ref in tool_deps.references],
|
||||||
tools=[DifyCliToolConfig.create_from_tool(tool) for tool in tools],
|
tools=[DifyCliToolConfig.create_from_tool(tool) for tool in tools],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from types import TracebackType
|
|||||||
|
|
||||||
from core.sandbox.sandbox import Sandbox
|
from core.sandbox.sandbox import Sandbox
|
||||||
from core.session.cli_api import CliApiSession, CliApiSessionManager
|
from core.session.cli_api import CliApiSession, CliApiSessionManager
|
||||||
from core.skill.entities.tool_artifact import ToolArtifact
|
from core.skill.entities.tool_dependencies import ToolDependencies
|
||||||
from core.virtual_environment.__base.helpers import pipeline
|
from core.virtual_environment.__base.helpers import pipeline
|
||||||
|
|
||||||
from ..bash.dify_cli import DifyCliConfig
|
from ..bash.dify_cli import DifyCliConfig
|
||||||
@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class SandboxBashSession:
|
class SandboxBashSession:
|
||||||
def __init__(self, *, sandbox: Sandbox, node_id: str, tools: ToolArtifact | None) -> None:
|
def __init__(self, *, sandbox: Sandbox, node_id: str, tools: ToolDependencies | None) -> None:
|
||||||
self._sandbox = sandbox
|
self._sandbox = sandbox
|
||||||
self._node_id = node_id
|
self._node_id = node_id
|
||||||
self._tools = tools
|
self._tools = tools
|
||||||
@ -49,7 +49,7 @@ class SandboxBashSession:
|
|||||||
def _setup_node_tools_directory(
|
def _setup_node_tools_directory(
|
||||||
self,
|
self,
|
||||||
node_id: str,
|
node_id: str,
|
||||||
tools: ToolArtifact,
|
tools: ToolDependencies,
|
||||||
cli_api_session: CliApiSession,
|
cli_api_session: CliApiSession,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
node_tools_path = f"{DifyCli.TOOLS_ROOT}/{node_id}"
|
node_tools_path = f"{DifyCli.TOOLS_ROOT}/{node_id}"
|
||||||
@ -63,7 +63,7 @@ class SandboxBashSession:
|
|||||||
)
|
)
|
||||||
|
|
||||||
config_json = json.dumps(
|
config_json = json.dumps(
|
||||||
DifyCliConfig.create(session=cli_api_session, tenant_id=self._tenant_id, artifact=tools).model_dump(
|
DifyCliConfig.create(session=cli_api_session, tenant_id=self._tenant_id, tool_deps=tools).model_dump(
|
||||||
mode="json"
|
mode="json"
|
||||||
),
|
),
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
|
|||||||
@ -45,8 +45,6 @@ class DifyCliInitializer(SandboxInitializer):
|
|||||||
|
|
||||||
vm.upload_file(DifyCli.PATH, BytesIO(binary.path.read_bytes()))
|
vm.upload_file(DifyCli.PATH, BytesIO(binary.path.read_bytes()))
|
||||||
|
|
||||||
# Use 'cp' with mode preservation workaround: copy file to itself to claim ownership,
|
|
||||||
# then use 'install' to set executable permission
|
|
||||||
pipeline(vm).add(
|
pipeline(vm).add(
|
||||||
[
|
[
|
||||||
"sh",
|
"sh",
|
||||||
@ -60,19 +58,18 @@ class DifyCliInitializer(SandboxInitializer):
|
|||||||
|
|
||||||
logger.info("Dify CLI uploaded to sandbox, path=%s", DifyCli.PATH)
|
logger.info("Dify CLI uploaded to sandbox, path=%s", DifyCli.PATH)
|
||||||
|
|
||||||
artifact = SkillManager.load_artifact(self._tenant_id, self._app_id, self._assets_id)
|
bundle = SkillManager.load_bundle(self._tenant_id, self._app_id, self._assets_id)
|
||||||
if artifact is None or not artifact.get_tool_artifact().is_empty:
|
if bundle is None or not bundle.get_tool_dependencies().is_empty():
|
||||||
logger.info("No tools found in artifact for assets_id=%s", self._assets_id)
|
logger.info("No tools found in bundle for assets_id=%s", self._assets_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
# FIXME(Mairuis): store it in workflow context
|
|
||||||
self._cli_api_session = CliApiSessionManager().create(tenant_id=self._tenant_id, user_id=self._user_id)
|
self._cli_api_session = CliApiSessionManager().create(tenant_id=self._tenant_id, user_id=self._user_id)
|
||||||
|
|
||||||
pipeline(vm).add(
|
pipeline(vm).add(
|
||||||
["mkdir", "-p", DifyCli.GLOBAL_TOOLS_PATH], error_message="Failed to create global tools dir"
|
["mkdir", "-p", DifyCli.GLOBAL_TOOLS_PATH], error_message="Failed to create global tools dir"
|
||||||
).execute(raise_on_error=True)
|
).execute(raise_on_error=True)
|
||||||
|
|
||||||
config = DifyCliConfig.create(self._cli_api_session, self._tenant_id, artifact.get_tool_artifact())
|
config = DifyCliConfig.create(self._cli_api_session, self._tenant_id, bundle.get_tool_dependencies())
|
||||||
config_json = json.dumps(config.model_dump(mode="json"), ensure_ascii=False)
|
config_json = json.dumps(config.model_dump(mode="json"), ensure_ascii=False)
|
||||||
config_path = f"{DifyCli.GLOBAL_TOOLS_PATH}/{DifyCli.CONFIG_FILENAME}"
|
config_path = f"{DifyCli.GLOBAL_TOOLS_PATH}/{DifyCli.CONFIG_FILENAME}"
|
||||||
vm.upload_file(config_path, BytesIO(config_json.encode("utf-8")))
|
vm.upload_file(config_path, BytesIO(config_json.encode("utf-8")))
|
||||||
|
|||||||
@ -25,19 +25,19 @@ class SkillInitializer(SandboxInitializer):
|
|||||||
self._assets_id = assets_id
|
self._assets_id = assets_id
|
||||||
|
|
||||||
def initialize(self, sandbox: Sandbox) -> None:
|
def initialize(self, sandbox: Sandbox) -> None:
|
||||||
artifact_set = SkillManager.load_artifact(
|
bundle = SkillManager.load_bundle(
|
||||||
self._tenant_id,
|
self._tenant_id,
|
||||||
self._app_id,
|
self._app_id,
|
||||||
self._assets_id,
|
self._assets_id,
|
||||||
)
|
)
|
||||||
if artifact_set is None:
|
if bundle is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"No skill artifact set found for tenant_id={self._tenant_id},"
|
f"No skill bundle found for tenant_id={self._tenant_id},"
|
||||||
f"app_id={self._app_id}, "
|
f"app_id={self._app_id}, "
|
||||||
f"assets_id={self._assets_id} "
|
f"assets_id={self._assets_id} "
|
||||||
)
|
)
|
||||||
|
|
||||||
sandbox.attrs.set(
|
sandbox.attrs.set(
|
||||||
SkillAttrs.ARTIFACT_SET,
|
SkillAttrs.BUNDLE,
|
||||||
artifact_set,
|
bundle,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
from .constants import SkillAttrs
|
from .constants import SkillAttrs
|
||||||
from .entities import ToolArtifact, ToolDependency, ToolReference
|
from .entities import ToolDependencies, ToolDependency, ToolReference
|
||||||
from .skill_manager import SkillManager
|
from .skill_manager import SkillManager
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"SkillAttrs",
|
"SkillAttrs",
|
||||||
"SkillManager",
|
"SkillManager",
|
||||||
"ToolArtifact",
|
"ToolDependencies",
|
||||||
"ToolDependency",
|
"ToolDependency",
|
||||||
"ToolReference",
|
"ToolReference",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
from core.skill.entities.skill_artifact_set import SkillArtifactSet
|
from core.skill.entities.skill_bundle import SkillBundle
|
||||||
from libs.attr_map import AttrKey
|
from libs.attr_map import AttrKey
|
||||||
|
|
||||||
|
|
||||||
class SkillAttrs:
|
class SkillAttrs:
|
||||||
# Skill artifact set
|
BUNDLE = AttrKey("skill_bundle", SkillBundle)
|
||||||
ARTIFACT_SET = AttrKey("skill_artifact_set", SkillArtifactSet)
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from .file_artifact import FilesArtifact
|
from .asset_references import AssetReferences
|
||||||
from .skill_artifact import SkillArtifact, SkillSourceInfo
|
from .skill_bundle import SkillBundle
|
||||||
from .skill_artifact_set import SkillArtifactSet
|
from .skill_bundle_entry import SkillBundleEntry, SourceInfo
|
||||||
from .skill_document import SkillDocument
|
from .skill_document import SkillDocument
|
||||||
from .skill_metadata import (
|
from .skill_metadata import (
|
||||||
FileReference,
|
FileReference,
|
||||||
@ -9,18 +9,18 @@ from .skill_metadata import (
|
|||||||
ToolFieldConfig,
|
ToolFieldConfig,
|
||||||
ToolReference,
|
ToolReference,
|
||||||
)
|
)
|
||||||
from .tool_artifact import ToolArtifact, ToolDependency
|
from .tool_dependencies import ToolDependencies, ToolDependency
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"AssetReferences",
|
||||||
"FileReference",
|
"FileReference",
|
||||||
"FilesArtifact",
|
"SkillBundle",
|
||||||
"SkillArtifact",
|
"SkillBundleEntry",
|
||||||
"SkillArtifactSet",
|
|
||||||
"SkillDocument",
|
"SkillDocument",
|
||||||
"SkillMetadata",
|
"SkillMetadata",
|
||||||
"SkillSourceInfo",
|
"SourceInfo",
|
||||||
"ToolArtifact",
|
|
||||||
"ToolConfiguration",
|
"ToolConfiguration",
|
||||||
|
"ToolDependencies",
|
||||||
"ToolDependency",
|
"ToolDependency",
|
||||||
"ToolFieldConfig",
|
"ToolFieldConfig",
|
||||||
"ToolReference",
|
"ToolReference",
|
||||||
|
|||||||
@ -3,11 +3,7 @@ from pydantic import BaseModel, ConfigDict, Field
|
|||||||
from core.skill.entities.skill_metadata import FileReference
|
from core.skill.entities.skill_metadata import FileReference
|
||||||
|
|
||||||
|
|
||||||
class FilesArtifact(BaseModel):
|
class AssetReferences(BaseModel):
|
||||||
"""
|
|
||||||
File artifact - contains all file references (transitive closure)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
references: list[FileReference] = Field(default_factory=list, description="All file references")
|
references: list[FileReference] = Field(default_factory=list)
|
||||||
@ -1,30 +0,0 @@
|
|||||||
from pydantic import BaseModel, ConfigDict, Field
|
|
||||||
|
|
||||||
from core.skill.entities.file_artifact import FilesArtifact
|
|
||||||
from core.skill.entities.tool_artifact import ToolArtifact
|
|
||||||
|
|
||||||
|
|
||||||
class SkillSourceInfo(BaseModel):
|
|
||||||
"""Source file information for change detection."""
|
|
||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
asset_id: str = Field(description="Asset ID of the source skill file")
|
|
||||||
content_digest: str = Field(description="Hash of the original content for change detection")
|
|
||||||
|
|
||||||
|
|
||||||
class SkillArtifact(BaseModel):
|
|
||||||
"""
|
|
||||||
Compiled artifact for a single skill.
|
|
||||||
|
|
||||||
Contains the transitive closure of all tool and file dependencies,
|
|
||||||
plus the resolved content with all references replaced.
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
skill_id: str = Field(description="Unique identifier for this skill")
|
|
||||||
source: SkillSourceInfo = Field(description="Source file information")
|
|
||||||
tools: ToolArtifact = Field(description="All tool dependencies (transitive closure)")
|
|
||||||
files: FilesArtifact = Field(description="All file references (transitive closure)")
|
|
||||||
content: str = Field(description="Resolved content with all references replaced")
|
|
||||||
@ -3,27 +3,19 @@ from datetime import datetime
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
from core.skill.entities.skill_artifact import SkillArtifact
|
from core.skill.entities.skill_bundle_entry import SkillBundleEntry
|
||||||
from core.skill.entities.skill_metadata import ToolReference
|
from core.skill.entities.skill_metadata import ToolReference
|
||||||
from core.skill.entities.tool_artifact import ToolArtifact, ToolDependency
|
from core.skill.entities.tool_dependencies import ToolDependencies, ToolDependency
|
||||||
|
|
||||||
|
|
||||||
class SkillArtifactSet(BaseModel):
|
class SkillBundle(BaseModel):
|
||||||
"""
|
|
||||||
Compiled index for an entire skill project.
|
|
||||||
|
|
||||||
- Corresponds to a single JSON file in S3
|
|
||||||
- Load once, query multiple times
|
|
||||||
- All persistence operations handled by SkillManager
|
|
||||||
"""
|
|
||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
assets_id: str = Field(description="Assets ID this artifact set belongs to")
|
assets_id: str = Field(description="Assets ID this bundle belongs to")
|
||||||
schema_version: int = Field(default=1, description="Schema version for forward compatibility")
|
schema_version: int = Field(default=1, description="Schema version for forward compatibility")
|
||||||
built_at: datetime | None = Field(default=None, description="Build timestamp")
|
built_at: datetime | None = Field(default=None, description="Build timestamp")
|
||||||
|
|
||||||
items: dict[str, SkillArtifact] = Field(default_factory=dict, description="skill_id -> SkillArtifact")
|
entries: dict[str, SkillBundleEntry] = Field(default_factory=dict, description="skill_id -> SkillBundleEntry")
|
||||||
|
|
||||||
dependency_graph: dict[str, list[str]] = Field(
|
dependency_graph: dict[str, list[str]] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
@ -35,14 +27,14 @@ class SkillArtifactSet(BaseModel):
|
|||||||
description="skill_id -> list of skill_ids that depend on it",
|
description="skill_id -> list of skill_ids that depend on it",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, skill_id: str) -> SkillArtifact | None:
|
def get(self, skill_id: str) -> SkillBundleEntry | None:
|
||||||
return self.items.get(skill_id)
|
return self.entries.get(skill_id)
|
||||||
|
|
||||||
def upsert(self, artifact: SkillArtifact) -> None:
|
def upsert(self, entry: SkillBundleEntry) -> None:
|
||||||
self.items[artifact.skill_id] = artifact
|
self.entries[entry.skill_id] = entry
|
||||||
|
|
||||||
def remove(self, skill_id: str) -> None:
|
def remove(self, skill_id: str) -> None:
|
||||||
self.items.pop(skill_id, None)
|
self.entries.pop(skill_id, None)
|
||||||
self.dependency_graph.pop(skill_id, None)
|
self.dependency_graph.pop(skill_id, None)
|
||||||
self.reverse_graph.pop(skill_id, None)
|
self.reverse_graph.pop(skill_id, None)
|
||||||
for deps in self.reverse_graph.values():
|
for deps in self.reverse_graph.values():
|
||||||
@ -66,13 +58,13 @@ class SkillArtifactSet(BaseModel):
|
|||||||
queue.append(dependent)
|
queue.append(dependent)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def subset(self, skill_ids: Iterable[str]) -> "SkillArtifactSet":
|
def subset(self, skill_ids: Iterable[str]) -> "SkillBundle":
|
||||||
skill_id_set = set(skill_ids)
|
skill_id_set = set(skill_ids)
|
||||||
return SkillArtifactSet(
|
return SkillBundle(
|
||||||
assets_id=self.assets_id,
|
assets_id=self.assets_id,
|
||||||
schema_version=self.schema_version,
|
schema_version=self.schema_version,
|
||||||
built_at=self.built_at,
|
built_at=self.built_at,
|
||||||
items={sid: self.items[sid] for sid in skill_id_set if sid in self.items},
|
entries={sid: self.entries[sid] for sid in skill_id_set if sid in self.entries},
|
||||||
dependency_graph={
|
dependency_graph={
|
||||||
sid: [dep for dep in deps if dep in skill_id_set]
|
sid: [dep for dep in deps if dep in skill_id_set]
|
||||||
for sid, deps in self.dependency_graph.items()
|
for sid, deps in self.dependency_graph.items()
|
||||||
@ -85,21 +77,21 @@ class SkillArtifactSet(BaseModel):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_tool_artifact(self) -> ToolArtifact:
|
def get_tool_dependencies(self) -> ToolDependencies:
|
||||||
dependencies: dict[str, ToolDependency] = {}
|
dependencies: dict[str, ToolDependency] = {}
|
||||||
references: dict[str, ToolReference] = {}
|
references: dict[str, ToolReference] = {}
|
||||||
|
|
||||||
for artifact in self.items.values():
|
for entry in self.entries.values():
|
||||||
for dep in artifact.tools.dependencies:
|
for dep in entry.tools.dependencies:
|
||||||
key = f"{dep.provider}.{dep.tool_name}"
|
key = f"{dep.provider}.{dep.tool_name}"
|
||||||
if key not in dependencies:
|
if key not in dependencies:
|
||||||
dependencies[key] = dep
|
dependencies[key] = dep
|
||||||
|
|
||||||
for ref in artifact.tools.references:
|
for ref in entry.tools.references:
|
||||||
if ref.uuid not in references:
|
if ref.uuid not in references:
|
||||||
references[ref.uuid] = ref
|
references[ref.uuid] = ref
|
||||||
|
|
||||||
return ToolArtifact(
|
return ToolDependencies(
|
||||||
dependencies=list(dependencies.values()),
|
dependencies=list(dependencies.values()),
|
||||||
references=list(references.values()),
|
references=list(references.values()),
|
||||||
)
|
)
|
||||||
21
api/core/skill/entities/skill_bundle_entry.py
Normal file
21
api/core/skill/entities/skill_bundle_entry.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
from core.skill.entities.asset_references import AssetReferences
|
||||||
|
from core.skill.entities.tool_dependencies import ToolDependencies
|
||||||
|
|
||||||
|
|
||||||
|
class SourceInfo(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
asset_id: str = Field(description="Asset ID of the source skill file")
|
||||||
|
content_digest: str = Field(description="Hash of the original content for change detection")
|
||||||
|
|
||||||
|
|
||||||
|
class SkillBundleEntry(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
skill_id: str = Field(description="Unique identifier for this skill")
|
||||||
|
source: SourceInfo = Field(description="Source file information")
|
||||||
|
tools: ToolDependencies = Field(description="All tool dependencies (transitive closure)")
|
||||||
|
files: AssetReferences = Field(description="All file references (transitive closure)")
|
||||||
|
content: str = Field(description="Resolved content with all references replaced")
|
||||||
@ -12,7 +12,7 @@ class ToolDependency(BaseModel):
|
|||||||
tool_name: str
|
tool_name: str
|
||||||
|
|
||||||
|
|
||||||
class ToolArtifact(BaseModel):
|
class ToolDependencies(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
dependencies: list[ToolDependency] = Field(default_factory=list)
|
dependencies: list[ToolDependency] = Field(default_factory=list)
|
||||||
@ -21,9 +21,9 @@ class ToolArtifact(BaseModel):
|
|||||||
def is_empty(self) -> bool:
|
def is_empty(self) -> bool:
|
||||||
return not self.dependencies and not self.references
|
return not self.dependencies and not self.references
|
||||||
|
|
||||||
def filter(self, tools: list[tuple[str, str]]) -> "ToolArtifact":
|
def filter(self, tools: list[tuple[str, str]]) -> "ToolDependencies":
|
||||||
tool_names = {f"{provider}.{tool_name}" for provider, tool_name in tools}
|
tool_names = {f"{provider}.{tool_name}" for provider, tool_name in tools}
|
||||||
return ToolArtifact(
|
return ToolDependencies(
|
||||||
dependencies=[
|
dependencies=[
|
||||||
dependency
|
dependency
|
||||||
for dependency in self.dependencies
|
for dependency in self.dependencies
|
||||||
@ -36,7 +36,7 @@ class ToolArtifact(BaseModel):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def merge(self, other: "ToolArtifact") -> "ToolArtifact":
|
def merge(self, other: "ToolDependencies") -> "ToolDependencies":
|
||||||
dep_map: dict[str, ToolDependency] = {}
|
dep_map: dict[str, ToolDependency] = {}
|
||||||
for dep in self.dependencies:
|
for dep in self.dependencies:
|
||||||
key = f"{dep.provider}.{dep.tool_name}"
|
key = f"{dep.provider}.{dep.tool_name}"
|
||||||
@ -53,7 +53,7 @@ class ToolArtifact(BaseModel):
|
|||||||
if ref.uuid not in ref_map:
|
if ref.uuid not in ref_map:
|
||||||
ref_map[ref.uuid] = ref
|
ref_map[ref.uuid] = ref
|
||||||
|
|
||||||
return ToolArtifact(
|
return ToolDependencies(
|
||||||
dependencies=list(dep_map.values()),
|
dependencies=list(dep_map.values()),
|
||||||
references=list(ref_map.values()),
|
references=list(ref_map.values()),
|
||||||
)
|
)
|
||||||
@ -6,9 +6,9 @@ from datetime import UTC, datetime
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from core.app.entities.app_asset_entities import AppAssetFileTree
|
from core.app.entities.app_asset_entities import AppAssetFileTree
|
||||||
from core.skill.entities.file_artifact import FilesArtifact
|
from core.skill.entities.asset_references import AssetReferences
|
||||||
from core.skill.entities.skill_artifact import SkillArtifact, SkillSourceInfo
|
from core.skill.entities.skill_bundle import SkillBundle
|
||||||
from core.skill.entities.skill_artifact_set import SkillArtifactSet
|
from core.skill.entities.skill_bundle_entry import SkillBundleEntry, SourceInfo
|
||||||
from core.skill.entities.skill_document import SkillDocument
|
from core.skill.entities.skill_document import SkillDocument
|
||||||
from core.skill.entities.skill_metadata import (
|
from core.skill.entities.skill_metadata import (
|
||||||
FileReference,
|
FileReference,
|
||||||
@ -16,7 +16,7 @@ from core.skill.entities.skill_metadata import (
|
|||||||
ToolConfiguration,
|
ToolConfiguration,
|
||||||
ToolReference,
|
ToolReference,
|
||||||
)
|
)
|
||||||
from core.skill.entities.tool_artifact import ToolArtifact, ToolDependency
|
from core.skill.entities.tool_dependencies import ToolDependencies, ToolDependency
|
||||||
from core.tools.entities.tool_entities import ToolProviderType
|
from core.tools.entities.tool_entities import ToolProviderType
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -26,17 +26,6 @@ FILE_REFERENCE_PATTERN = re.compile(r"§\[file\]\.\[([^\]]+)\]\.\[([^\]]+)\]§")
|
|||||||
|
|
||||||
|
|
||||||
class SkillCompiler:
|
class SkillCompiler:
|
||||||
"""
|
|
||||||
Stateless skill compiler.
|
|
||||||
|
|
||||||
Responsibilities:
|
|
||||||
- Parse raw metadata dict into SkillMetadata
|
|
||||||
- Parse direct dependencies from skill content
|
|
||||||
- Compute transitive closure based on existing artifact set
|
|
||||||
- Resolve content by replacing references
|
|
||||||
- Generate SkillArtifact
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _parse_metadata(self, content: str, raw_metadata: Mapping[str, Any]) -> SkillMetadata:
|
def _parse_metadata(self, content: str, raw_metadata: Mapping[str, Any]) -> SkillMetadata:
|
||||||
tools_raw: dict[str, Any] = dict(raw_metadata.get("tools", {}))
|
tools_raw: dict[str, Any] = dict(raw_metadata.get("tools", {}))
|
||||||
tools: dict[str, ToolReference] = {}
|
tools: dict[str, ToolReference] = {}
|
||||||
@ -76,8 +65,8 @@ class SkillCompiler:
|
|||||||
documents: list[SkillDocument],
|
documents: list[SkillDocument],
|
||||||
file_tree: AppAssetFileTree,
|
file_tree: AppAssetFileTree,
|
||||||
assets_id: str,
|
assets_id: str,
|
||||||
) -> SkillArtifactSet:
|
) -> SkillBundle:
|
||||||
artifact_set = SkillArtifactSet(
|
bundle = SkillBundle(
|
||||||
assets_id=assets_id,
|
assets_id=assets_id,
|
||||||
built_at=datetime.now(UTC),
|
built_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
@ -89,26 +78,26 @@ class SkillCompiler:
|
|||||||
metadata = self._parse_metadata(doc.content, doc.metadata)
|
metadata = self._parse_metadata(doc.content, doc.metadata)
|
||||||
parsed_metadata[doc.skill_id] = metadata
|
parsed_metadata[doc.skill_id] = metadata
|
||||||
direct_skill_refs = self._extract_skill_refs(metadata, doc_map)
|
direct_skill_refs = self._extract_skill_refs(metadata, doc_map)
|
||||||
artifact_set.dependency_graph[doc.skill_id] = list(direct_skill_refs)
|
bundle.dependency_graph[doc.skill_id] = list(direct_skill_refs)
|
||||||
for ref_id in direct_skill_refs:
|
for ref_id in direct_skill_refs:
|
||||||
if ref_id not in artifact_set.reverse_graph:
|
if ref_id not in bundle.reverse_graph:
|
||||||
artifact_set.reverse_graph[ref_id] = []
|
bundle.reverse_graph[ref_id] = []
|
||||||
artifact_set.reverse_graph[ref_id].append(doc.skill_id)
|
bundle.reverse_graph[ref_id].append(doc.skill_id)
|
||||||
|
|
||||||
for doc in documents:
|
for doc in documents:
|
||||||
metadata = parsed_metadata[doc.skill_id]
|
metadata = parsed_metadata[doc.skill_id]
|
||||||
artifact = self._compile_single(doc, metadata, artifact_set, parsed_metadata, file_tree)
|
entry = self._compile_single(doc, metadata, bundle, parsed_metadata, file_tree)
|
||||||
artifact_set.upsert(artifact)
|
bundle.upsert(entry)
|
||||||
|
|
||||||
return artifact_set
|
return bundle
|
||||||
|
|
||||||
def compile_one(
|
def compile_one(
|
||||||
self,
|
self,
|
||||||
artifact_set: SkillArtifactSet,
|
bundle: SkillBundle,
|
||||||
document: SkillDocument,
|
document: SkillDocument,
|
||||||
file_tree: AppAssetFileTree,
|
file_tree: AppAssetFileTree,
|
||||||
all_documents: dict[str, SkillDocument] | None = None,
|
all_documents: dict[str, SkillDocument] | None = None,
|
||||||
) -> SkillArtifact:
|
) -> SkillBundleEntry:
|
||||||
doc_map = all_documents or {}
|
doc_map = all_documents or {}
|
||||||
if document.skill_id not in doc_map:
|
if document.skill_id not in doc_map:
|
||||||
doc_map[document.skill_id] = document
|
doc_map[document.skill_id] = document
|
||||||
@ -119,25 +108,25 @@ class SkillCompiler:
|
|||||||
|
|
||||||
metadata = parsed_metadata[document.skill_id]
|
metadata = parsed_metadata[document.skill_id]
|
||||||
direct_skill_refs = self._extract_skill_refs(metadata, doc_map)
|
direct_skill_refs = self._extract_skill_refs(metadata, doc_map)
|
||||||
artifact_set.dependency_graph[document.skill_id] = list(direct_skill_refs)
|
bundle.dependency_graph[document.skill_id] = list(direct_skill_refs)
|
||||||
for ref_id in direct_skill_refs:
|
for ref_id in direct_skill_refs:
|
||||||
if ref_id not in artifact_set.reverse_graph:
|
if ref_id not in bundle.reverse_graph:
|
||||||
artifact_set.reverse_graph[ref_id] = []
|
bundle.reverse_graph[ref_id] = []
|
||||||
if document.skill_id not in artifact_set.reverse_graph[ref_id]:
|
if document.skill_id not in bundle.reverse_graph[ref_id]:
|
||||||
artifact_set.reverse_graph[ref_id].append(document.skill_id)
|
bundle.reverse_graph[ref_id].append(document.skill_id)
|
||||||
|
|
||||||
return self._compile_single(document, metadata, artifact_set, parsed_metadata, file_tree)
|
return self._compile_single(document, metadata, bundle, parsed_metadata, file_tree)
|
||||||
|
|
||||||
def _compile_single(
|
def _compile_single(
|
||||||
self,
|
self,
|
||||||
document: SkillDocument,
|
document: SkillDocument,
|
||||||
metadata: SkillMetadata,
|
metadata: SkillMetadata,
|
||||||
artifact_set: SkillArtifactSet,
|
bundle: SkillBundle,
|
||||||
parsed_metadata: dict[str, SkillMetadata],
|
parsed_metadata: dict[str, SkillMetadata],
|
||||||
file_tree: AppAssetFileTree,
|
file_tree: AppAssetFileTree,
|
||||||
) -> SkillArtifact:
|
) -> SkillBundleEntry:
|
||||||
all_tools, all_files = self._compute_transitive_closure(
|
all_tools, all_files = self._compute_transitive_closure(
|
||||||
document.skill_id, artifact_set, parsed_metadata
|
document.skill_id, bundle, parsed_metadata
|
||||||
)
|
)
|
||||||
|
|
||||||
current_node = file_tree.get(document.skill_id)
|
current_node = file_tree.get(document.skill_id)
|
||||||
@ -148,17 +137,17 @@ class SkillCompiler:
|
|||||||
|
|
||||||
content_digest = hashlib.sha256(document.content.encode("utf-8")).hexdigest()
|
content_digest = hashlib.sha256(document.content.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
return SkillArtifact(
|
return SkillBundleEntry(
|
||||||
skill_id=document.skill_id,
|
skill_id=document.skill_id,
|
||||||
source=SkillSourceInfo(
|
source=SourceInfo(
|
||||||
asset_id=document.skill_id,
|
asset_id=document.skill_id,
|
||||||
content_digest=content_digest,
|
content_digest=content_digest,
|
||||||
),
|
),
|
||||||
tools=ToolArtifact(
|
tools=ToolDependencies(
|
||||||
dependencies=list(all_tools.values()),
|
dependencies=list(all_tools.values()),
|
||||||
references=list(metadata.tools.values()),
|
references=list(metadata.tools.values()),
|
||||||
),
|
),
|
||||||
files=FilesArtifact(
|
files=AssetReferences(
|
||||||
references=list(all_files.values()),
|
references=list(all_files.values()),
|
||||||
),
|
),
|
||||||
content=resolved_content,
|
content=resolved_content,
|
||||||
@ -178,7 +167,7 @@ class SkillCompiler:
|
|||||||
def _compute_transitive_closure(
|
def _compute_transitive_closure(
|
||||||
self,
|
self,
|
||||||
skill_id: str,
|
skill_id: str,
|
||||||
artifact_set: SkillArtifactSet,
|
bundle: SkillBundle,
|
||||||
parsed_metadata: dict[str, SkillMetadata],
|
parsed_metadata: dict[str, SkillMetadata],
|
||||||
) -> tuple[dict[str, ToolDependency], dict[str, FileReference]]:
|
) -> tuple[dict[str, ToolDependency], dict[str, FileReference]]:
|
||||||
all_tools: dict[str, ToolDependency] = {}
|
all_tools: dict[str, ToolDependency] = {}
|
||||||
@ -195,13 +184,13 @@ class SkillCompiler:
|
|||||||
|
|
||||||
metadata = parsed_metadata.get(current_id)
|
metadata = parsed_metadata.get(current_id)
|
||||||
if metadata is None:
|
if metadata is None:
|
||||||
existing_artifact = artifact_set.get(current_id)
|
existing_entry = bundle.get(current_id)
|
||||||
if existing_artifact:
|
if existing_entry:
|
||||||
for dep in existing_artifact.tools.dependencies:
|
for dep in existing_entry.tools.dependencies:
|
||||||
key = f"{dep.provider}.{dep.tool_name}"
|
key = f"{dep.provider}.{dep.tool_name}"
|
||||||
if key not in all_tools:
|
if key not in all_tools:
|
||||||
all_tools[key] = dep
|
all_tools[key] = dep
|
||||||
for file_ref in existing_artifact.files.references:
|
for file_ref in existing_entry.files.references:
|
||||||
if file_ref.asset_id not in all_files:
|
if file_ref.asset_id not in all_files:
|
||||||
all_files[file_ref.asset_id] = file_ref
|
all_files[file_ref.asset_id] = file_ref
|
||||||
continue
|
continue
|
||||||
@ -219,7 +208,7 @@ class SkillCompiler:
|
|||||||
if file_ref.asset_id not in all_files:
|
if file_ref.asset_id not in all_files:
|
||||||
all_files[file_ref.asset_id] = file_ref
|
all_files[file_ref.asset_id] = file_ref
|
||||||
|
|
||||||
for dep_id in artifact_set.dependency_graph.get(current_id, []):
|
for dep_id in bundle.dependency_graph.get(current_id, []):
|
||||||
if dep_id not in visited:
|
if dep_id not in visited:
|
||||||
queue.append(dep_id)
|
queue.append(dep_id)
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,28 @@
|
|||||||
from core.app_assets.paths import AssetPaths
|
from core.app_assets.paths import AssetPaths
|
||||||
from core.skill.entities.skill_artifact_set import SkillArtifactSet
|
from core.skill.entities.skill_bundle import SkillBundle
|
||||||
from extensions.ext_storage import storage
|
from extensions.ext_storage import storage
|
||||||
|
|
||||||
|
|
||||||
class SkillManager:
|
class SkillManager:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_artifact(
|
def load_bundle(
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
app_id: str,
|
app_id: str,
|
||||||
assets_id: str,
|
assets_id: str,
|
||||||
) -> SkillArtifactSet | None:
|
) -> SkillBundle | None:
|
||||||
key = AssetPaths.build_skill_artifact_set(tenant_id, app_id, assets_id)
|
key = AssetPaths.build_skill_artifact_set(tenant_id, app_id, assets_id)
|
||||||
try:
|
try:
|
||||||
data = storage.load_once(key)
|
data = storage.load_once(key)
|
||||||
return SkillArtifactSet.model_validate_json(data)
|
return SkillBundle.model_validate_json(data)
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save_artifact(
|
def save_bundle(
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
app_id: str,
|
app_id: str,
|
||||||
assets_id: str,
|
assets_id: str,
|
||||||
artifact_set: SkillArtifactSet,
|
bundle: SkillBundle,
|
||||||
) -> None:
|
) -> None:
|
||||||
key = AssetPaths.build_skill_artifact_set(tenant_id, app_id, assets_id)
|
key = AssetPaths.build_skill_artifact_set(tenant_id, app_id, assets_id)
|
||||||
storage.save(key, artifact_set.model_dump_json(indent=2).encode("utf-8"))
|
storage.save(key, bundle.model_dump_json(indent=2).encode("utf-8"))
|
||||||
|
|||||||
@ -57,9 +57,9 @@ from core.sandbox import Sandbox
|
|||||||
from core.sandbox.bash.session import SandboxBashSession
|
from core.sandbox.bash.session import SandboxBashSession
|
||||||
from core.sandbox.entities.config import AppAssets
|
from core.sandbox.entities.config import AppAssets
|
||||||
from core.skill.constants import SkillAttrs
|
from core.skill.constants import SkillAttrs
|
||||||
from core.skill.entities.skill_artifact_set import SkillArtifactSet
|
from core.skill.entities.skill_bundle import SkillBundle
|
||||||
from core.skill.entities.skill_document import SkillDocument
|
from core.skill.entities.skill_document import SkillDocument
|
||||||
from core.skill.entities.tool_artifact import ToolArtifact
|
from core.skill.entities.tool_dependencies import ToolDependencies
|
||||||
from core.skill.skill_compiler import SkillCompiler
|
from core.skill.skill_compiler import SkillCompiler
|
||||||
from core.tools.__base.tool import Tool
|
from core.tools.__base.tool import Tool
|
||||||
from core.tools.signature import sign_upload_file
|
from core.tools.signature import sign_upload_file
|
||||||
@ -299,14 +299,14 @@ class LLMNode(Node[LLMNodeData]):
|
|||||||
|
|
||||||
sandbox = self.graph_runtime_state.sandbox
|
sandbox = self.graph_runtime_state.sandbox
|
||||||
if sandbox:
|
if sandbox:
|
||||||
tool_artifact = self._extract_tool_artifact()
|
tool_dependencies = self._extract_tool_dependencies()
|
||||||
generator = self._invoke_llm_with_sandbox(
|
generator = self._invoke_llm_with_sandbox(
|
||||||
sandbox=sandbox,
|
sandbox=sandbox,
|
||||||
model_instance=model_instance,
|
model_instance=model_instance,
|
||||||
prompt_messages=prompt_messages,
|
prompt_messages=prompt_messages,
|
||||||
stop=stop,
|
stop=stop,
|
||||||
variable_pool=variable_pool,
|
variable_pool=variable_pool,
|
||||||
tool_artifact=tool_artifact,
|
tool_dependencies=tool_dependencies,
|
||||||
)
|
)
|
||||||
elif self.tool_call_enabled:
|
elif self.tool_call_enabled:
|
||||||
generator = self._invoke_llm_with_tools(
|
generator = self._invoke_llm_with_tools(
|
||||||
@ -1492,11 +1492,10 @@ class LLMNode(Node[LLMNodeData]):
|
|||||||
) -> Sequence[PromptMessage]:
|
) -> Sequence[PromptMessage]:
|
||||||
prompt_messages: list[PromptMessage] = []
|
prompt_messages: list[PromptMessage] = []
|
||||||
|
|
||||||
# Extract skill compilation context from sandbox if available
|
bundle: SkillBundle | None = None
|
||||||
artifact_set: SkillArtifactSet | None = None
|
|
||||||
file_tree: AppAssetFileTree | None = None
|
file_tree: AppAssetFileTree | None = None
|
||||||
if sandbox:
|
if sandbox:
|
||||||
artifact_set = sandbox.attrs.get(SkillAttrs.ARTIFACT_SET)
|
bundle = sandbox.attrs.get(SkillAttrs.BUNDLE)
|
||||||
file_tree = sandbox.attrs.get(AppAssetsAttrs.FILE_TREE)
|
file_tree = sandbox.attrs.get(AppAssetsAttrs.FILE_TREE)
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
@ -1507,29 +1506,26 @@ class LLMNode(Node[LLMNodeData]):
|
|||||||
variable_pool=variable_pool,
|
variable_pool=variable_pool,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compile skill references after jinja2 rendering
|
if bundle is not None and file_tree is not None:
|
||||||
if artifact_set is not None and file_tree is not None:
|
skill_entry = SkillCompiler().compile_one(
|
||||||
skill_artifact = SkillCompiler().compile_one(
|
bundle=bundle,
|
||||||
artifact_set=artifact_set,
|
|
||||||
document=SkillDocument(skill_id="anonymous", content=result_text, metadata={}),
|
document=SkillDocument(skill_id="anonymous", content=result_text, metadata={}),
|
||||||
file_tree=file_tree,
|
file_tree=file_tree,
|
||||||
base_path=AppAssets.PATH,
|
base_path=AppAssets.PATH,
|
||||||
)
|
)
|
||||||
result_text = skill_artifact.content
|
result_text = skill_entry.content
|
||||||
|
|
||||||
prompt_message = _combine_message_content_with_role(
|
prompt_message = _combine_message_content_with_role(
|
||||||
contents=[TextPromptMessageContent(data=result_text)], role=message.role
|
contents=[TextPromptMessageContent(data=result_text)], role=message.role
|
||||||
)
|
)
|
||||||
prompt_messages.append(prompt_message)
|
prompt_messages.append(prompt_message)
|
||||||
else:
|
else:
|
||||||
# Get segment group from basic message
|
|
||||||
if context:
|
if context:
|
||||||
template = message.text.replace("{#context#}", context)
|
template = message.text.replace("{#context#}", context)
|
||||||
else:
|
else:
|
||||||
template = message.text
|
template = message.text
|
||||||
segment_group = variable_pool.convert_template(template)
|
segment_group = variable_pool.convert_template(template)
|
||||||
|
|
||||||
# Process segments for images
|
|
||||||
file_contents = []
|
file_contents = []
|
||||||
for segment in segment_group.value:
|
for segment in segment_group.value:
|
||||||
if isinstance(segment, ArrayFileSegment):
|
if isinstance(segment, ArrayFileSegment):
|
||||||
@ -1547,18 +1543,16 @@ class LLMNode(Node[LLMNodeData]):
|
|||||||
)
|
)
|
||||||
file_contents.append(file_content)
|
file_contents.append(file_content)
|
||||||
|
|
||||||
# Create message with text from all segments
|
|
||||||
plain_text = segment_group.text
|
plain_text = segment_group.text
|
||||||
|
|
||||||
# Compile skill references after context and variable substitution
|
if plain_text and bundle is not None and file_tree is not None:
|
||||||
if plain_text and artifact_set is not None and file_tree is not None:
|
skill_entry = SkillCompiler().compile_one(
|
||||||
skill_artifact = SkillCompiler().compile_one(
|
bundle=bundle,
|
||||||
artifact_set=artifact_set,
|
|
||||||
document=SkillDocument(skill_id="anonymous", content=plain_text, metadata={}),
|
document=SkillDocument(skill_id="anonymous", content=plain_text, metadata={}),
|
||||||
file_tree=file_tree,
|
file_tree=file_tree,
|
||||||
base_path=AppAssets.PATH,
|
base_path=AppAssets.PATH,
|
||||||
)
|
)
|
||||||
plain_text = skill_artifact.content
|
plain_text = skill_entry.content
|
||||||
|
|
||||||
if plain_text:
|
if plain_text:
|
||||||
prompt_message = _combine_message_content_with_role(
|
prompt_message = _combine_message_content_with_role(
|
||||||
@ -1813,30 +1807,30 @@ class LLMNode(Node[LLMNodeData]):
|
|||||||
generation_data,
|
generation_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _extract_tool_artifact(self) -> ToolArtifact | None:
|
def _extract_tool_dependencies(self) -> ToolDependencies | None:
|
||||||
"""Extract tool artifact from prompt template."""
|
"""Extract tool artifact from prompt template."""
|
||||||
|
|
||||||
sandbox = self.graph_runtime_state.sandbox
|
sandbox = self.graph_runtime_state.sandbox
|
||||||
if not sandbox:
|
if not sandbox:
|
||||||
raise LLMNodeError("Sandbox not found")
|
raise LLMNodeError("Sandbox not found")
|
||||||
|
|
||||||
artifact_set = sandbox.attrs.get(SkillAttrs.ARTIFACT_SET)
|
bundle = sandbox.attrs.get(SkillAttrs.BUNDLE)
|
||||||
file_tree = sandbox.attrs.get(AppAssetsAttrs.FILE_TREE)
|
file_tree = sandbox.attrs.get(AppAssetsAttrs.FILE_TREE)
|
||||||
tool_artifacts: list[ToolArtifact] = []
|
tool_deps_list: list[ToolDependencies] = []
|
||||||
for prompt in self.node_data.prompt_template:
|
for prompt in self.node_data.prompt_template:
|
||||||
if isinstance(prompt, LLMNodeChatModelMessage):
|
if isinstance(prompt, LLMNodeChatModelMessage):
|
||||||
skill_artifact = SkillCompiler().compile_one(
|
skill_entry = SkillCompiler().compile_one(
|
||||||
artifact_set=artifact_set,
|
bundle=bundle,
|
||||||
document=SkillDocument(skill_id="anonymous", content=prompt.text, metadata={}),
|
document=SkillDocument(skill_id="anonymous", content=prompt.text, metadata={}),
|
||||||
file_tree=file_tree,
|
file_tree=file_tree,
|
||||||
base_path=AppAssets.PATH,
|
base_path=AppAssets.PATH,
|
||||||
)
|
)
|
||||||
tool_artifacts.append(skill_artifact.tools)
|
tool_deps_list.append(skill_entry.tools)
|
||||||
|
|
||||||
if len(tool_artifacts) == 0:
|
if len(tool_deps_list) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return reduce(lambda x, y: x.merge(y), tool_artifacts)
|
return reduce(lambda x, y: x.merge(y), tool_deps_list)
|
||||||
|
|
||||||
def _invoke_llm_with_tools(
|
def _invoke_llm_with_tools(
|
||||||
self,
|
self,
|
||||||
@ -1889,11 +1883,11 @@ class LLMNode(Node[LLMNodeData]):
|
|||||||
prompt_messages: Sequence[PromptMessage],
|
prompt_messages: Sequence[PromptMessage],
|
||||||
stop: Sequence[str] | None,
|
stop: Sequence[str] | None,
|
||||||
variable_pool: VariablePool,
|
variable_pool: VariablePool,
|
||||||
tool_artifact: ToolArtifact | None,
|
tool_dependencies: ToolDependencies | None,
|
||||||
) -> Generator[NodeEventBase, None, LLMGenerationData]:
|
) -> Generator[NodeEventBase, None, LLMGenerationData]:
|
||||||
result: LLMGenerationData | None = None
|
result: LLMGenerationData | None = None
|
||||||
|
|
||||||
with SandboxBashSession(sandbox=sandbox, node_id=self.id, tools=tool_artifact) as session:
|
with SandboxBashSession(sandbox=sandbox, node_id=self.id, tools=tool_dependencies) as session:
|
||||||
prompt_files = self._extract_prompt_files(variable_pool)
|
prompt_files = self._extract_prompt_files(variable_pool)
|
||||||
model_features = self._get_model_features(model_instance)
|
model_features = self._get_model_features(model_instance)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from core.app.entities.app_asset_entities import AppAssetFileTree, AppAssetNode
|
from core.app.entities.app_asset_entities import AppAssetFileTree, AppAssetNode
|
||||||
from core.skill.entities.skill_artifact_set import SkillArtifactSet
|
from core.skill.entities.skill_bundle import SkillBundle
|
||||||
from core.skill.entities.skill_document import SkillDocument
|
from core.skill.entities.skill_document import SkillDocument
|
||||||
from core.skill.entities.skill_metadata import FileReference, ToolConfiguration, ToolReference
|
from core.skill.entities.skill_metadata import FileReference, ToolConfiguration, ToolReference
|
||||||
from core.skill.skill_compiler import SkillCompiler
|
from core.skill.skill_compiler import SkillCompiler
|
||||||
@ -48,7 +48,7 @@ class TestSkillCompilerBasic:
|
|||||||
|
|
||||||
# then
|
# then
|
||||||
assert artifact_set.assets_id == "assets-1"
|
assert artifact_set.assets_id == "assets-1"
|
||||||
assert len(artifact_set.items) == 1
|
assert len(artifact_set.entries) == 1
|
||||||
|
|
||||||
artifact = artifact_set.get("skill-1")
|
artifact = artifact_set.get("skill-1")
|
||||||
assert artifact is not None
|
assert artifact is not None
|
||||||
@ -235,7 +235,7 @@ class TestSkillCompilerTransitiveDependencies:
|
|||||||
assert tool_names_c == {"tool_c"}
|
assert tool_names_c == {"tool_c"}
|
||||||
|
|
||||||
|
|
||||||
class TestSkillArtifactSetQueries:
|
class TestSkillBundleQueries:
|
||||||
def test_recompile_group_ids(self):
|
def test_recompile_group_ids(self):
|
||||||
# given
|
# given
|
||||||
# skill-a -> skill-b -> skill-c
|
# skill-a -> skill-b -> skill-c
|
||||||
@ -774,11 +774,11 @@ class TestSkillCompilerComplexScenarios:
|
|||||||
|
|
||||||
# when - serialize and deserialize
|
# when - serialize and deserialize
|
||||||
json_str = original.model_dump_json()
|
json_str = original.model_dump_json()
|
||||||
restored = SkillArtifactSet.model_validate_json(json_str)
|
restored = SkillBundle.model_validate_json(json_str)
|
||||||
|
|
||||||
# then - all data preserved
|
# then - all data preserved
|
||||||
assert restored.assets_id == original.assets_id
|
assert restored.assets_id == original.assets_id
|
||||||
assert len(restored.items) == len(original.items)
|
assert len(restored.entries) == len(original.entries)
|
||||||
assert restored.dependency_graph == original.dependency_graph
|
assert restored.dependency_graph == original.dependency_graph
|
||||||
assert restored.reverse_graph == original.reverse_graph
|
assert restored.reverse_graph == original.reverse_graph
|
||||||
|
|
||||||
@ -836,7 +836,7 @@ class TestSkillCompilerComplexScenarios:
|
|||||||
subset = full_set.subset(["skill-b", "skill-c"])
|
subset = full_set.subset(["skill-b", "skill-c"])
|
||||||
|
|
||||||
# then
|
# then
|
||||||
assert len(subset.items) == 2
|
assert len(subset.entries) == 2
|
||||||
assert subset.get("skill-b") is not None
|
assert subset.get("skill-b") is not None
|
||||||
assert subset.get("skill-c") is not None
|
assert subset.get("skill-c") is not None
|
||||||
assert subset.get("skill-a") is None
|
assert subset.get("skill-a") is None
|
||||||
|
|||||||
Reference in New Issue
Block a user