flowent 0.0.13 → 0.1.0
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.
- package/backend/pyproject.toml +2 -1
- package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
- package/backend/src/flowent/agent.py +28 -7
- package/backend/src/flowent/main.py +106 -9
- package/backend/src/flowent/mcp.py +484 -0
- package/backend/src/flowent/mcp_import.py +202 -0
- package/backend/src/flowent/skills.py +157 -0
- package/backend/src/flowent/static/assets/index-DqTHSMBo.js +81 -0
- package/backend/src/flowent/static/assets/index-d3FBbOXX.css +2 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/storage.py +240 -0
- package/backend/src/flowent/tools.py +6 -2
- package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/test_mcp.py +710 -0
- package/backend/tests/test_skills.py +462 -0
- package/backend/uv.lock +160 -1
- package/dist/frontend/assets/index-DqTHSMBo.js +81 -0
- package/dist/frontend/assets/index-d3FBbOXX.css +2 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/backend/src/flowent/static/assets/index-CEZrWoDG.css +0 -2
- package/backend/src/flowent/static/assets/index-S5a0Rkj1.js +0 -81
- package/dist/frontend/assets/index-CEZrWoDG.css +0 -2
- package/dist/frontend/assets/index-S5a0Rkj1.js +0 -81
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from flowent.llm import ChatMessage
|
|
8
|
+
from flowent.storage import StateStore, StoredSkill
|
|
9
|
+
|
|
10
|
+
PROJECT_SKILLS_DIRECTORY = Path(".flowent") / "skills"
|
|
11
|
+
AGENTS_SKILLS_DIRECTORY = Path(".agents") / "skills"
|
|
12
|
+
SKILL_FILENAME = "SKILL.md"
|
|
13
|
+
SKILL_REFERENCE_PATTERN = re.compile(r"(?<!\w)\$([a-z0-9][a-z0-9-]*)\b")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class SkillDocument:
|
|
18
|
+
body: str
|
|
19
|
+
skill: StoredSkill
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def skill_slug(value: str) -> str:
|
|
23
|
+
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
24
|
+
return slug or "skill"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def skill_id(scope: str, path: Path) -> str:
|
|
28
|
+
return f"{scope}:{path.resolve(strict=False)}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def skill_directories(cwd: Path, store: StateStore) -> list[tuple[str, Path]]:
|
|
32
|
+
return [
|
|
33
|
+
("project", cwd.resolve(strict=False) / PROJECT_SKILLS_DIRECTORY),
|
|
34
|
+
("project", cwd.resolve(strict=False) / AGENTS_SKILLS_DIRECTORY),
|
|
35
|
+
("user", store.directory / "skills"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_skill_frontmatter(content: str) -> tuple[dict[str, str], str]:
|
|
40
|
+
if not content.startswith("---\n"):
|
|
41
|
+
return {}, content
|
|
42
|
+
|
|
43
|
+
end_index = content.find("\n---", 4)
|
|
44
|
+
if end_index == -1:
|
|
45
|
+
return {}, content
|
|
46
|
+
|
|
47
|
+
metadata: dict[str, str] = {}
|
|
48
|
+
for line in content[4:end_index].splitlines():
|
|
49
|
+
key, separator, value = line.partition(":")
|
|
50
|
+
if not separator:
|
|
51
|
+
continue
|
|
52
|
+
metadata[key.strip().lower()] = value.strip().strip("\"'")
|
|
53
|
+
|
|
54
|
+
body_start = end_index + len("\n---")
|
|
55
|
+
if content[body_start : body_start + 1] == "\n":
|
|
56
|
+
body_start += 1
|
|
57
|
+
return metadata, content[body_start:]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_skill_document(scope: str, path: Path, enabled: bool) -> SkillDocument:
|
|
61
|
+
content = path.read_text(errors="replace")
|
|
62
|
+
metadata, body = parse_skill_frontmatter(content)
|
|
63
|
+
name = metadata.get("name", "").strip()
|
|
64
|
+
description = metadata.get("description", "").strip()
|
|
65
|
+
fallback_name = path.parent.name.replace("-", " ").strip().title() or "Skill"
|
|
66
|
+
display_name = name or fallback_name
|
|
67
|
+
error = "" if name and description else "Skill needs a name and description."
|
|
68
|
+
slug = skill_slug(display_name)
|
|
69
|
+
return SkillDocument(
|
|
70
|
+
body=body.strip(),
|
|
71
|
+
skill=StoredSkill(
|
|
72
|
+
description=description,
|
|
73
|
+
enabled=enabled,
|
|
74
|
+
error=error,
|
|
75
|
+
id=skill_id(scope, path),
|
|
76
|
+
name=display_name,
|
|
77
|
+
path=str(path.resolve(strict=False)),
|
|
78
|
+
scope=scope,
|
|
79
|
+
slug=slug,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def discover_skill_documents(cwd: Path, store: StateStore) -> list[SkillDocument]:
|
|
85
|
+
enabled_by_id = store.read_skill_enabled()
|
|
86
|
+
documents: list[SkillDocument] = []
|
|
87
|
+
for scope, directory in skill_directories(cwd, store):
|
|
88
|
+
if not directory.is_dir():
|
|
89
|
+
continue
|
|
90
|
+
for path in sorted(directory.glob(f"*/{SKILL_FILENAME}")):
|
|
91
|
+
resolved_id = skill_id(scope, path)
|
|
92
|
+
documents.append(
|
|
93
|
+
load_skill_document(
|
|
94
|
+
scope,
|
|
95
|
+
path,
|
|
96
|
+
enabled_by_id.get(resolved_id, True),
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
return documents
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def discover_skills(cwd: Path, store: StateStore) -> list[StoredSkill]:
|
|
103
|
+
return [document.skill for document in discover_skill_documents(cwd, store)]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def referenced_skill_slugs(content: str) -> list[str]:
|
|
107
|
+
slugs: list[str] = []
|
|
108
|
+
for match in SKILL_REFERENCE_PATTERN.finditer(content):
|
|
109
|
+
slug = match.group(1)
|
|
110
|
+
if slug not in slugs:
|
|
111
|
+
slugs.append(slug)
|
|
112
|
+
return slugs
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def explicit_skill_messages(
|
|
116
|
+
cwd: Path,
|
|
117
|
+
store: StateStore,
|
|
118
|
+
content: str,
|
|
119
|
+
) -> list[ChatMessage]:
|
|
120
|
+
requested_slugs = referenced_skill_slugs(content)
|
|
121
|
+
if not requested_slugs:
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
documents_by_slug = {
|
|
125
|
+
document.skill.slug: document
|
|
126
|
+
for document in discover_skill_documents(cwd, store)
|
|
127
|
+
if document.skill.enabled and not document.skill.error
|
|
128
|
+
}
|
|
129
|
+
messages: list[ChatMessage] = []
|
|
130
|
+
for slug in requested_slugs:
|
|
131
|
+
document = documents_by_slug.get(slug)
|
|
132
|
+
if document is None:
|
|
133
|
+
continue
|
|
134
|
+
messages.append(
|
|
135
|
+
ChatMessage(
|
|
136
|
+
role="user",
|
|
137
|
+
content=(
|
|
138
|
+
f'<skill name="{document.skill.name}" slug="{document.skill.slug}">\n'
|
|
139
|
+
f"{document.body}\n"
|
|
140
|
+
"</skill>"
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
return messages
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def update_skill_enabled(
|
|
148
|
+
cwd: Path,
|
|
149
|
+
store: StateStore,
|
|
150
|
+
skill_id_value: str,
|
|
151
|
+
enabled: bool,
|
|
152
|
+
) -> StoredSkill:
|
|
153
|
+
for skill in discover_skills(cwd, store):
|
|
154
|
+
if skill.id == skill_id_value:
|
|
155
|
+
store.save_skill_enabled(skill_id_value, enabled)
|
|
156
|
+
return skill.model_copy(update={"enabled": enabled})
|
|
157
|
+
raise KeyError(skill_id_value)
|