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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 139 additions & 17 deletions frontend/server/skills/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,119 @@
import tempfile
import zipfile
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Any

from .archive import SkillArchive

DEGRADED_SKILLSPACE_WARNING = "部分关联异常,已恢复可读取技能"


@dataclass(frozen=True)
class SkillSpaceListResult:
items: tuple[dict[str, object], ...]
total_count: int
degraded: bool = False


def _is_missing_skill_relation(error: BaseException) -> bool:
expected = "ResourceNotFound.skill"
for name in ("code", "error_code", "Code"):
if str(getattr(error, name, "") or "").strip() == expected:
return True
return False


def list_skill_space_items(
client: Any,
skills_types: Any,
*,
space_id: str,
page: int = 1,
page_size: int = 100,
) -> SkillSpaceListResult:
"""List authoritative relations, recovering readable names only on one 404."""

try:
response = client.list_skills_by_skill_space(
skills_types.ListSkillsBySkillSpaceRequest(
SkillSpaceId=space_id,
PageNumber=page,
PageSize=page_size,
)
)
except Exception as relation_error:
if not _is_missing_skill_relation(relation_error):
raise
space = client.get_skill_space(skills_types.GetSkillSpaceRequest(Id=space_id))
space_name = str(getattr(space, "name", "") or "").strip()
if not space_name:
raise relation_error
fallback = client.list_skills_by_space_id(
skills_types.ListSkillsBySpaceIdRequest(
SkillSpaceId=space_id,
SkillSpaceName=space_name,
)
)
recovered: list[dict[str, object]] = []
for basic in list(getattr(fallback, "items", None) or []):
name = str(getattr(basic, "name", "") or "").strip()
if not name:
continue
try:
info = client.get_skill_info(
skills_types.GetSkillInfoRequest(
SkillName=name,
SkillSpaceName=space_name,
SkillSpaceId=space_id,
)
)
except Exception as info_error:
if _is_missing_skill_relation(info_error):
continue
raise
recovered.append(
{
"skillId": "",
"skillName": str(getattr(info, "skill_name", "") or name),
"skillDescription": str(
getattr(info, "description", "")
or getattr(basic, "description", "")
or ""
),
"version": "",
"skillStatus": "",
"lookupByName": True,
"degraded": True,
}
)
start = (page - 1) * page_size
return SkillSpaceListResult(
items=tuple(recovered[start : start + page_size]),
total_count=len(recovered),
degraded=True,
)

raw_items = list(getattr(response, "items", None) or [])
return SkillSpaceListResult(
items=tuple(
{
"skillId": str(getattr(item, "skill_id", "") or ""),
"skillName": str(getattr(item, "skill_name", "") or ""),
"skillDescription": str(getattr(item, "skill_description", "") or ""),
"version": str(getattr(item, "version", "") or ""),
"skillStatus": str(getattr(item, "skill_status", "") or ""),
}
for item in raw_items
),
total_count=(
int(response.total_count)
if getattr(response, "total_count", None) is not None
else len(raw_items)
),
)


def _is_macos_metadata(path: PurePosixPath) -> bool:
return bool(path.parts) and (
Expand Down Expand Up @@ -85,6 +193,20 @@ def resolve_skill_response(
"""Read either a managed Skill version or a legacy SkillSpace Skill."""
from agentkit.sdk.skills import types as skills_types

if (
skill_space_name
and skill_name
and not version
and (not skill_id or skill_id == skill_name)
):
return client.get_skill_info(
skills_types.GetSkillInfoRequest(
SkillName=skill_name,
SkillSpaceName=skill_space_name,
SkillSpaceId=space_id,
)
)

try:
return client.get_skill_version(
skills_types.GetSkillVersionRequest(Id=skill_id, SkillVersion=version)
Expand Down Expand Up @@ -514,27 +636,21 @@ def _space_has_skill_named(
page_size = 100
expected = name.casefold()
while True:
response = client.list_skills_by_skill_space(
skills_types.ListSkillsBySkillSpaceRequest(
SkillSpaceId=space_id,
PageNumber=page,
PageSize=page_size,
)
result = list_skill_space_items(
client,
skills_types,
space_id=space_id,
page=page,
page_size=page_size,
)
items = list(getattr(response, "items", None) or [])
items = list(result.items)
if any(
AgentKitSkillRepository._skill_relation_name(item).casefold()
== expected
str(item.get("skillName") or "").casefold() == expected
for item in items
):
return True
total_count = getattr(response, "total_count", None)
if total_count is not None:
try:
if page * page_size >= int(total_count):
return False
except (TypeError, ValueError):
pass
if page * page_size >= result.total_count:
return False
if len(items) < page_size:
return False
page += 1
Expand Down Expand Up @@ -568,4 +684,10 @@ def _space_item(value: Any, region: str) -> dict[str, object]:
}


__all__ = ["AgentKitSkillRepository", "SkillRepositoryError"]
__all__ = [
"AgentKitSkillRepository",
"DEGRADED_SKILLSPACE_WARNING",
"SkillRepositoryError",
"SkillSpaceListResult",
"list_skill_space_items",
]
56 changes: 27 additions & 29 deletions frontend/server/studio_routes/skill_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
import httpx

from frontend.server.agentkit_clients import create_agentkit_client
from frontend.server.skills.repository import (
DEGRADED_SKILLSPACE_WARNING,
list_skill_space_items,
)
from frontend.server.skills.storage import resolve_skill_publish_credentials
from frontend.server.storage import StudioProvider

Expand Down Expand Up @@ -124,19 +128,19 @@ async def list_skills(
space_id: str,
region: str,
) -> dict[str, Any]:
from agentkit.sdk.skills.types import ListSkillsBySkillSpaceRequest
from agentkit.sdk.skills import types as skills_types

if not re.fullmatch(r"[A-Za-z0-9._~-]{1,256}", space_id):
raise StudioSkillCatalogError(400, "invalid Skill Space id")
resolved_region = self.regions(region)[0]
try:
response = await asyncio.to_thread(
self._client(resolved_region).list_skills_by_skill_space,
ListSkillsBySkillSpaceRequest(
SkillSpaceId=space_id,
PageNumber=1,
PageSize=100,
),
result = await asyncio.to_thread(
list_skill_space_items,
self._client(resolved_region),
skills_types,
space_id=space_id,
page=1,
page_size=100,
)
except StudioSkillCatalogError:
raise
Expand All @@ -145,22 +149,16 @@ async def list_skills(
502,
"Studio could not load Skills from this Skill Space.",
) from error
items = list(response.items or [])
return {
"items": [
{
"skillId": skill.skill_id or "",
"skillName": skill.skill_name or "",
"skillDescription": skill.skill_description or "",
"version": skill.version or "",
"skillStatus": skill.skill_status or "",
}
for skill in items
],
"totalCount": (
response.total_count if response.total_count is not None else len(items)
),
payload: dict[str, Any] = {
"items": list(result.items),
"totalCount": result.total_count,
}
if result.degraded:
payload.update(
degraded=True,
warnings=[DEGRADED_SKILLSPACE_WARNING],
)
return payload

async def search_findskill(
self,
Expand Down Expand Up @@ -196,13 +194,13 @@ async def search_findskill(
name = str(raw.get("Name") or "").strip()
if not slug or not name:
continue
metadata = (
raw.get("Metadata") if isinstance(raw.get("Metadata"), dict) else {}
raw_metadata = raw.get("Metadata")
metadata: dict[str, Any] = (
dict(raw_metadata) if isinstance(raw_metadata, dict) else {}
)
evaluation = (
raw.get("EvaluationMetadata")
if isinstance(raw.get("EvaluationMetadata"), dict)
else {}
raw_evaluation = raw.get("EvaluationMetadata")
evaluation: dict[str, Any] = (
dict(raw_evaluation) if isinstance(raw_evaluation, dict) else {}
)
items.append(
{
Expand Down
10 changes: 10 additions & 0 deletions frontend/service/studio_release_server/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,13 @@ def _build_local_requirements(


def _studio_run_script(*, thin: bool = False) -> str:
managed_source = (
"export VEADK_STUDIO_AGENTKIT_CLI_RUNTIME_MANIFEST="
f'"$ROOT_DIR/{_STUDIO_RUNTIME_MANIFEST}"\n'
if thin
else "export VEADK_STUDIO_AGENTKIT_CLI_ARCHIVE="
f'"$ROOT_DIR/{_AGENTKIT_CLI_ARCHIVE}"\n'
)
companion = (
"python3 -m veadk.cli.studio_companion "
f'--runtime-manifest "$ROOT_DIR/{_STUDIO_RUNTIME_MANIFEST}" '
Expand All @@ -1006,6 +1013,9 @@ def _studio_run_script(*, thin: bool = False) -> str:
"#!/bin/bash\n"
"set -ex\n"
'ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"\n'
"unset VEADK_STUDIO_AGENTKIT_CLI_ARCHIVE "
"VEADK_STUDIO_AGENTKIT_CLI_RUNTIME_MANIFEST\n"
f"{managed_source}"
'cd "$ROOT_DIR"\n'
'if [ -d "output" ]; then cd ./output/; fi\n'
"HOST=0.0.0.0\n"
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/adk/runSseError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const TOOL_ARGUMENT_JSON_PATTERN =
/Expecting (?:'[^']+'|\w+)(?: delimiter)?: line \d+ column \d+ \(char \d+\)/i;
const RESOURCE_COLLECTION_EXPIRED_PATTERN =
/Unknown or expired collection_id\s+'[^']+'\.\s*Call collect_resources first\./i;
const MODEL_QUOTA_PATTERN =
/(?:RateLimitError|\bTPM\b|\bRPM\b|tokens? per minute|requests? per minute)[\s\S]*(?:\b429\b|limit|quota)|\b429\b[\s\S]*(?:model|RateLimitError|\bTPM\b|\bRPM\b)/i;

function appendHint(message: string, hint: string): string {
return message.includes(hint) ? message : `${message}\n\n${hint}`;
Expand All @@ -21,6 +23,8 @@ export function formatRunSseError(error: unknown): string {
formatted = appendHint(formatted, adkT("runSse.toolArgumentHint"));
} else if (RESOURCE_COLLECTION_EXPIRED_PATTERN.test(message)) {
return appendHint(formatted, adkT("runSse.resourceCollectionExpiredHint"));
} else if (MODEL_QUOTA_PATTERN.test(message)) {
return appendHint(formatted, adkT("runSse.modelQuotaHint"));
} else if (SESSION_NOT_FOUND_PATTERN.test(message)) {
if (SESSION_DETAIL_PATTERN.test(message)) {
formatted = appendHint(formatted, adkT("runSse.persistentMemoryHint"));
Expand Down
Loading
Loading