devrel-origin 0.2.14__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.
- devrel_origin/__init__.py +15 -0
- devrel_origin/cli/__init__.py +92 -0
- devrel_origin/cli/_common.py +243 -0
- devrel_origin/cli/analytics.py +28 -0
- devrel_origin/cli/argus.py +497 -0
- devrel_origin/cli/auth.py +227 -0
- devrel_origin/cli/config.py +108 -0
- devrel_origin/cli/content.py +259 -0
- devrel_origin/cli/cost.py +108 -0
- devrel_origin/cli/cro.py +298 -0
- devrel_origin/cli/deliverables.py +65 -0
- devrel_origin/cli/docs.py +91 -0
- devrel_origin/cli/doctor.py +178 -0
- devrel_origin/cli/experiment.py +29 -0
- devrel_origin/cli/growth.py +97 -0
- devrel_origin/cli/init.py +472 -0
- devrel_origin/cli/intel.py +27 -0
- devrel_origin/cli/kb.py +96 -0
- devrel_origin/cli/listen.py +31 -0
- devrel_origin/cli/marketing.py +66 -0
- devrel_origin/cli/migrate.py +45 -0
- devrel_origin/cli/run.py +46 -0
- devrel_origin/cli/sales.py +57 -0
- devrel_origin/cli/schedule.py +62 -0
- devrel_origin/cli/synthesize.py +28 -0
- devrel_origin/cli/triage.py +29 -0
- devrel_origin/cli/video.py +35 -0
- devrel_origin/core/__init__.py +58 -0
- devrel_origin/core/agent_config.py +75 -0
- devrel_origin/core/argus.py +964 -0
- devrel_origin/core/atlas.py +1450 -0
- devrel_origin/core/base.py +372 -0
- devrel_origin/core/cyra.py +563 -0
- devrel_origin/core/dex.py +708 -0
- devrel_origin/core/echo.py +614 -0
- devrel_origin/core/growth/__init__.py +27 -0
- devrel_origin/core/growth/recommendations.py +219 -0
- devrel_origin/core/growth/target_kinds.py +51 -0
- devrel_origin/core/iris.py +513 -0
- devrel_origin/core/kai.py +1367 -0
- devrel_origin/core/llm.py +542 -0
- devrel_origin/core/llm_backends.py +274 -0
- devrel_origin/core/mox.py +514 -0
- devrel_origin/core/nova.py +349 -0
- devrel_origin/core/pax.py +1205 -0
- devrel_origin/core/rex.py +532 -0
- devrel_origin/core/sage.py +486 -0
- devrel_origin/core/sentinel.py +385 -0
- devrel_origin/core/types.py +98 -0
- devrel_origin/core/video/__init__.py +22 -0
- devrel_origin/core/video/assembler.py +131 -0
- devrel_origin/core/video/browser_recorder.py +118 -0
- devrel_origin/core/video/desktop_recorder.py +254 -0
- devrel_origin/core/video/overlay_renderer.py +143 -0
- devrel_origin/core/video/script_parser.py +147 -0
- devrel_origin/core/video/tts_engine.py +82 -0
- devrel_origin/core/vox.py +268 -0
- devrel_origin/core/watchdog.py +321 -0
- devrel_origin/project/__init__.py +1 -0
- devrel_origin/project/config.py +75 -0
- devrel_origin/project/cost_sink.py +61 -0
- devrel_origin/project/init.py +104 -0
- devrel_origin/project/paths.py +75 -0
- devrel_origin/project/state.py +241 -0
- devrel_origin/project/templates/__init__.py +4 -0
- devrel_origin/project/templates/config.toml +24 -0
- devrel_origin/project/templates/devrel.gitignore +10 -0
- devrel_origin/project/templates/slop-blocklist.md +45 -0
- devrel_origin/project/templates/style.md +24 -0
- devrel_origin/project/templates/voice.md +29 -0
- devrel_origin/quality/__init__.py +66 -0
- devrel_origin/quality/editorial.py +357 -0
- devrel_origin/quality/persona.py +84 -0
- devrel_origin/quality/readability.py +148 -0
- devrel_origin/quality/slop.py +167 -0
- devrel_origin/quality/style.py +110 -0
- devrel_origin/quality/voice.py +15 -0
- devrel_origin/tools/__init__.py +9 -0
- devrel_origin/tools/analytics.py +304 -0
- devrel_origin/tools/api_client.py +393 -0
- devrel_origin/tools/apollo_client.py +305 -0
- devrel_origin/tools/code_validator.py +428 -0
- devrel_origin/tools/github_tools.py +297 -0
- devrel_origin/tools/instantly_client.py +412 -0
- devrel_origin/tools/kb_harvester.py +340 -0
- devrel_origin/tools/mcp_server.py +578 -0
- devrel_origin/tools/notifications.py +245 -0
- devrel_origin/tools/run_report.py +193 -0
- devrel_origin/tools/scheduler.py +231 -0
- devrel_origin/tools/search_tools.py +321 -0
- devrel_origin/tools/self_improve.py +168 -0
- devrel_origin/tools/sheets.py +236 -0
- devrel_origin-0.2.14.dist-info/METADATA +354 -0
- devrel_origin-0.2.14.dist-info/RECORD +98 -0
- devrel_origin-0.2.14.dist-info/WHEEL +5 -0
- devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
- devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
- devrel_origin-0.2.14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sage — Community Manager Agent
|
|
3
|
+
|
|
4
|
+
Triages GitHub issues, analyzes sentiment, flags at-risk contributors,
|
|
5
|
+
and identifies community champions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from devrel_origin.tools.api_client import PostHogClient
|
|
15
|
+
from devrel_origin.tools.github_tools import GitHubIssue, GitHubTools
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Shared keyword vocabularies for triage classification. Single source of
|
|
21
|
+
# truth — avoids divergence between sentiment, category, and priority logic.
|
|
22
|
+
CHURN_SIGNALS: tuple[str, ...] = (
|
|
23
|
+
"switching to",
|
|
24
|
+
"give up",
|
|
25
|
+
"moving away",
|
|
26
|
+
"nth time",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
FRUSTRATION_SIGNALS: tuple[str, ...] = (
|
|
30
|
+
"broken",
|
|
31
|
+
"terrible",
|
|
32
|
+
"worst",
|
|
33
|
+
"!!!",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
BUG_KEYWORDS: tuple[str, ...] = (
|
|
37
|
+
"bug",
|
|
38
|
+
"error",
|
|
39
|
+
"crash",
|
|
40
|
+
"broken",
|
|
41
|
+
"fix",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
CRITICAL_KEYWORDS: tuple[str, ...] = (
|
|
45
|
+
"data loss",
|
|
46
|
+
"security",
|
|
47
|
+
"vulnerability",
|
|
48
|
+
"crash",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class IssuePriority(Enum):
|
|
53
|
+
CRITICAL = "critical" # Data loss, security, complete breakage
|
|
54
|
+
HIGH = "high" # Feature broken, no workaround
|
|
55
|
+
MEDIUM = "medium" # Feature degraded, workaround exists
|
|
56
|
+
LOW = "low" # Enhancement, cosmetic, nice-to-have
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SentimentScore(Enum):
|
|
60
|
+
POSITIVE = "positive"
|
|
61
|
+
NEUTRAL = "neutral"
|
|
62
|
+
FRUSTRATED = "frustrated"
|
|
63
|
+
CHURNING = "churning" # Signals intent to leave
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class TriagedIssue:
|
|
68
|
+
"""A GitHub issue that has been triaged and categorized."""
|
|
69
|
+
|
|
70
|
+
issue_number: int
|
|
71
|
+
title: str
|
|
72
|
+
author: str
|
|
73
|
+
priority: IssuePriority
|
|
74
|
+
sentiment: SentimentScore
|
|
75
|
+
category: str # bug, feature_request, question, docs, performance
|
|
76
|
+
product_area: str # analytics, replay, flags, experiments, surveys, etc.
|
|
77
|
+
summary: str
|
|
78
|
+
suggested_response: str
|
|
79
|
+
churn_risk: bool = False
|
|
80
|
+
champion_signal: bool = False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class TriageReport:
|
|
85
|
+
"""Weekly triage summary."""
|
|
86
|
+
|
|
87
|
+
week_of: str
|
|
88
|
+
total_issues: int
|
|
89
|
+
issues: list[TriagedIssue] = field(default_factory=list)
|
|
90
|
+
churn_risks: list[str] = field(default_factory=list)
|
|
91
|
+
champions: list[str] = field(default_factory=list)
|
|
92
|
+
sentiment_breakdown: dict[str, int] = field(default_factory=dict)
|
|
93
|
+
category_breakdown: dict[str, int] = field(default_factory=dict)
|
|
94
|
+
product_area_breakdown: dict[str, int] = field(default_factory=dict)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Sage:
|
|
98
|
+
"""
|
|
99
|
+
Community Manager agent for GitHub issue triage and community health.
|
|
100
|
+
|
|
101
|
+
Capabilities:
|
|
102
|
+
- Triage incoming GitHub issues by priority, category, and product area
|
|
103
|
+
- Analyze author sentiment and flag churn risks
|
|
104
|
+
- Identify community champions (frequent contributors, helpful commenters)
|
|
105
|
+
- Generate suggested first responses for each issue
|
|
106
|
+
- Produce weekly triage reports with actionable recommendations
|
|
107
|
+
|
|
108
|
+
Tools:
|
|
109
|
+
1. github_issues_fetch — Pull recent issues from devrel-ai-agents repo
|
|
110
|
+
2. github_comments_fetch — Pull comments on specific issues
|
|
111
|
+
3. github_user_history — Analyze a user's contribution history
|
|
112
|
+
4. sentiment_analyzer — Classify text sentiment
|
|
113
|
+
5. issue_categorizer — Map issues to product areas and types
|
|
114
|
+
6. churn_detector — Flag users showing departure signals
|
|
115
|
+
7. champion_identifier — Score users on community contribution
|
|
116
|
+
8. response_generator — Draft empathetic, helpful first responses
|
|
117
|
+
9. label_suggester — Suggest GitHub labels based on content
|
|
118
|
+
10. duplicate_detector — Find similar existing issues
|
|
119
|
+
11. priority_scorer — Score issue urgency based on impact/frequency
|
|
120
|
+
12. escalation_router — Route critical issues to the right team
|
|
121
|
+
13. weekly_report_compiler — Aggregate triage data into reports
|
|
122
|
+
14. notification_dispatcher — Alert team members about urgent issues
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
SYSTEM_PROMPT = """You are Sage, a community manager for OpenClaw, an open-source
|
|
126
|
+
system of 10 specialized AI agents that replaces a full DevRel + Sales team for DevTools
|
|
127
|
+
companies. OpenClaw provides orchestration (Atlas), an agent SDK, MCP tools,
|
|
128
|
+
a knowledge base, scoring/eval, prompt optimization, onboarding/docs, and security —
|
|
129
|
+
built on Claude SDK + MCP.
|
|
130
|
+
|
|
131
|
+
Your mission is to make every developer who interacts with OpenClaw feel heard,
|
|
132
|
+
helped, and valued. You triage issues with empathy and precision.
|
|
133
|
+
|
|
134
|
+
Triage principles:
|
|
135
|
+
1. EMPATHY FIRST — Acknowledge the person's frustration before diving into technical details
|
|
136
|
+
2. FAST RESPONSE — First response within 4 hours for critical, 24 hours for all others
|
|
137
|
+
3. ACCURATE ROUTING — Tag the right product area and team so nothing falls through cracks
|
|
138
|
+
4. CHURN PREVENTION — Flag users who show signs of giving up (repeated issues, frustrated tone)
|
|
139
|
+
5. CHAMPION CULTIVATION — Recognize users who help others, submit quality bug reports, or contribute PRs
|
|
140
|
+
|
|
141
|
+
Sentiment signals:
|
|
142
|
+
- CHURNING: "I'm switching to...", "This is the Nth time...", "I give up"
|
|
143
|
+
- FRUSTRATED: Multiple exclamation marks, ALL CAPS, words like "broken", "terrible"
|
|
144
|
+
- NEUTRAL: Standard bug reports, feature requests
|
|
145
|
+
- POSITIVE: "Love OpenClaw", "Great work", "This is exactly what I needed"
|
|
146
|
+
|
|
147
|
+
Champion signals:
|
|
148
|
+
- Helps other users in issues/Discourse
|
|
149
|
+
- Submits well-structured bug reports with reproduction steps
|
|
150
|
+
- Opens PRs or suggests fixes
|
|
151
|
+
- Shares OpenClaw content on social media"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
api_client: PostHogClient,
|
|
156
|
+
knowledge_base_path: Path,
|
|
157
|
+
github_tools: Optional["GitHubTools"] = None,
|
|
158
|
+
):
|
|
159
|
+
self.api_client = api_client
|
|
160
|
+
self.knowledge_base_path = knowledge_base_path
|
|
161
|
+
self.github_tools = github_tools
|
|
162
|
+
|
|
163
|
+
async def execute(
|
|
164
|
+
self,
|
|
165
|
+
task: str,
|
|
166
|
+
context: Optional[dict[str, Any]] = None,
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""
|
|
169
|
+
Execute a community management task.
|
|
170
|
+
|
|
171
|
+
Fetches GitHub issues, analyzes them, and produces triage output
|
|
172
|
+
with sentiment analysis and risk flags.
|
|
173
|
+
"""
|
|
174
|
+
logger.info(f"Sage executing: {task[:80]}...")
|
|
175
|
+
|
|
176
|
+
# Fetch issues from GitHub (graceful fallback if no tools)
|
|
177
|
+
raw_issues: list[GitHubIssue] = []
|
|
178
|
+
if self.github_tools:
|
|
179
|
+
try:
|
|
180
|
+
raw_issues = await self.github_tools.fetch_recent_issues(days=7)
|
|
181
|
+
# Filter out PRs
|
|
182
|
+
raw_issues = [i for i in raw_issues if not i.is_pull_request]
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
logger.warning(f"GitHub fetch failed: {exc}")
|
|
185
|
+
|
|
186
|
+
# Triage each issue
|
|
187
|
+
triaged: list[TriagedIssue] = []
|
|
188
|
+
for issue in raw_issues:
|
|
189
|
+
triaged.append(
|
|
190
|
+
await self.triage_issue(
|
|
191
|
+
issue_number=issue.number,
|
|
192
|
+
title=issue.title,
|
|
193
|
+
body=issue.body,
|
|
194
|
+
author=issue.author,
|
|
195
|
+
comments_count=getattr(issue, "comments_count", 0) or 0,
|
|
196
|
+
reactions_total=getattr(issue, "reactions_total", 0) or 0,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Build breakdowns
|
|
201
|
+
sentiment_breakdown: dict[str, int] = {}
|
|
202
|
+
category_breakdown: dict[str, int] = {}
|
|
203
|
+
product_area_breakdown: dict[str, int] = {}
|
|
204
|
+
churn_risks: list[str] = []
|
|
205
|
+
|
|
206
|
+
for t in triaged:
|
|
207
|
+
sentiment_breakdown[t.sentiment.value] = (
|
|
208
|
+
sentiment_breakdown.get(t.sentiment.value, 0) + 1
|
|
209
|
+
)
|
|
210
|
+
category_breakdown[t.category] = category_breakdown.get(t.category, 0) + 1
|
|
211
|
+
product_area_breakdown[t.product_area] = (
|
|
212
|
+
product_area_breakdown.get(t.product_area, 0) + 1
|
|
213
|
+
)
|
|
214
|
+
if t.churn_risk:
|
|
215
|
+
churn_risks.append(t.author)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"agent": "sage",
|
|
219
|
+
"task": task,
|
|
220
|
+
"issues": [
|
|
221
|
+
{
|
|
222
|
+
"number": t.issue_number,
|
|
223
|
+
"title": t.title,
|
|
224
|
+
"author": t.author,
|
|
225
|
+
"priority": t.priority.value,
|
|
226
|
+
"sentiment": t.sentiment.value,
|
|
227
|
+
"category": t.category,
|
|
228
|
+
"product_area": t.product_area,
|
|
229
|
+
"summary": t.summary,
|
|
230
|
+
"suggested_response": t.suggested_response,
|
|
231
|
+
"churn_risk": t.churn_risk,
|
|
232
|
+
}
|
|
233
|
+
for t in triaged
|
|
234
|
+
],
|
|
235
|
+
"churn_risks": churn_risks,
|
|
236
|
+
"champions": self._identify_champions(triaged),
|
|
237
|
+
"sentiment_breakdown": sentiment_breakdown,
|
|
238
|
+
"category_breakdown": category_breakdown,
|
|
239
|
+
"product_area_breakdown": product_area_breakdown,
|
|
240
|
+
"status": "triaged",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async def triage_issue(
|
|
244
|
+
self,
|
|
245
|
+
issue_number: int,
|
|
246
|
+
title: str,
|
|
247
|
+
body: str,
|
|
248
|
+
author: str,
|
|
249
|
+
comments_count: int = 0,
|
|
250
|
+
reactions_total: int = 0,
|
|
251
|
+
) -> TriagedIssue:
|
|
252
|
+
"""Triage a single GitHub issue."""
|
|
253
|
+
# Analyze sentiment
|
|
254
|
+
sentiment = self._analyze_sentiment(body)
|
|
255
|
+
|
|
256
|
+
# Detect churn risk
|
|
257
|
+
churn_risk = sentiment == SentimentScore.CHURNING
|
|
258
|
+
|
|
259
|
+
# Categorize
|
|
260
|
+
category = self._categorize_issue(title, body)
|
|
261
|
+
product_area = self._detect_product_area(title, body)
|
|
262
|
+
priority = self._score_priority(title, body, sentiment)
|
|
263
|
+
|
|
264
|
+
# Champion detection — high-engagement issues / PR-referencing bodies
|
|
265
|
+
# are candidate champions for downstream identification.
|
|
266
|
+
champion = self._detect_champion_signal(
|
|
267
|
+
body=body,
|
|
268
|
+
comments_count=comments_count,
|
|
269
|
+
reactions_total=reactions_total,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return TriagedIssue(
|
|
273
|
+
issue_number=issue_number,
|
|
274
|
+
title=title,
|
|
275
|
+
author=author,
|
|
276
|
+
priority=priority,
|
|
277
|
+
sentiment=sentiment,
|
|
278
|
+
category=category,
|
|
279
|
+
product_area=product_area,
|
|
280
|
+
summary=f"[{priority.value.upper()}] {category} in {product_area}",
|
|
281
|
+
suggested_response=self._draft_response(
|
|
282
|
+
title,
|
|
283
|
+
category,
|
|
284
|
+
priority,
|
|
285
|
+
sentiment,
|
|
286
|
+
author,
|
|
287
|
+
),
|
|
288
|
+
churn_risk=churn_risk,
|
|
289
|
+
champion_signal=champion,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Module-level threshold map — tuned high enough that random noise
|
|
293
|
+
# doesn't trigger champion-flagging, low enough that genuine community
|
|
294
|
+
# engagement is caught.
|
|
295
|
+
CHAMPION_THRESHOLDS = {
|
|
296
|
+
"comments_count": 3,
|
|
297
|
+
"reactions_total": 5,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
def _detect_champion_signal(
|
|
301
|
+
self,
|
|
302
|
+
body: str,
|
|
303
|
+
comments_count: int = 0,
|
|
304
|
+
reactions_total: int = 0,
|
|
305
|
+
) -> bool:
|
|
306
|
+
"""Return True if the issue shows champion-grade engagement.
|
|
307
|
+
|
|
308
|
+
A "champion signal" means the user is going beyond just filing —
|
|
309
|
+
attracting community discussion (comments), strong reactions, or
|
|
310
|
+
referencing a PR they opened to fix the issue. Used by
|
|
311
|
+
``_identify_champions`` downstream.
|
|
312
|
+
"""
|
|
313
|
+
if (comments_count or 0) >= self.CHAMPION_THRESHOLDS["comments_count"]:
|
|
314
|
+
return True
|
|
315
|
+
if (reactions_total or 0) >= self.CHAMPION_THRESHOLDS["reactions_total"]:
|
|
316
|
+
return True
|
|
317
|
+
body_lower = (body or "").lower()
|
|
318
|
+
if "pr #" in body_lower or "#pull" in body_lower or "pull/" in body_lower:
|
|
319
|
+
return True
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
def _analyze_sentiment(self, text: str) -> SentimentScore:
|
|
323
|
+
"""Rule-based sentiment pre-filter before LLM analysis."""
|
|
324
|
+
text_lower = text.lower()
|
|
325
|
+
|
|
326
|
+
if any(signal in text_lower for signal in CHURN_SIGNALS):
|
|
327
|
+
return SentimentScore.CHURNING
|
|
328
|
+
if any(signal in text_lower for signal in FRUSTRATION_SIGNALS):
|
|
329
|
+
return SentimentScore.FRUSTRATED
|
|
330
|
+
if any(word in text_lower for word in ["love", "great", "awesome", "thanks"]):
|
|
331
|
+
return SentimentScore.POSITIVE
|
|
332
|
+
return SentimentScore.NEUTRAL
|
|
333
|
+
|
|
334
|
+
def _categorize_issue(self, title: str, body: str) -> str:
|
|
335
|
+
"""Categorize issue type based on content."""
|
|
336
|
+
text = f"{title} {body}".lower()
|
|
337
|
+
if any(w in text for w in BUG_KEYWORDS):
|
|
338
|
+
return "bug"
|
|
339
|
+
if any(w in text for w in ["feature", "request", "would be nice", "suggestion"]):
|
|
340
|
+
return "feature_request"
|
|
341
|
+
if any(w in text for w in ["how to", "question", "help", "?"]):
|
|
342
|
+
return "question"
|
|
343
|
+
if any(w in text for w in ["docs", "documentation", "readme", "typo"]):
|
|
344
|
+
return "docs"
|
|
345
|
+
if any(w in text for w in ["slow", "performance", "latency", "timeout"]):
|
|
346
|
+
return "performance"
|
|
347
|
+
return "bug"
|
|
348
|
+
|
|
349
|
+
def _detect_product_area(self, title: str, body: str) -> str:
|
|
350
|
+
"""Map issue to OpenClaw product area."""
|
|
351
|
+
text = f"{title} {body}".lower()
|
|
352
|
+
areas = {
|
|
353
|
+
"orchestration": [
|
|
354
|
+
"orchestrat",
|
|
355
|
+
"atlas",
|
|
356
|
+
"pipeline",
|
|
357
|
+
"weekly cycle",
|
|
358
|
+
"delegation",
|
|
359
|
+
"hub",
|
|
360
|
+
"spoke",
|
|
361
|
+
],
|
|
362
|
+
"agent_sdk": [
|
|
363
|
+
"agent sdk",
|
|
364
|
+
"sdk",
|
|
365
|
+
"claude sdk",
|
|
366
|
+
"agent framework",
|
|
367
|
+
"base agent",
|
|
368
|
+
"execute",
|
|
369
|
+
],
|
|
370
|
+
"mcp_tools": ["mcp", "tool", "json-rpc", "stdio", "tool definition", "manifest"],
|
|
371
|
+
"knowledge_base": [
|
|
372
|
+
"knowledge base",
|
|
373
|
+
"knowledge",
|
|
374
|
+
"docs",
|
|
375
|
+
"markdown",
|
|
376
|
+
"rglob",
|
|
377
|
+
"indexing",
|
|
378
|
+
],
|
|
379
|
+
"scoring_eval": [
|
|
380
|
+
"score",
|
|
381
|
+
"scoring",
|
|
382
|
+
"eval",
|
|
383
|
+
"evaluation",
|
|
384
|
+
"metrics",
|
|
385
|
+
"benchmark",
|
|
386
|
+
"test",
|
|
387
|
+
],
|
|
388
|
+
"prompt_optimization": ["prompt", "template", "optimization", "tuning", "generation"],
|
|
389
|
+
"onboarding_docs": [
|
|
390
|
+
"onboarding",
|
|
391
|
+
"documentation",
|
|
392
|
+
"tutorial",
|
|
393
|
+
"guide",
|
|
394
|
+
"getting started",
|
|
395
|
+
"setup",
|
|
396
|
+
],
|
|
397
|
+
"security": [
|
|
398
|
+
"security",
|
|
399
|
+
"auth",
|
|
400
|
+
"permission",
|
|
401
|
+
"token",
|
|
402
|
+
"secret",
|
|
403
|
+
"vulnerability",
|
|
404
|
+
"access",
|
|
405
|
+
],
|
|
406
|
+
}
|
|
407
|
+
for area, keywords in areas.items():
|
|
408
|
+
if any(kw in text for kw in keywords):
|
|
409
|
+
return area
|
|
410
|
+
return "orchestration" # default
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _identify_champions(triaged: list[TriagedIssue]) -> list[str]:
|
|
414
|
+
"""Identify community champions from triaged issues.
|
|
415
|
+
|
|
416
|
+
Champions are users with positive sentiment, helpful contributions,
|
|
417
|
+
or multiple quality issue reports.
|
|
418
|
+
"""
|
|
419
|
+
author_signals: dict[str, int] = {}
|
|
420
|
+
for issue in triaged:
|
|
421
|
+
author = issue.author
|
|
422
|
+
score = 0
|
|
423
|
+
if issue.sentiment == SentimentScore.POSITIVE:
|
|
424
|
+
score += 2
|
|
425
|
+
if issue.champion_signal:
|
|
426
|
+
score += 3
|
|
427
|
+
if issue.category in ("feature_request", "question"):
|
|
428
|
+
score += 1 # Engaged users file features/questions
|
|
429
|
+
if score > 0:
|
|
430
|
+
author_signals[author] = author_signals.get(author, 0) + score
|
|
431
|
+
|
|
432
|
+
# Champions = authors with score >= 3
|
|
433
|
+
return [author for author, score in author_signals.items() if score >= 3]
|
|
434
|
+
|
|
435
|
+
def _score_priority(self, title: str, body: str, sentiment: SentimentScore) -> IssuePriority:
|
|
436
|
+
"""Score issue priority based on content and sentiment."""
|
|
437
|
+
text = f"{title} {body}".lower()
|
|
438
|
+
if any(w in text for w in CRITICAL_KEYWORDS):
|
|
439
|
+
return IssuePriority.CRITICAL
|
|
440
|
+
if sentiment == SentimentScore.CHURNING:
|
|
441
|
+
return IssuePriority.HIGH
|
|
442
|
+
if any(w in text for w in ["broken", "cannot", "unable", "blocking"]):
|
|
443
|
+
return IssuePriority.HIGH
|
|
444
|
+
if sentiment == SentimentScore.FRUSTRATED:
|
|
445
|
+
return IssuePriority.MEDIUM
|
|
446
|
+
return IssuePriority.LOW
|
|
447
|
+
|
|
448
|
+
def _draft_response(
|
|
449
|
+
self,
|
|
450
|
+
title: str,
|
|
451
|
+
category: str,
|
|
452
|
+
priority: IssuePriority,
|
|
453
|
+
sentiment: SentimentScore = SentimentScore.NEUTRAL,
|
|
454
|
+
author: str = "",
|
|
455
|
+
) -> str:
|
|
456
|
+
"""Draft a suggested first response.
|
|
457
|
+
|
|
458
|
+
Branch order matters: a CHURNING user with a CRITICAL bug should
|
|
459
|
+
receive the empathetic response, not the templated critical one,
|
|
460
|
+
so we check sentiment first.
|
|
461
|
+
"""
|
|
462
|
+
# CHURNING comes BEFORE CRITICAL on purpose — frustrated users
|
|
463
|
+
# need empathy first, not a templated triage notice.
|
|
464
|
+
if sentiment == SentimentScore.CHURNING:
|
|
465
|
+
handle = f"@{author} " if author else ""
|
|
466
|
+
return (
|
|
467
|
+
f"Hey {handle}— I hear you, and I'm sorry this has been frustrating. "
|
|
468
|
+
f"This is on me to help you fix. Can you share: (1) what version "
|
|
469
|
+
f"you're on, (2) the exact error or behavior you're seeing, and "
|
|
470
|
+
f"(3) what you've already tried? I'll dig in personally."
|
|
471
|
+
)
|
|
472
|
+
if priority == IssuePriority.CRITICAL:
|
|
473
|
+
return (
|
|
474
|
+
"Thanks for reporting this — we're treating this as critical priority. "
|
|
475
|
+
"Our team is investigating now. Could you share your OpenClaw version "
|
|
476
|
+
"and any relevant error logs?"
|
|
477
|
+
)
|
|
478
|
+
if category == "question":
|
|
479
|
+
return (
|
|
480
|
+
"Great question! Let me point you to the relevant docs. "
|
|
481
|
+
"If those don't fully answer it, let us know and we'll dig deeper."
|
|
482
|
+
)
|
|
483
|
+
return (
|
|
484
|
+
"Thanks for raising this! We've added it to our triage queue. "
|
|
485
|
+
"A team member will follow up shortly."
|
|
486
|
+
)
|