Skip to content

Commit e4b99af

Browse files
authored
Merge pull request #5 from FlowLLM-AI/dev_skills
op for skills
2 parents 419a4c3 + de43061 commit e4b99af

31 files changed

+1051
-232
lines changed

docs/zh/todo.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@
1616
- [ ] github action
1717
- [ ] contribution guide
1818
- [ ] ah feature & backtest
19+
- [ ] Look at the Bollinger Bands range with 95% standard deviation.
20+

flowllm/config/default.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,26 @@ flow:
4040
description: "user query"
4141
required: true
4242

43+
skill_agent_flow:
44+
flow_content: |
45+
skill_op = SkillAgentOp()
46+
skill_op.ops.load_metadata = LoadSkillMetadataOp()
47+
skill_op.ops.load_skill = LoadSkillOp()
48+
skill_op.ops.read_reference = ReadReferenceFileOp()
49+
skill_op.ops.run_shell = RunShellCommandOp()
50+
skill_op
51+
description: "Automatically uses pre-built Skills relevant to the query when needed."
52+
input_schema:
53+
query:
54+
type: string
55+
description: "query"
56+
required: true
57+
skill_dir:
58+
type: string
59+
description: "skill dir"
60+
required: true
61+
62+
4363
llm:
4464
default:
4565
backend: openai_compatible

flowllm/core/context/base_context.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ def keys(self):
127127
"""
128128
return self._data.keys()
129129

130+
def values(self):
131+
"""Get all values in the context.
132+
133+
Returns:
134+
A view object of all values in the context.
135+
"""
136+
return self._data.values()
137+
130138
def update(self, kwargs: dict):
131139
"""Update context with new key-value pairs.
132140

flowllm/core/op/base_async_tool_op.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def output_keys(self) -> str | List[str]:
137137
return output_keys
138138

139139
@property
140-
def output(self) -> str:
140+
def output(self):
141141
"""Convenience accessor for the primary output value.
142142
143143
Raises:

flowllm/core/op/base_op.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def __init__(
105105
name: str = "",
106106
async_mode: bool = False,
107107
max_retries: int = 1,
108-
raise_exception: bool = True,
108+
raise_exception: bool = False,
109109
enable_multithread: bool = True,
110110
language: str = "",
111111
prompt_path: str = "",

flowllm/core/schema/message.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,12 @@ class Message(BaseModel):
2020
time_created: str = Field(default_factory=lambda: datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
2121
metadata: dict = Field(default_factory=dict)
2222

23-
def simple_dump(self, add_reason_content: bool = True) -> dict:
23+
def simple_dump(self, add_reasoning: bool = False) -> dict:
2424
"""Convert Message to a simple dictionary format for API serialization."""
25-
result: dict
26-
if self.content:
27-
result = {"role": self.role.value, "content": self.content}
28-
elif add_reason_content and self.reasoning_content:
29-
result = {"role": self.role.value, "content": self.reasoning_content}
30-
else:
31-
result = {"role": self.role.value, "content": ""}
25+
result: dict = {"role": self.role.value, "content": self.content}
26+
27+
if add_reasoning:
28+
result["reasoning_content"] = self.reasoning_content
3229

3330
if self.tool_calls:
3431
result["tool_calls"] = [x.simple_output_dump() for x in self.tool_calls]

flowllm/core/service/base_service.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,22 @@ def run(self):
5656
prints the logo (optional) and suppresses deprecation warnings. Concrete
5757
services should call super().run() before their own startup logic.
5858
"""
59+
flow_names = []
5960
for _, flow in C.flow_dict.items():
6061
assert isinstance(flow, BaseFlow)
6162
if flow.stream:
6263
if self.integrate_stream_flow(flow):
63-
logger.info(f"integrate {flow.name}[stream]")
64+
flow_names.append(flow.name)
6465

6566
elif isinstance(flow, BaseToolFlow):
6667
if self.integrate_tool_flow(flow):
67-
logger.info(f"integrate {flow.name}")
68+
flow_names.append(flow.name)
6869

6970
else:
7071
if self.integrate_flow(flow):
71-
logger.info(f"integrate {flow.name}")
72+
flow_names.append(flow.name)
73+
74+
logger.info(f"integrate {','.join(flow_names)}")
7275

7376
import warnings
7477

flowllm/extensions/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
- file_tool: File-related operations including editing and searching files
77
- data: Data-related operations including downloading stock data
88
- utils: Utility functions for date/time operations and other helpers
9+
- skills: Skill-based operations for managing and executing specialized skills
910
"""
1011

11-
from . import data, file_tool, utils
12+
from . import data, file_tool, skills, utils
1213

1314
__all__ = [
1415
"data",
1516
"file_tool",
17+
"skills",
1618
"utils",
1719
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Skills extension module for flowllm.
2+
3+
This module provides operations for managing and executing skills,
4+
including loading skill metadata, reading reference files, and running shell commands.
5+
"""
6+
7+
from .load_skill_metadata_op import LoadSkillMetadataOp
8+
from .load_skill_op import LoadSkillOp
9+
from .read_reference_file_op import ReadReferenceFileOp
10+
from .run_shell_command_op import RunShellCommandOp
11+
from .skill_agent_op import SkillAgentOp
12+
13+
__all__ = [
14+
"LoadSkillMetadataOp",
15+
"LoadSkillOp",
16+
"ReadReferenceFileOp",
17+
"RunShellCommandOp",
18+
"SkillAgentOp",
19+
]
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Operation for loading skill metadata.
2+
3+
This module provides the LoadSkillMetadataOp class which scans the skills
4+
directory and extracts metadata (name and description) from all SKILL.md files.
5+
"""
6+
7+
from pathlib import Path
8+
9+
from loguru import logger
10+
11+
from ...core.context import C
12+
from ...core.op import BaseAsyncToolOp
13+
from ...core.schema import ToolCall
14+
15+
16+
@C.register_op()
17+
class LoadSkillMetadataOp(BaseAsyncToolOp):
18+
"""Operation for loading metadata from all available skills.
19+
20+
This tool scans the skills directory for SKILL.md files and extracts
21+
their metadata (name and description) from YAML frontmatter.
22+
Returns a JSON string containing a list of skill metadata.
23+
"""
24+
25+
def build_tool_call(self) -> ToolCall:
26+
"""Build the tool call definition for load_skill_metadata.
27+
28+
Returns:
29+
A ToolCall object defining the load_skill_metadata tool.
30+
"""
31+
tool_params = {
32+
"name": "load_skill_metadata",
33+
"description": "Load metadata (name and description) for all available skills from the skills directory.",
34+
"input_schema": {},
35+
}
36+
return ToolCall(**tool_params)
37+
38+
@staticmethod
39+
async def parse_skill_metadata(content: str, path: str) -> dict[str, str] | None:
40+
"""Extract skill metadata (name and description) from SKILL.md content.
41+
42+
Parses YAML frontmatter from SKILL.md files to extract the skill name
43+
and description. The frontmatter should be in the format:
44+
---
45+
name: skill_name
46+
description: skill description
47+
---
48+
49+
Args:
50+
content: The content of the SKILL.md file.
51+
path: The file path (used for logging purposes).
52+
53+
Returns:
54+
A dictionary with 'name' and 'description' keys, or None if
55+
parsing fails or required fields are missing.
56+
"""
57+
parts = content.split("---")
58+
if len(parts) < 3:
59+
logger.warning(f"No YAML frontmatter found in skill from {path}")
60+
return None
61+
62+
frontmatter_text = parts[1].strip()
63+
name = None
64+
description = None
65+
66+
for line in frontmatter_text.split("\n"):
67+
line = line.strip()
68+
if line.startswith("name:"):
69+
name = line.split(":", 1)[1].strip().strip("\"'")
70+
elif line.startswith("description:"):
71+
description = line.split(":", 1)[1].strip().strip("\"'")
72+
73+
if not name or not description:
74+
logger.warning(f"Missing name or description in skill from {path}")
75+
return None
76+
77+
return {
78+
"name": name,
79+
"description": description,
80+
}
81+
82+
async def async_execute(self):
83+
"""Execute the load skill metadata operation.
84+
85+
Scans the skills directory recursively for all SKILL.md files,
86+
extracts their metadata, and returns a JSON string containing
87+
a list of all skill metadata entries.
88+
"""
89+
skill_dir = Path(self.context.skill_dir)
90+
logger.info(f"🔧 Tool called: load_skill_metadata(path={skill_dir})")
91+
skill_files = list(skill_dir.rglob("SKILL.md"))
92+
93+
skill_metadata_dict = {}
94+
for skill_file in skill_files:
95+
content = skill_file.read_text(encoding="utf-8")
96+
metadata = await self.parse_skill_metadata(content, str(skill_file))
97+
if metadata:
98+
skill_dir = skill_file.parent.as_posix()
99+
skill_metadata_dict[metadata["name"]] = {
100+
"description": metadata["description"],
101+
"skill_dir": skill_dir,
102+
}
103+
logger.info(f"✅ Loaded skill {metadata['name']} metadata skill_dir={skill_dir}")
104+
105+
logger.info(f"✅ Loaded {len(skill_metadata_dict)} skill metadata entries")
106+
self.set_output(skill_metadata_dict)

0 commit comments

Comments
 (0)