agno 2.2.13__py3-none-any.whl → 2.4.3__py3-none-any.whl
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.
- agno/agent/__init__.py +6 -0
- agno/agent/agent.py +5252 -3145
- agno/agent/remote.py +525 -0
- agno/api/api.py +2 -0
- agno/client/__init__.py +3 -0
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/client/os.py +2669 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/manager.py +2 -2
- agno/db/base.py +927 -6
- agno/db/dynamo/dynamo.py +788 -2
- agno/db/dynamo/schemas.py +128 -0
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/firestore.py +674 -50
- agno/db/firestore/schemas.py +41 -0
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/gcs_json_db.py +506 -3
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/in_memory_db.py +203 -4
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/json_db.py +498 -2
- agno/db/json/utils.py +14 -2
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +977 -0
- agno/db/mongo/async_mongo.py +1013 -39
- agno/db/mongo/mongo.py +684 -4
- agno/db/mongo/schemas.py +48 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2958 -0
- agno/db/mysql/mysql.py +722 -53
- agno/db/mysql/schemas.py +77 -11
- agno/db/mysql/utils.py +151 -8
- agno/db/postgres/async_postgres.py +1254 -137
- agno/db/postgres/postgres.py +2316 -93
- agno/db/postgres/schemas.py +153 -21
- agno/db/postgres/utils.py +22 -7
- agno/db/redis/redis.py +531 -3
- agno/db/redis/schemas.py +36 -0
- agno/db/redis/utils.py +31 -15
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +20 -9
- agno/db/singlestore/schemas.py +70 -1
- agno/db/singlestore/singlestore.py +737 -74
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/async_sqlite.py +1069 -89
- agno/db/sqlite/schemas.py +133 -1
- agno/db/sqlite/sqlite.py +2203 -165
- agno/db/sqlite/utils.py +21 -11
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +603 -1
- agno/db/utils.py +60 -0
- agno/eval/__init__.py +26 -3
- agno/eval/accuracy.py +25 -12
- agno/eval/agent_as_judge.py +871 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/eval/utils.py +2 -1
- agno/exceptions.py +42 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +13 -2
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +65 -4
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/markdown.py +102 -11
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/chunking/semantic.py +130 -48
- agno/knowledge/chunking/strategy.py +18 -0
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/knowledge/embedder/mistral.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/openai.py +16 -12
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +4261 -1199
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +3 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +91 -42
- agno/knowledge/reader/docx_reader.py +9 -10
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
- agno/knowledge/reader/firecrawl_reader.py +3 -2
- agno/knowledge/reader/json_reader.py +16 -22
- agno/knowledge/reader/markdown_reader.py +15 -14
- agno/knowledge/reader/pdf_reader.py +33 -28
- agno/knowledge/reader/pptx_reader.py +9 -10
- agno/knowledge/reader/reader_factory.py +135 -1
- agno/knowledge/reader/s3_reader.py +8 -16
- agno/knowledge/reader/tavily_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +15 -14
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/knowledge/reader/web_search_reader.py +8 -65
- agno/knowledge/reader/website_reader.py +16 -13
- agno/knowledge/reader/wikipedia_reader.py +36 -3
- agno/knowledge/reader/youtube_reader.py +3 -2
- agno/knowledge/remote_content/__init__.py +33 -0
- agno/knowledge/remote_content/config.py +266 -0
- agno/knowledge/remote_content/remote_content.py +105 -17
- agno/knowledge/utils.py +76 -22
- agno/learn/__init__.py +71 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +725 -0
- agno/learn/schemas.py +1114 -0
- agno/learn/stores/__init__.py +38 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +223 -8
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +434 -59
- agno/models/aws/bedrock.py +121 -20
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +10 -6
- agno/models/azure/openai_chat.py +33 -10
- agno/models/base.py +1162 -561
- agno/models/cerebras/cerebras.py +120 -24
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +65 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +959 -89
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +48 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +88 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +24 -5
- agno/models/meta/llama.py +40 -13
- agno/models/meta/llama_openai.py +22 -21
- agno/models/metrics.py +12 -0
- agno/models/mistral/mistral.py +8 -4
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/chat.py +17 -6
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/chat.py +117 -26
- agno/models/openai/open_responses.py +46 -0
- agno/models/openai/responses.py +110 -32
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/openrouter.py +67 -2
- agno/models/openrouter/responses.py +146 -0
- agno/models/perplexity/perplexity.py +19 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +19 -2
- agno/models/response.py +20 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/claude.py +124 -4
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +467 -137
- agno/os/auth.py +253 -5
- agno/os/config.py +22 -0
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +635 -26
- agno/os/interfaces/a2a/utils.py +32 -33
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +26 -16
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +16 -7
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +35 -7
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +652 -79
- agno/os/middleware/__init__.py +4 -0
- agno/os/middleware/jwt.py +718 -115
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +105 -1558
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +655 -0
- agno/os/routers/agents/schema.py +288 -0
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/database.py +155 -0
- agno/os/routers/evals/evals.py +111 -18
- agno/os/routers/evals/schemas.py +38 -5
- agno/os/routers/evals/utils.py +80 -11
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +284 -35
- agno/os/routers/knowledge/schemas.py +14 -2
- agno/os/routers/memory/memory.py +274 -11
- agno/os/routers/memory/schemas.py +44 -3
- agno/os/routers/metrics/metrics.py +30 -15
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/session/session.py +143 -14
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +550 -0
- agno/os/routers/teams/schema.py +280 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +549 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +757 -0
- agno/os/routers/workflows/schema.py +139 -0
- agno/os/schema.py +157 -584
- agno/os/scopes.py +469 -0
- agno/os/settings.py +3 -0
- agno/os/utils.py +574 -185
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +102 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +87 -3
- agno/reasoning/groq.py +109 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +1238 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +115 -1
- agno/reasoning/vertexai.py +85 -1
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +581 -0
- agno/run/__init__.py +2 -4
- agno/run/agent.py +134 -19
- agno/run/base.py +49 -1
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +111 -19
- agno/run/workflow.py +2 -1
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -115
- agno/session/workflow.py +353 -57
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +377 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/table.py +10 -0
- agno/team/__init__.py +5 -1
- agno/team/remote.py +447 -0
- agno/team/team.py +3769 -2202
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +225 -16
- agno/tools/crawl4ai.py +3 -0
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/file.py +14 -13
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +94 -113
- agno/tools/google_bigquery.py +11 -2
- agno/tools/google_drive.py +4 -3
- agno/tools/knowledge.py +9 -4
- agno/tools/mcp/mcp.py +301 -18
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/mlx_transcribe.py +10 -7
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/parallel.py +0 -7
- agno/tools/postgres.py +76 -36
- agno/tools/python.py +14 -6
- agno/tools/reasoning.py +30 -23
- agno/tools/redshift.py +406 -0
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +4 -1
- agno/tools/toolkit.py +253 -18
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +56 -48
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +161 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +112 -0
- agno/utils/agent.py +251 -10
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +264 -7
- agno/utils/hooks.py +111 -3
- agno/utils/http.py +161 -2
- agno/utils/mcp.py +49 -8
- agno/utils/media.py +22 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +20 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/os.py +0 -0
- agno/utils/print_response/agent.py +99 -16
- agno/utils/print_response/team.py +223 -24
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/prompts.py +8 -6
- agno/utils/remote.py +23 -0
- agno/utils/response.py +1 -13
- agno/utils/string.py +91 -2
- agno/utils/team.py +62 -12
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +15 -2
- agno/vectordb/cassandra/cassandra.py +1 -1
- agno/vectordb/chroma/__init__.py +2 -1
- agno/vectordb/chroma/chromadb.py +468 -23
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/vectordb/lancedb/lance_db.py +7 -38
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +118 -84
- agno/vectordb/mongodb/__init__.py +2 -1
- agno/vectordb/mongodb/mongodb.py +14 -31
- agno/vectordb/pgvector/pgvector.py +120 -66
- agno/vectordb/pineconedb/pineconedb.py +2 -19
- agno/vectordb/qdrant/__init__.py +2 -1
- agno/vectordb/qdrant/qdrant.py +33 -56
- agno/vectordb/redis/__init__.py +2 -1
- agno/vectordb/redis/redisdb.py +19 -31
- agno/vectordb/singlestore/singlestore.py +17 -9
- agno/vectordb/surrealdb/surrealdb.py +2 -38
- agno/vectordb/weaviate/__init__.py +2 -1
- agno/vectordb/weaviate/weaviate.py +7 -3
- agno/workflow/__init__.py +5 -1
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +12 -10
- agno/workflow/loop.py +28 -9
- agno/workflow/parallel.py +21 -13
- agno/workflow/remote.py +362 -0
- agno/workflow/router.py +12 -9
- agno/workflow/step.py +261 -36
- agno/workflow/steps.py +12 -8
- agno/workflow/types.py +40 -77
- agno/workflow/workflow.py +939 -213
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
- agno-2.4.3.dist-info/RECORD +677 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
- agno/tools/googlesearch.py +0 -98
- agno/tools/memori.py +0 -339
- agno-2.2.13.dist-info/RECORD +0 -575
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from agno.skills.errors import SkillValidationError
|
|
7
|
+
from agno.skills.loaders.base import SkillLoader
|
|
8
|
+
from agno.skills.skill import Skill
|
|
9
|
+
from agno.skills.utils import is_safe_path, read_file_safe, run_script
|
|
10
|
+
from agno.tools.function import Function
|
|
11
|
+
from agno.utils.log import log_debug, log_warning
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Skills:
|
|
15
|
+
"""Orchestrates skill loading and provides tools for agents to access skills.
|
|
16
|
+
|
|
17
|
+
The Skills class is responsible for:
|
|
18
|
+
1. Loading skills from various sources (loaders)
|
|
19
|
+
2. Providing methods to access loaded skills
|
|
20
|
+
3. Generating tools for agents to use skills
|
|
21
|
+
4. Creating system prompt snippets with available skills metadata
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
loaders: List of SkillLoader instances to load skills from.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, loaders: List[SkillLoader]):
|
|
28
|
+
self.loaders = loaders
|
|
29
|
+
self._skills: Dict[str, Skill] = {}
|
|
30
|
+
self._load_skills()
|
|
31
|
+
|
|
32
|
+
def _load_skills(self) -> None:
|
|
33
|
+
"""Load skills from all loaders.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
SkillValidationError: If any skill fails validation.
|
|
37
|
+
"""
|
|
38
|
+
for loader in self.loaders:
|
|
39
|
+
try:
|
|
40
|
+
skills = loader.load()
|
|
41
|
+
for skill in skills:
|
|
42
|
+
if skill.name in self._skills:
|
|
43
|
+
log_warning(f"Duplicate skill name '{skill.name}', overwriting with newer version")
|
|
44
|
+
self._skills[skill.name] = skill
|
|
45
|
+
except SkillValidationError:
|
|
46
|
+
raise # Re-raise validation errors as hard failures
|
|
47
|
+
except Exception as e:
|
|
48
|
+
log_warning(f"Error loading skills from {loader}: {e}")
|
|
49
|
+
|
|
50
|
+
log_debug(f"Loaded {len(self._skills)} total skills")
|
|
51
|
+
|
|
52
|
+
def reload(self) -> None:
|
|
53
|
+
"""Reload skills from all loaders, clearing existing skills.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
SkillValidationError: If any skill fails validation.
|
|
57
|
+
"""
|
|
58
|
+
self._skills.clear()
|
|
59
|
+
self._load_skills()
|
|
60
|
+
|
|
61
|
+
def get_skill(self, name: str) -> Optional[Skill]:
|
|
62
|
+
"""Get a skill by name.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
name: The name of the skill to retrieve.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The Skill object if found, None otherwise.
|
|
69
|
+
"""
|
|
70
|
+
return self._skills.get(name)
|
|
71
|
+
|
|
72
|
+
def get_all_skills(self) -> List[Skill]:
|
|
73
|
+
"""Get all loaded skills.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
A list of all loaded Skill objects.
|
|
77
|
+
"""
|
|
78
|
+
return list(self._skills.values())
|
|
79
|
+
|
|
80
|
+
def get_skill_names(self) -> List[str]:
|
|
81
|
+
"""Get the names of all loaded skills.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
A list of skill names.
|
|
85
|
+
"""
|
|
86
|
+
return list(self._skills.keys())
|
|
87
|
+
|
|
88
|
+
def get_system_prompt_snippet(self) -> str:
|
|
89
|
+
"""Generate a system prompt snippet with available skills metadata.
|
|
90
|
+
|
|
91
|
+
This creates an XML-formatted snippet that provides the agent with
|
|
92
|
+
information about available skills without including the full instructions.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
An XML-formatted string with skills metadata.
|
|
96
|
+
"""
|
|
97
|
+
if not self._skills:
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
lines = [
|
|
101
|
+
"<skills_system>",
|
|
102
|
+
"",
|
|
103
|
+
"## What are Skills?",
|
|
104
|
+
"Skills are packages of domain expertise that extend your capabilities. Each skill contains:",
|
|
105
|
+
"- **Instructions**: Detailed guidance on when and how to apply the skill",
|
|
106
|
+
"- **Scripts**: Executable code templates you can use or adapt",
|
|
107
|
+
"- **References**: Supporting documentation (guides, cheatsheets, examples)",
|
|
108
|
+
"",
|
|
109
|
+
"## IMPORTANT: How to Use Skills",
|
|
110
|
+
"**Skill names are NOT callable functions.** You cannot call a skill directly by its name.",
|
|
111
|
+
"Instead, you MUST use the provided skill access tools:",
|
|
112
|
+
"",
|
|
113
|
+
"1. `get_skill_instructions(skill_name)` - Load the full instructions for a skill",
|
|
114
|
+
"2. `get_skill_reference(skill_name, reference_path)` - Access specific documentation",
|
|
115
|
+
"3. `get_skill_script(skill_name, script_path, execute=False)` - Read or run scripts",
|
|
116
|
+
"",
|
|
117
|
+
"## Progressive Discovery Workflow",
|
|
118
|
+
"1. **Browse**: Review the skill summaries below to understand what's available",
|
|
119
|
+
"2. **Load**: When a task matches a skill, call `get_skill_instructions(skill_name)` first",
|
|
120
|
+
"3. **Reference**: Use `get_skill_reference` to access specific documentation as needed",
|
|
121
|
+
"4. **Scripts**: Use `get_skill_script` to read or execute scripts from a skill",
|
|
122
|
+
"",
|
|
123
|
+
"This approach ensures you only load detailed instructions when actually needed.",
|
|
124
|
+
"",
|
|
125
|
+
"## Available Skills",
|
|
126
|
+
]
|
|
127
|
+
for skill in self._skills.values():
|
|
128
|
+
lines.append("<skill>")
|
|
129
|
+
lines.append(f" <name>{skill.name}</name>")
|
|
130
|
+
lines.append(f" <description>{skill.description}</description>")
|
|
131
|
+
if skill.scripts:
|
|
132
|
+
script_names = [s["name"] if isinstance(s, dict) else s for s in skill.scripts]
|
|
133
|
+
lines.append(f" <scripts>{', '.join(script_names)}</scripts>")
|
|
134
|
+
if skill.references:
|
|
135
|
+
ref_names = [r["name"] if isinstance(r, dict) else r for r in skill.references]
|
|
136
|
+
lines.append(f" <references>{', '.join(ref_names)}</references>")
|
|
137
|
+
lines.append("</skill>")
|
|
138
|
+
lines.append("")
|
|
139
|
+
lines.append("</skills_system>")
|
|
140
|
+
|
|
141
|
+
return "\n".join(lines)
|
|
142
|
+
|
|
143
|
+
def get_tools(self) -> List[Function]:
|
|
144
|
+
"""Get the tools for accessing skills.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
A list of Function objects that agents can use to access skills.
|
|
148
|
+
"""
|
|
149
|
+
tools: List[Function] = []
|
|
150
|
+
|
|
151
|
+
# Tool: get_skill_instructions
|
|
152
|
+
tools.append(
|
|
153
|
+
Function(
|
|
154
|
+
name="get_skill_instructions",
|
|
155
|
+
description="Load the full instructions for a skill. Use this when you need to follow a skill's guidance.",
|
|
156
|
+
entrypoint=self._get_skill_instructions,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Tool: get_skill_reference
|
|
161
|
+
tools.append(
|
|
162
|
+
Function(
|
|
163
|
+
name="get_skill_reference",
|
|
164
|
+
description="Load a reference document from a skill's references. Use this to access detailed documentation.",
|
|
165
|
+
entrypoint=self._get_skill_reference,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Tool: get_skill_script
|
|
170
|
+
tools.append(
|
|
171
|
+
Function(
|
|
172
|
+
name="get_skill_script",
|
|
173
|
+
description="Read or execute a script from a skill. Set execute=True to run the script and get output, or execute=False (default) to read the script content.",
|
|
174
|
+
entrypoint=self._get_skill_script,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return tools
|
|
179
|
+
|
|
180
|
+
def _get_skill_instructions(self, skill_name: str) -> str:
|
|
181
|
+
"""Load the full instructions for a skill.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
skill_name: The name of the skill to get instructions for.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
A JSON string with the skill's instructions and metadata.
|
|
188
|
+
"""
|
|
189
|
+
skill = self.get_skill(skill_name)
|
|
190
|
+
if skill is None:
|
|
191
|
+
available = ", ".join(self.get_skill_names())
|
|
192
|
+
return json.dumps(
|
|
193
|
+
{
|
|
194
|
+
"error": f"Skill '{skill_name}' not found",
|
|
195
|
+
"available_skills": available,
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return json.dumps(
|
|
200
|
+
{
|
|
201
|
+
"skill_name": skill.name,
|
|
202
|
+
"description": skill.description,
|
|
203
|
+
"instructions": skill.instructions,
|
|
204
|
+
"available_scripts": skill.scripts,
|
|
205
|
+
"available_references": skill.references,
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def _get_skill_reference(self, skill_name: str, reference_path: str) -> str:
|
|
210
|
+
"""Load a reference document from a skill.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
skill_name: The name of the skill.
|
|
214
|
+
reference_path: The filename of the reference document.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
A JSON string with the reference content.
|
|
218
|
+
"""
|
|
219
|
+
skill = self.get_skill(skill_name)
|
|
220
|
+
if skill is None:
|
|
221
|
+
available = ", ".join(self.get_skill_names())
|
|
222
|
+
return json.dumps(
|
|
223
|
+
{
|
|
224
|
+
"error": f"Skill '{skill_name}' not found",
|
|
225
|
+
"available_skills": available,
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if reference_path not in skill.references:
|
|
230
|
+
return json.dumps(
|
|
231
|
+
{
|
|
232
|
+
"error": f"Reference '{reference_path}' not found in skill '{skill_name}'",
|
|
233
|
+
"available_references": skill.references,
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Validate path to prevent path traversal attacks
|
|
238
|
+
refs_dir = Path(skill.source_path) / "references"
|
|
239
|
+
if not is_safe_path(refs_dir, reference_path):
|
|
240
|
+
return json.dumps(
|
|
241
|
+
{
|
|
242
|
+
"error": f"Invalid reference path: '{reference_path}'",
|
|
243
|
+
"skill_name": skill_name,
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Load the reference file
|
|
248
|
+
ref_file = refs_dir / reference_path
|
|
249
|
+
try:
|
|
250
|
+
content = read_file_safe(ref_file)
|
|
251
|
+
return json.dumps(
|
|
252
|
+
{
|
|
253
|
+
"skill_name": skill_name,
|
|
254
|
+
"reference_path": reference_path,
|
|
255
|
+
"content": content,
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
except Exception as e:
|
|
259
|
+
return json.dumps(
|
|
260
|
+
{
|
|
261
|
+
"error": f"Error reading reference file: {e}",
|
|
262
|
+
"skill_name": skill_name,
|
|
263
|
+
"reference_path": reference_path,
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def _get_skill_script(
|
|
268
|
+
self,
|
|
269
|
+
skill_name: str,
|
|
270
|
+
script_path: str,
|
|
271
|
+
execute: bool = False,
|
|
272
|
+
args: Optional[List[str]] = None,
|
|
273
|
+
timeout: int = 30,
|
|
274
|
+
) -> str:
|
|
275
|
+
"""Read or execute a script from a skill.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
skill_name: The name of the skill.
|
|
279
|
+
script_path: The filename of the script.
|
|
280
|
+
execute: If True, execute the script. If False (default), return content.
|
|
281
|
+
args: Optional list of arguments to pass to the script (only used if execute=True).
|
|
282
|
+
timeout: Maximum execution time in seconds (default: 30, only used if execute=True).
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
A JSON string with either the script content or execution results.
|
|
286
|
+
"""
|
|
287
|
+
skill = self.get_skill(skill_name)
|
|
288
|
+
if skill is None:
|
|
289
|
+
available = ", ".join(self.get_skill_names())
|
|
290
|
+
return json.dumps(
|
|
291
|
+
{
|
|
292
|
+
"error": f"Skill '{skill_name}' not found",
|
|
293
|
+
"available_skills": available,
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if script_path not in skill.scripts:
|
|
298
|
+
return json.dumps(
|
|
299
|
+
{
|
|
300
|
+
"error": f"Script '{script_path}' not found in skill '{skill_name}'",
|
|
301
|
+
"available_scripts": skill.scripts,
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Validate path to prevent path traversal attacks
|
|
306
|
+
scripts_dir = Path(skill.source_path) / "scripts"
|
|
307
|
+
if not is_safe_path(scripts_dir, script_path):
|
|
308
|
+
return json.dumps(
|
|
309
|
+
{
|
|
310
|
+
"error": f"Invalid script path: '{script_path}'",
|
|
311
|
+
"skill_name": skill_name,
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
script_file = scripts_dir / script_path
|
|
316
|
+
|
|
317
|
+
if not execute:
|
|
318
|
+
# Read mode: return script content
|
|
319
|
+
try:
|
|
320
|
+
content = read_file_safe(script_file)
|
|
321
|
+
return json.dumps(
|
|
322
|
+
{
|
|
323
|
+
"skill_name": skill_name,
|
|
324
|
+
"script_path": script_path,
|
|
325
|
+
"content": content,
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
except Exception as e:
|
|
329
|
+
return json.dumps(
|
|
330
|
+
{
|
|
331
|
+
"error": f"Error reading script file: {e}",
|
|
332
|
+
"skill_name": skill_name,
|
|
333
|
+
"script_path": script_path,
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Execute mode: run the script
|
|
338
|
+
try:
|
|
339
|
+
result = run_script(
|
|
340
|
+
script_path=script_file,
|
|
341
|
+
args=args,
|
|
342
|
+
timeout=timeout,
|
|
343
|
+
cwd=Path(skill.source_path),
|
|
344
|
+
)
|
|
345
|
+
return json.dumps(
|
|
346
|
+
{
|
|
347
|
+
"skill_name": skill_name,
|
|
348
|
+
"script_path": script_path,
|
|
349
|
+
"stdout": result.stdout,
|
|
350
|
+
"stderr": result.stderr,
|
|
351
|
+
"returncode": result.returncode,
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
except subprocess.TimeoutExpired:
|
|
355
|
+
return json.dumps(
|
|
356
|
+
{
|
|
357
|
+
"error": f"Script execution timed out after {timeout} seconds",
|
|
358
|
+
"skill_name": skill_name,
|
|
359
|
+
"script_path": script_path,
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
except FileNotFoundError as e:
|
|
363
|
+
return json.dumps(
|
|
364
|
+
{
|
|
365
|
+
"error": f"Interpreter or script not found: {e}",
|
|
366
|
+
"skill_name": skill_name,
|
|
367
|
+
"script_path": script_path,
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
except Exception as e:
|
|
371
|
+
return json.dumps(
|
|
372
|
+
{
|
|
373
|
+
"error": f"Error executing script: {e}",
|
|
374
|
+
"skill_name": skill_name,
|
|
375
|
+
"script_path": script_path,
|
|
376
|
+
}
|
|
377
|
+
)
|
agno/skills/errors.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Skill-related exceptions."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SkillError(Exception):
|
|
7
|
+
"""Base exception for all skill-related errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SkillParseError(SkillError):
|
|
13
|
+
"""Raised when SKILL.md parsing fails."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SkillValidationError(SkillError):
|
|
19
|
+
"""Raised when skill validation fails.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
errors: List of validation error messages.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str, errors: Optional[List[str]] = None):
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.errors = errors if errors is not None else [message]
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
if len(self.errors) == 1:
|
|
31
|
+
return self.errors[0]
|
|
32
|
+
return f"{len(self.errors)} validation errors: {'; '.join(self.errors)}"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from agno.skills.skill import Skill
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SkillLoader(ABC):
|
|
8
|
+
"""Abstract base class for skill loaders.
|
|
9
|
+
|
|
10
|
+
Skill loaders are responsible for loading skills from various sources
|
|
11
|
+
(local filesystem, GitHub, URLs, etc.) and returning them as Skill objects.
|
|
12
|
+
|
|
13
|
+
Subclasses must implement the `load()` method to define how skills
|
|
14
|
+
are loaded from their specific source.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def load(self) -> List[Skill]:
|
|
19
|
+
"""Load skills from the source.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
A list of Skill objects loaded from the source.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
SkillLoadError: If there's an error loading skills from the source.
|
|
26
|
+
"""
|
|
27
|
+
pass
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from agno.skills.errors import SkillValidationError
|
|
6
|
+
from agno.skills.loaders.base import SkillLoader
|
|
7
|
+
from agno.skills.skill import Skill
|
|
8
|
+
from agno.skills.validator import validate_skill_directory
|
|
9
|
+
from agno.utils.log import log_debug, log_warning
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LocalSkills(SkillLoader):
|
|
13
|
+
"""Loads skills from the local filesystem.
|
|
14
|
+
|
|
15
|
+
This loader can handle both:
|
|
16
|
+
1. A single skill folder (contains SKILL.md)
|
|
17
|
+
2. A directory containing multiple skill folders
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
path: Path to a skill folder or directory containing skill folders.
|
|
21
|
+
validate: Whether to validate skills against the Agent Skills spec.
|
|
22
|
+
If True (default), invalid skills will raise SkillValidationError.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, path: str, validate: bool = True):
|
|
26
|
+
self.path = Path(path).resolve()
|
|
27
|
+
self.validate = validate
|
|
28
|
+
|
|
29
|
+
def load(self) -> List[Skill]:
|
|
30
|
+
"""Load skills from the local filesystem.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A list of Skill objects loaded from the filesystem.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
FileNotFoundError: If the path doesn't exist.
|
|
37
|
+
"""
|
|
38
|
+
if not self.path.exists():
|
|
39
|
+
raise FileNotFoundError(f"Skills path does not exist: {self.path}")
|
|
40
|
+
|
|
41
|
+
skills: List[Skill] = []
|
|
42
|
+
|
|
43
|
+
# Check if this is a single skill folder or a directory of skills
|
|
44
|
+
skill_md_path = self.path / "SKILL.md"
|
|
45
|
+
if skill_md_path.exists():
|
|
46
|
+
# Single skill folder
|
|
47
|
+
skill = self._load_skill_from_folder(self.path)
|
|
48
|
+
if skill:
|
|
49
|
+
skills.append(skill)
|
|
50
|
+
else:
|
|
51
|
+
# Directory of skill folders
|
|
52
|
+
for item in self.path.iterdir():
|
|
53
|
+
if item.is_dir() and not item.name.startswith("."):
|
|
54
|
+
skill_md = item / "SKILL.md"
|
|
55
|
+
if skill_md.exists():
|
|
56
|
+
skill = self._load_skill_from_folder(item)
|
|
57
|
+
if skill:
|
|
58
|
+
skills.append(skill)
|
|
59
|
+
else:
|
|
60
|
+
log_debug(f"Skipping directory without SKILL.md: {item}")
|
|
61
|
+
|
|
62
|
+
log_debug(f"Loaded {len(skills)} skills from {self.path}")
|
|
63
|
+
return skills
|
|
64
|
+
|
|
65
|
+
def _load_skill_from_folder(self, folder: Path) -> Optional[Skill]:
|
|
66
|
+
"""Load a single skill from a folder.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
folder: Path to the skill folder.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A Skill object if successful, None if there's an error.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
SkillValidationError: If validation is enabled and the skill is invalid.
|
|
76
|
+
"""
|
|
77
|
+
# Validate skill directory structure and content if validation is enabled
|
|
78
|
+
if self.validate:
|
|
79
|
+
errors = validate_skill_directory(folder)
|
|
80
|
+
if errors:
|
|
81
|
+
raise SkillValidationError(
|
|
82
|
+
f"Skill validation failed for '{folder.name}'",
|
|
83
|
+
errors=errors,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
skill_md_path = folder / "SKILL.md"
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
content = skill_md_path.read_text(encoding="utf-8")
|
|
90
|
+
frontmatter, instructions = self._parse_skill_md(content)
|
|
91
|
+
|
|
92
|
+
# Get skill name from the frontmatter or folder name
|
|
93
|
+
name = frontmatter.get("name", folder.name)
|
|
94
|
+
description = frontmatter.get("description", "")
|
|
95
|
+
|
|
96
|
+
# Get optional fields
|
|
97
|
+
license_info = frontmatter.get("license")
|
|
98
|
+
metadata = frontmatter.get("metadata")
|
|
99
|
+
compatibility = frontmatter.get("compatibility")
|
|
100
|
+
allowed_tools = frontmatter.get("allowed-tools")
|
|
101
|
+
|
|
102
|
+
# Discover scripts
|
|
103
|
+
scripts = self._discover_scripts(folder)
|
|
104
|
+
|
|
105
|
+
# Discover references
|
|
106
|
+
references = self._discover_references(folder)
|
|
107
|
+
|
|
108
|
+
return Skill(
|
|
109
|
+
name=name,
|
|
110
|
+
description=description,
|
|
111
|
+
instructions=instructions,
|
|
112
|
+
source_path=str(folder),
|
|
113
|
+
scripts=scripts,
|
|
114
|
+
references=references,
|
|
115
|
+
metadata=metadata,
|
|
116
|
+
license=license_info,
|
|
117
|
+
compatibility=compatibility,
|
|
118
|
+
allowed_tools=allowed_tools,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
except SkillValidationError:
|
|
122
|
+
raise # Re-raise validation errors
|
|
123
|
+
except Exception as e:
|
|
124
|
+
log_warning(f"Error loading skill from {folder}: {e}")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def _parse_skill_md(self, content: str) -> Tuple[Dict[str, Any], str]:
|
|
128
|
+
"""Parse SKILL.md content into frontmatter and instructions.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
content: The raw SKILL.md content.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
A tuple of (frontmatter_dict, instructions_body).
|
|
135
|
+
"""
|
|
136
|
+
frontmatter: Dict[str, Any] = {}
|
|
137
|
+
instructions = content
|
|
138
|
+
|
|
139
|
+
# Check for YAML frontmatter (between --- delimiters)
|
|
140
|
+
frontmatter_match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", content, re.DOTALL)
|
|
141
|
+
|
|
142
|
+
if frontmatter_match:
|
|
143
|
+
frontmatter_text = frontmatter_match.group(1)
|
|
144
|
+
instructions = frontmatter_match.group(2).strip()
|
|
145
|
+
|
|
146
|
+
# Parse YAML frontmatter
|
|
147
|
+
try:
|
|
148
|
+
import yaml
|
|
149
|
+
|
|
150
|
+
frontmatter = yaml.safe_load(frontmatter_text) or {}
|
|
151
|
+
except ImportError:
|
|
152
|
+
# Fallback: simple key-value parsing if yaml not available
|
|
153
|
+
frontmatter = self._parse_simple_frontmatter(frontmatter_text)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
log_warning(f"Error parsing YAML frontmatter: {e}")
|
|
156
|
+
frontmatter = self._parse_simple_frontmatter(frontmatter_text)
|
|
157
|
+
|
|
158
|
+
return frontmatter, instructions
|
|
159
|
+
|
|
160
|
+
def _parse_simple_frontmatter(self, text: str) -> Dict[str, Any]:
|
|
161
|
+
"""Simple fallback frontmatter parser for basic key: value pairs.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
text: The frontmatter text.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
A dictionary of parsed key-value pairs.
|
|
168
|
+
"""
|
|
169
|
+
result: Dict[str, Any] = {}
|
|
170
|
+
for line in text.strip().split("\n"):
|
|
171
|
+
if ":" in line:
|
|
172
|
+
key, value = line.split(":", 1)
|
|
173
|
+
key = key.strip()
|
|
174
|
+
value = value.strip().strip('"').strip("'")
|
|
175
|
+
result[key] = value
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
def _discover_scripts(self, folder: Path) -> List[str]:
|
|
179
|
+
"""Discover script files in the scripts/ subdirectory.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
folder: Path to the skill folder.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
A list of script filenames.
|
|
186
|
+
"""
|
|
187
|
+
scripts_dir = folder / "scripts"
|
|
188
|
+
if not scripts_dir.exists() or not scripts_dir.is_dir():
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
scripts: List[str] = []
|
|
192
|
+
for item in scripts_dir.iterdir():
|
|
193
|
+
if item.is_file() and not item.name.startswith("."):
|
|
194
|
+
scripts.append(item.name)
|
|
195
|
+
|
|
196
|
+
return sorted(scripts)
|
|
197
|
+
|
|
198
|
+
def _discover_references(self, folder: Path) -> List[str]:
|
|
199
|
+
"""Discover reference files in the references/ subdirectory.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
folder: Path to the skill folder.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
A list of reference filenames.
|
|
206
|
+
"""
|
|
207
|
+
refs_dir = folder / "references"
|
|
208
|
+
if not refs_dir.exists() or not refs_dir.is_dir():
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
references: List[str] = []
|
|
212
|
+
for item in refs_dir.iterdir():
|
|
213
|
+
if item.is_file() and not item.name.startswith("."):
|
|
214
|
+
references.append(item.name)
|
|
215
|
+
|
|
216
|
+
return sorted(references)
|