soothe-plugins 0.2.6__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.
@@ -0,0 +1,322 @@
1
+ """AgentComposer -- skill harmonization and tool resolution (RFC-0005) -- community edition.
2
+
3
+ The composer is the core of Weaver's value proposition. It takes raw skills
4
+ from Skillify and resolves conflicts, overlaps, and gaps before handing
5
+ a coherent blueprint to the generator.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import re
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING
15
+
16
+ from .models import (
17
+ AgentBlueprint,
18
+ CapabilitySignature,
19
+ HarmonizedSkillSet,
20
+ SkillConflictReport,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from langchain_core.language_models import BaseChatModel
25
+
26
+ from soothe_plugins.skillify.models import SkillBundle
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Constants for skill content formatting
31
+ _MAX_AGENT_NAME_WORDS = 4
32
+ _MAX_SKILL_CONTENT_LENGTH = 1500
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Prompts
36
+ # ---------------------------------------------------------------------------
37
+
38
+ _CONFLICT_DETECTION_PROMPT = """\
39
+ You are analysing a set of agent skills for conflicts, overlaps, and gaps.
40
+
41
+ User objective: {objective}
42
+
43
+ Skills (each identified by ID):
44
+ {skills_text}
45
+
46
+ Analyse ALL skills and output ONLY valid JSON:
47
+ {{
48
+ "conflicts": [
49
+ {{
50
+ "skill_a_id": "id",
51
+ "skill_b_id": "id",
52
+ "conflict_type": "contradictory|ambiguous|version_mismatch",
53
+ "description": "what the conflict is",
54
+ "severity": "low|medium|high",
55
+ "resolution": "how to resolve it"
56
+ }}
57
+ ],
58
+ "overlaps": [["id1", "id2"]],
59
+ "gaps": ["missing capability description"],
60
+ "harmonization_summary": "brief summary"
61
+ }}
62
+
63
+ If there are no conflicts, return empty lists. Be thorough but concise."""
64
+
65
+ _MERGE_PROMPT = """\
66
+ Given the conflict analysis below and the original skill contents, produce a \
67
+ deduplicated and merged skill set.
68
+
69
+ Objective: {objective}
70
+
71
+ Conflict report:
72
+ {report_json}
73
+
74
+ Original skill contents:
75
+ {skills_text}
76
+
77
+ For each skill, decide: KEEP (as-is), MERGE (combine with another), or DROP \
78
+ (redundant/conflicting). For merged skills, provide the merged content.
79
+
80
+ Output ONLY valid JSON:
81
+ {{
82
+ "kept_skills": {{"skill_id": "content"}},
83
+ "dropped_skills": ["skill_id"],
84
+ "merge_log": ["decision description"]
85
+ }}"""
86
+
87
+ _GAP_ANALYSIS_PROMPT = """\
88
+ Given the user objective and the resolved skill set below, identify any \
89
+ missing connective logic -- instructions needed to make these skills work \
90
+ together coherently for this specific task.
91
+
92
+ Objective: {objective}
93
+
94
+ Capabilities needed: {capabilities}
95
+
96
+ Resolved skills:
97
+ {skills_text}
98
+
99
+ Generate bridge instructions (plain text) that fill the gaps. If no gaps \
100
+ exist, return "No additional instructions needed."."""
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Composer
105
+ # ---------------------------------------------------------------------------
106
+
107
+
108
+ class AgentComposer:
109
+ """Composes an agent blueprint from skills and tools with harmonization.
110
+
111
+ The three-step harmonization pipeline resolves conflicts, overlaps, and
112
+ gaps that arise when combining skills from diverse creators.
113
+
114
+ Args:
115
+ model: Chat model for LLM-assisted harmonization.
116
+ allowed_tool_groups: Tool groups the generated agent may use.
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ model: BaseChatModel,
122
+ allowed_tool_groups: list[str] | None = None,
123
+ ) -> None:
124
+ """Initialize the agent composer.
125
+
126
+ Args:
127
+ model: Chat model for LLM-assisted harmonization.
128
+ allowed_tool_groups: Tool groups the generated agent may use.
129
+ """
130
+ self._model = model
131
+ self._allowed_tools = allowed_tool_groups or []
132
+
133
+ async def compose(
134
+ self,
135
+ capability: CapabilitySignature,
136
+ skill_bundle: "SkillBundle",
137
+ ) -> AgentBlueprint:
138
+ """Compose an agent blueprint from skills and tools.
139
+
140
+ Args:
141
+ capability: Analysed capability signature.
142
+ skill_bundle: Skills retrieved from Skillify.
143
+
144
+ Returns:
145
+ Complete agent blueprint ready for generation.
146
+ """
147
+ skill_contents = self._load_skill_contents(skill_bundle)
148
+ harmonized = await self.harmonize_skills(skill_contents, capability)
149
+ tools = self._resolve_tools(capability)
150
+ agent_name = self._generate_name(capability.description)
151
+
152
+ return AgentBlueprint(
153
+ capability=capability,
154
+ harmonized=harmonized,
155
+ tools=tools,
156
+ agent_name=agent_name,
157
+ )
158
+
159
+ async def harmonize_skills(
160
+ self,
161
+ skill_contents: dict[str, str],
162
+ capability: CapabilitySignature,
163
+ ) -> HarmonizedSkillSet:
164
+ """Three-step skill harmonization pipeline.
165
+
166
+ Args:
167
+ skill_contents: Mapping of skill_id to SKILL.md content.
168
+ capability: The target capability signature.
169
+
170
+ Returns:
171
+ Harmonized skill set with conflicts resolved and gaps filled.
172
+ """
173
+ if not skill_contents:
174
+ return HarmonizedSkillSet(bridge_instructions="No skills available.")
175
+
176
+ if len(skill_contents) == 1:
177
+ sid, content = next(iter(skill_contents.items()))
178
+ return HarmonizedSkillSet(
179
+ skills=[sid],
180
+ skill_contents={sid: content},
181
+ )
182
+
183
+ # Step 1: Conflict detection
184
+ report = await self._detect_conflicts(skill_contents, capability)
185
+
186
+ # Step 2: Deduplication and merging
187
+ merged_contents, dropped, merge_log = await self._merge_skills(skill_contents, report, capability)
188
+
189
+ # Step 3: Gap analysis
190
+ bridge = await self._analyze_gaps(merged_contents, capability)
191
+
192
+ return HarmonizedSkillSet(
193
+ skills=list(merged_contents.keys()),
194
+ skill_contents=merged_contents,
195
+ bridge_instructions=bridge,
196
+ dropped_skills=dropped,
197
+ merge_log=merge_log,
198
+ )
199
+
200
+ # -- Step 1: Conflict detection -----------------------------------------
201
+
202
+ async def _detect_conflicts(
203
+ self,
204
+ skill_contents: dict[str, str],
205
+ capability: CapabilitySignature,
206
+ ) -> SkillConflictReport:
207
+ """Detect conflicts, overlaps, and gaps across candidate skills."""
208
+ skills_text = self._format_skills_for_prompt(skill_contents)
209
+ prompt = _CONFLICT_DETECTION_PROMPT.format(
210
+ objective=capability.description,
211
+ skills_text=skills_text,
212
+ )
213
+
214
+ try:
215
+ resp = await self._model.ainvoke([{"role": "user", "content": prompt}])
216
+ parsed = json.loads(str(resp.content))
217
+ return SkillConflictReport(**parsed)
218
+ except (json.JSONDecodeError, Exception):
219
+ logger.warning("Conflict detection LLM call failed, assuming no conflicts", exc_info=True)
220
+ return SkillConflictReport(harmonization_summary="Analysis skipped due to error.")
221
+
222
+ # -- Step 2: Deduplication and merging ----------------------------------
223
+
224
+ async def _merge_skills(
225
+ self,
226
+ skill_contents: dict[str, str],
227
+ report: SkillConflictReport,
228
+ capability: CapabilitySignature,
229
+ ) -> tuple[dict[str, str], list[str], list[str]]:
230
+ """Deduplicate and merge skills based on the conflict report.
231
+
232
+ Returns:
233
+ Tuple of (merged_contents, dropped_ids, merge_log).
234
+ """
235
+ if not report.conflicts and not report.overlaps:
236
+ return skill_contents, [], ["No conflicts or overlaps detected."]
237
+
238
+ skills_text = self._format_skills_for_prompt(skill_contents)
239
+ prompt = _MERGE_PROMPT.format(
240
+ objective=capability.description,
241
+ report_json=report.model_dump_json(indent=2),
242
+ skills_text=skills_text,
243
+ )
244
+
245
+ try:
246
+ resp = await self._model.ainvoke([{"role": "user", "content": prompt}])
247
+ parsed = json.loads(str(resp.content))
248
+ kept = parsed.get("kept_skills", {})
249
+ dropped = parsed.get("dropped_skills", [])
250
+ log = parsed.get("merge_log", [])
251
+
252
+ if not kept:
253
+ return skill_contents, [], ["Merge returned empty; keeping all skills."]
254
+
255
+ except (json.JSONDecodeError, Exception):
256
+ logger.warning("Merge LLM call failed, keeping all skills", exc_info=True)
257
+ return skill_contents, [], ["Merge skipped due to error."]
258
+ else:
259
+ return kept, dropped, log
260
+
261
+ # -- Step 3: Gap analysis -----------------------------------------------
262
+
263
+ async def _analyze_gaps(
264
+ self,
265
+ resolved_contents: dict[str, str],
266
+ capability: CapabilitySignature,
267
+ ) -> str:
268
+ """Identify missing glue logic and generate bridge instructions."""
269
+ skills_text = self._format_skills_for_prompt(resolved_contents)
270
+ caps = ", ".join(capability.required_capabilities) or capability.description[:200]
271
+ prompt = _GAP_ANALYSIS_PROMPT.format(
272
+ objective=capability.description,
273
+ capabilities=caps,
274
+ skills_text=skills_text,
275
+ )
276
+
277
+ try:
278
+ resp = await self._model.ainvoke([{"role": "user", "content": prompt}])
279
+ return str(resp.content).strip()
280
+ except Exception:
281
+ logger.warning("Gap analysis LLM call failed", exc_info=True)
282
+ return ""
283
+
284
+ # -- Helpers ------------------------------------------------------------
285
+
286
+ def _resolve_tools(self, capability: CapabilitySignature) -> list[str]:
287
+ """Match capabilities to allowed tool groups."""
288
+ cap_lower = {c.lower() for c in capability.required_capabilities}
289
+ return [
290
+ tool_group
291
+ for tool_group in self._allowed_tools
292
+ if tool_group.lower() in cap_lower or any(tool_group.lower() in c for c in cap_lower)
293
+ ]
294
+
295
+ @staticmethod
296
+ def _generate_name(description: str) -> str:
297
+ """Generate a hyphenated agent name from a description."""
298
+ words = re.sub(r"[^a-z0-9\s]", "", description.lower()).split()
299
+ name_words = words[:_MAX_AGENT_NAME_WORDS] if len(words) > _MAX_AGENT_NAME_WORDS else words
300
+ return "-".join(name_words) if name_words else "generated-agent"
301
+
302
+ @staticmethod
303
+ def _format_skills_for_prompt(skill_contents: dict[str, str]) -> str:
304
+ """Format skill contents for inclusion in LLM prompts."""
305
+ parts: list[str] = []
306
+ for sid, content in skill_contents.items():
307
+ truncated = content[:_MAX_SKILL_CONTENT_LENGTH] if len(content) > _MAX_SKILL_CONTENT_LENGTH else content
308
+ parts.append(f"--- Skill ID: {sid} ---\n{truncated}\n")
309
+ return "\n".join(parts)
310
+
311
+ @staticmethod
312
+ def _load_skill_contents(skill_bundle: "SkillBundle") -> dict[str, str]:
313
+ """Load full SKILL.md content for each skill in the bundle."""
314
+ contents: dict[str, str] = {}
315
+ for sr in skill_bundle.results:
316
+ skill_md = Path(sr.record.path) / "SKILL.md"
317
+ if skill_md.is_file():
318
+ try:
319
+ contents[sr.record.id] = skill_md.read_text(encoding="utf-8")
320
+ except Exception:
321
+ logger.warning("Failed to read %s", skill_md, exc_info=True)
322
+ return contents
@@ -0,0 +1,223 @@
1
+ """Weaver subagent events.
2
+
3
+ This module defines events for the weaver subagent.
4
+ Events are self-registered at module load time.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Literal
10
+
11
+ from pydantic import ConfigDict, Field
12
+
13
+ from soothe_sdk.core.events import SubagentEvent
14
+
15
+
16
+ class WeaverDispatchedEvent(SubagentEvent):
17
+ """Weaver subagent dispatched event."""
18
+
19
+ type: Literal["soothe.subagent.weaver.dispatched"] = "soothe.subagent.weaver.dispatched"
20
+ task: str = ""
21
+
22
+ model_config = ConfigDict(extra="allow")
23
+
24
+
25
+ class WeaverCompletedEvent(SubagentEvent):
26
+ """Weaver subagent completed event."""
27
+
28
+ type: Literal["soothe.subagent.weaver.completed"] = "soothe.subagent.weaver.completed"
29
+ duration_ms: int = 0
30
+ agent_name: str = ""
31
+
32
+ model_config = ConfigDict(extra="allow")
33
+
34
+
35
+ class WeaverAnalysisStartedEvent(SubagentEvent):
36
+ """Weaver analysis started event."""
37
+
38
+ type: Literal["soothe.subagent.weaver.analysis_started"] = "soothe.subagent.weaver.analysis_started"
39
+ task_preview: str = ""
40
+
41
+ model_config = ConfigDict(extra="allow")
42
+
43
+
44
+ class WeaverAnalysisCompletedEvent(SubagentEvent):
45
+ """Weaver analysis completed event."""
46
+
47
+ type: Literal["soothe.subagent.weaver.analysis_completed"] = "soothe.subagent.weaver.analysis_completed"
48
+ capabilities: list[Any] = Field(default_factory=list)
49
+ constraints: list[Any] = Field(default_factory=list)
50
+
51
+ model_config = ConfigDict(extra="allow")
52
+
53
+
54
+ class WeaverReuseHitEvent(SubagentEvent):
55
+ """Weaver reuse hit event."""
56
+
57
+ type: Literal["soothe.subagent.weaver.reuse_hit"] = "soothe.subagent.weaver.reuse_hit"
58
+ agent_name: str = ""
59
+ confidence: float = 0.0
60
+
61
+ model_config = ConfigDict(extra="allow")
62
+
63
+
64
+ class WeaverReuseMissEvent(SubagentEvent):
65
+ """Weaver reuse miss event."""
66
+
67
+ type: Literal["soothe.subagent.weaver.reuse_miss"] = "soothe.subagent.weaver.reuse_miss"
68
+ best_confidence: float = 0.0
69
+
70
+ model_config = ConfigDict(extra="allow")
71
+
72
+
73
+ class WeaverSkillifyPendingEvent(SubagentEvent):
74
+ """Weaver skillify pending event."""
75
+
76
+ type: Literal["soothe.subagent.weaver.skillify_pending"] = "soothe.subagent.weaver.skillify_pending"
77
+
78
+ model_config = ConfigDict(extra="allow")
79
+
80
+
81
+ class WeaverHarmonizeStartedEvent(SubagentEvent):
82
+ """Weaver harmonize started event."""
83
+
84
+ type: Literal["soothe.subagent.weaver.harmonize_started"] = "soothe.subagent.weaver.harmonize_started"
85
+ skill_count: int = 0
86
+
87
+ model_config = ConfigDict(extra="allow")
88
+
89
+
90
+ class WeaverHarmonizeCompletedEvent(SubagentEvent):
91
+ """Weaver harmonize completed event."""
92
+
93
+ type: Literal["soothe.subagent.weaver.harmonize_completed"] = "soothe.subagent.weaver.harmonize_completed"
94
+ retained: int = 0
95
+ dropped: int = 0
96
+ bridge_length: int = 0
97
+
98
+ model_config = ConfigDict(extra="allow")
99
+
100
+
101
+ class WeaverGenerateStartedEvent(SubagentEvent):
102
+ """Weaver generate started event."""
103
+
104
+ type: Literal["soothe.subagent.weaver.generate_started"] = "soothe.subagent.weaver.generate_started"
105
+ agent_name: str = ""
106
+
107
+ model_config = ConfigDict(extra="allow")
108
+
109
+
110
+ class WeaverGenerateCompletedEvent(SubagentEvent):
111
+ """Weaver generate completed event."""
112
+
113
+ type: Literal["soothe.subagent.weaver.generate_completed"] = "soothe.subagent.weaver.generate_completed"
114
+ agent_name: str = ""
115
+ path: str = ""
116
+
117
+ model_config = ConfigDict(extra="allow")
118
+
119
+
120
+ class WeaverValidateStartedEvent(SubagentEvent):
121
+ """Weaver validate started event."""
122
+
123
+ type: Literal["soothe.subagent.weaver.validate_started"] = "soothe.subagent.weaver.validate_started"
124
+ agent_name: str = ""
125
+
126
+ model_config = ConfigDict(extra="allow")
127
+
128
+
129
+ class WeaverValidateCompletedEvent(SubagentEvent):
130
+ """Weaver validate completed event."""
131
+
132
+ type: Literal["soothe.subagent.weaver.validate_completed"] = "soothe.subagent.weaver.validate_completed"
133
+ agent_name: str = ""
134
+
135
+ model_config = ConfigDict(extra="allow")
136
+
137
+
138
+ class WeaverRegistryUpdatedEvent(SubagentEvent):
139
+ """Weaver registry updated event."""
140
+
141
+ type: Literal["soothe.subagent.weaver.registry_updated"] = "soothe.subagent.weaver.registry_updated"
142
+ agent_name: str = ""
143
+ version: str = ""
144
+
145
+ model_config = ConfigDict(extra="allow")
146
+
147
+
148
+ class WeaverExecuteStartedEvent(SubagentEvent):
149
+ """Weaver execute started event."""
150
+
151
+ type: Literal["soothe.subagent.weaver.execute_started"] = "soothe.subagent.weaver.execute_started"
152
+ agent_name: str = ""
153
+ task_preview: str = ""
154
+
155
+ model_config = ConfigDict(extra="allow")
156
+
157
+
158
+ class WeaverExecuteCompletedEvent(SubagentEvent):
159
+ """Weaver execute completed event."""
160
+
161
+ type: Literal["soothe.subagent.weaver.execute_completed"] = "soothe.subagent.weaver.execute_completed"
162
+ agent_name: str = ""
163
+ result_length: int = 0
164
+
165
+ model_config = ConfigDict(extra="allow")
166
+
167
+
168
+ # Events are self-contained for community plugins.
169
+ # Daemon will handle event registration based on type strings.
170
+ # No explicit registration needed here.
171
+
172
+ # Event type constants
173
+ SUBAGENT_WEAVER_DISPATCHED = "soothe.subagent.weaver.dispatched"
174
+ SUBAGENT_WEAVER_COMPLETED = "soothe.subagent.weaver.completed"
175
+ SUBAGENT_WEAVER_ANALYSIS_STARTED = "soothe.subagent.weaver.analysis_started"
176
+ SUBAGENT_WEAVER_ANALYSIS_COMPLETED = "soothe.subagent.weaver.analysis_completed"
177
+ SUBAGENT_WEAVER_REUSE_HIT = "soothe.subagent.weaver.reuse_hit"
178
+ SUBAGENT_WEAVER_REUSE_MISS = "soothe.subagent.weaver.reuse_miss"
179
+ SUBAGENT_WEAVER_SKILLIFY_PENDING = "soothe.subagent.weaver.skillify_pending"
180
+ SUBAGENT_WEAVER_HARMONIZE_STARTED = "soothe.subagent.weaver.harmonize_started"
181
+ SUBAGENT_WEAVER_HARMONIZE_COMPLETED = "soothe.subagent.weaver.harmonize_completed"
182
+ SUBAGENT_WEAVER_GENERATE_STARTED = "soothe.subagent.weaver.generate_started"
183
+ SUBAGENT_WEAVER_GENERATE_COMPLETED = "soothe.subagent.weaver.generate_completed"
184
+ SUBAGENT_WEAVER_VALIDATE_STARTED = "soothe.subagent.weaver.validate_started"
185
+ SUBAGENT_WEAVER_VALIDATE_COMPLETED = "soothe.subagent.weaver.validate_completed"
186
+ SUBAGENT_WEAVER_REGISTRY_UPDATED = "soothe.subagent.weaver.registry_updated"
187
+ SUBAGENT_WEAVER_EXECUTE_STARTED = "soothe.subagent.weaver.execute_started"
188
+ SUBAGENT_WEAVER_EXECUTE_COMPLETED = "soothe.subagent.weaver.execute_completed"
189
+
190
+ __all__ = [
191
+ "SUBAGENT_WEAVER_ANALYSIS_COMPLETED",
192
+ "SUBAGENT_WEAVER_ANALYSIS_STARTED",
193
+ "SUBAGENT_WEAVER_COMPLETED",
194
+ "SUBAGENT_WEAVER_DISPATCHED",
195
+ "SUBAGENT_WEAVER_EXECUTE_COMPLETED",
196
+ "SUBAGENT_WEAVER_EXECUTE_STARTED",
197
+ "SUBAGENT_WEAVER_GENERATE_COMPLETED",
198
+ "SUBAGENT_WEAVER_GENERATE_STARTED",
199
+ "SUBAGENT_WEAVER_HARMONIZE_COMPLETED",
200
+ "SUBAGENT_WEAVER_HARMONIZE_STARTED",
201
+ "SUBAGENT_WEAVER_REGISTRY_UPDATED",
202
+ "SUBAGENT_WEAVER_REUSE_HIT",
203
+ "SUBAGENT_WEAVER_REUSE_MISS",
204
+ "SUBAGENT_WEAVER_SKILLIFY_PENDING",
205
+ "SUBAGENT_WEAVER_VALIDATE_COMPLETED",
206
+ "SUBAGENT_WEAVER_VALIDATE_STARTED",
207
+ "WeaverAnalysisCompletedEvent",
208
+ "WeaverAnalysisStartedEvent",
209
+ "WeaverCompletedEvent",
210
+ "WeaverDispatchedEvent",
211
+ "WeaverExecuteCompletedEvent",
212
+ "WeaverExecuteStartedEvent",
213
+ "WeaverGenerateCompletedEvent",
214
+ "WeaverGenerateStartedEvent",
215
+ "WeaverHarmonizeCompletedEvent",
216
+ "WeaverHarmonizeStartedEvent",
217
+ "WeaverRegistryUpdatedEvent",
218
+ "WeaverReuseHitEvent",
219
+ "WeaverReuseMissEvent",
220
+ "WeaverSkillifyPendingEvent",
221
+ "WeaverValidateCompletedEvent",
222
+ "WeaverValidateStartedEvent",
223
+ ]