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,219 @@
|
|
|
1
|
+
"""Pillar-agnostic Recommendation dataclass + persistence + lifecycle queries.
|
|
2
|
+
|
|
3
|
+
This module is the contract every Growth-pipeline auditor (and Argus) writes
|
|
4
|
+
through. Each pillar produces `Recommendation` instances and calls
|
|
5
|
+
`persist_recommendation` to land them in `analytics_recommendations`. Lifecycle
|
|
6
|
+
helpers (`find_open_by_target`, `mark_applied`, `find_stale`) drive the
|
|
7
|
+
recommendation closed-loop that Mox consumes for brief generation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sqlite3
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import date, timedelta
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Callable, Optional
|
|
19
|
+
|
|
20
|
+
from devrel_origin.core.growth.target_kinds import (
|
|
21
|
+
Pillar,
|
|
22
|
+
TargetKind,
|
|
23
|
+
validate_target_kind_for_pillar,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Recommendation:
|
|
29
|
+
"""A single structured action recommendation emitted by a Growth auditor.
|
|
30
|
+
|
|
31
|
+
Maps 1:1 to a row in `analytics_recommendations`.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
pillar: Pillar
|
|
35
|
+
action: str
|
|
36
|
+
target: str
|
|
37
|
+
target_kind: TargetKind
|
|
38
|
+
confidence: float
|
|
39
|
+
source_ids: list[str]
|
|
40
|
+
first_seen_period: str
|
|
41
|
+
applied_at: Optional[str] = None
|
|
42
|
+
rationale: Optional[str] = None
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
if isinstance(self.pillar, str):
|
|
46
|
+
self.pillar = Pillar(self.pillar)
|
|
47
|
+
if isinstance(self.target_kind, str):
|
|
48
|
+
self.target_kind = TargetKind(self.target_kind)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def persist_recommendation(db_path: Path, report_id: int, rec: Recommendation) -> None:
|
|
52
|
+
"""Insert a Recommendation into `analytics_recommendations`.
|
|
53
|
+
|
|
54
|
+
Validates `(pillar, target_kind)` before INSERT: accidental cross-pillar
|
|
55
|
+
target_kinds are caught here, not at calibration time.
|
|
56
|
+
"""
|
|
57
|
+
validate_target_kind_for_pillar(rec.pillar, rec.target_kind)
|
|
58
|
+
|
|
59
|
+
with sqlite3.connect(db_path) as conn:
|
|
60
|
+
conn.execute(
|
|
61
|
+
"""
|
|
62
|
+
INSERT INTO analytics_recommendations
|
|
63
|
+
(report_id, period_end, action, target, target_type, rationale,
|
|
64
|
+
confidence, source_ids_json, first_seen_period, applied_at,
|
|
65
|
+
pillar, target_kind)
|
|
66
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
67
|
+
""",
|
|
68
|
+
(
|
|
69
|
+
report_id,
|
|
70
|
+
rec.first_seen_period,
|
|
71
|
+
rec.action,
|
|
72
|
+
rec.target,
|
|
73
|
+
rec.target_kind.value,
|
|
74
|
+
rec.rationale or "",
|
|
75
|
+
rec.confidence,
|
|
76
|
+
json.dumps(rec.source_ids),
|
|
77
|
+
rec.first_seen_period,
|
|
78
|
+
rec.applied_at,
|
|
79
|
+
rec.pillar.value,
|
|
80
|
+
rec.target_kind.value,
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
conn.commit()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _row_to_recommendation(row: tuple) -> Recommendation:
|
|
87
|
+
return Recommendation(
|
|
88
|
+
pillar=Pillar(row[0]),
|
|
89
|
+
action=row[1],
|
|
90
|
+
target=row[2],
|
|
91
|
+
target_kind=TargetKind(row[3]),
|
|
92
|
+
confidence=row[4],
|
|
93
|
+
source_ids=json.loads(row[5] or "[]"),
|
|
94
|
+
first_seen_period=row[6],
|
|
95
|
+
applied_at=row[7],
|
|
96
|
+
rationale=row[8] if len(row) > 8 else None,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def find_open_by_target(db_path: Path, pillar: Pillar) -> list[Recommendation]:
|
|
101
|
+
"""Return all unapplied recommendations for a pillar, newest-first."""
|
|
102
|
+
with sqlite3.connect(db_path) as conn:
|
|
103
|
+
cur = conn.execute(
|
|
104
|
+
"""
|
|
105
|
+
SELECT pillar, action, target, target_kind, confidence,
|
|
106
|
+
source_ids_json, first_seen_period, applied_at, rationale
|
|
107
|
+
FROM analytics_recommendations
|
|
108
|
+
WHERE pillar = ? AND applied_at IS NULL
|
|
109
|
+
ORDER BY first_seen_period DESC
|
|
110
|
+
""",
|
|
111
|
+
(pillar.value,),
|
|
112
|
+
)
|
|
113
|
+
return [_row_to_recommendation(row) for row in cur.fetchall()]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def mark_applied(
|
|
117
|
+
db_path: Path,
|
|
118
|
+
pillar: Pillar,
|
|
119
|
+
*,
|
|
120
|
+
action: str,
|
|
121
|
+
target: str,
|
|
122
|
+
target_kind: TargetKind,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Stamp a recommendation as applied (Mox shipped the change)."""
|
|
125
|
+
with sqlite3.connect(db_path) as conn:
|
|
126
|
+
conn.execute(
|
|
127
|
+
"""
|
|
128
|
+
UPDATE analytics_recommendations
|
|
129
|
+
SET applied_at = datetime('now')
|
|
130
|
+
WHERE pillar = ? AND action = ? AND target = ? AND target_kind = ?
|
|
131
|
+
AND applied_at IS NULL
|
|
132
|
+
""",
|
|
133
|
+
(pillar.value, action, target, target_kind.value),
|
|
134
|
+
)
|
|
135
|
+
conn.commit()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def find_stale(
|
|
139
|
+
db_path: Path,
|
|
140
|
+
pillar: Pillar,
|
|
141
|
+
*,
|
|
142
|
+
current_period: str,
|
|
143
|
+
stale_after_periods: int = 2,
|
|
144
|
+
) -> list[Recommendation]:
|
|
145
|
+
"""Return open recommendations whose first_seen_period is N+ periods old.
|
|
146
|
+
|
|
147
|
+
`period` here is calendar weeks; `stale_after_periods=2` means
|
|
148
|
+
"first_seen at least 14 days before current_period" qualifies as stale.
|
|
149
|
+
"""
|
|
150
|
+
cutoff = date.fromisoformat(current_period) - timedelta(weeks=stale_after_periods)
|
|
151
|
+
with sqlite3.connect(db_path) as conn:
|
|
152
|
+
cur = conn.execute(
|
|
153
|
+
"""
|
|
154
|
+
SELECT pillar, action, target, target_kind, confidence,
|
|
155
|
+
source_ids_json, first_seen_period, applied_at, rationale
|
|
156
|
+
FROM analytics_recommendations
|
|
157
|
+
WHERE pillar = ? AND applied_at IS NULL
|
|
158
|
+
AND first_seen_period <= ?
|
|
159
|
+
ORDER BY first_seen_period ASC
|
|
160
|
+
""",
|
|
161
|
+
(pillar.value, cutoff.isoformat()),
|
|
162
|
+
)
|
|
163
|
+
return [_row_to_recommendation(row) for row in cur.fetchall()]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def calibrate(
|
|
167
|
+
db_path: Path,
|
|
168
|
+
pillar: Pillar,
|
|
169
|
+
*,
|
|
170
|
+
outcome_scorer: Callable[[Recommendation], str],
|
|
171
|
+
) -> dict[str, dict[str, float | int]]:
|
|
172
|
+
"""Per-action hit-rate calibration for one pillar's applied recommendations.
|
|
173
|
+
|
|
174
|
+
`outcome_scorer(rec)` returns one of {'improved', 'unchanged', 'regressed'}.
|
|
175
|
+
Each pillar implements its own scorer based on subsequent fact-table rows
|
|
176
|
+
(for example, SEO checks if keyword position improved; CRO checks if
|
|
177
|
+
conversion rate rose). This helper just aggregates.
|
|
178
|
+
|
|
179
|
+
Returns: {action: {applied_count, hit_rate, lift_vs_coinflip,
|
|
180
|
+
avg_confidence, high_conf_hit_rate}}
|
|
181
|
+
"""
|
|
182
|
+
with sqlite3.connect(db_path) as conn:
|
|
183
|
+
cur = conn.execute(
|
|
184
|
+
"""
|
|
185
|
+
SELECT pillar, action, target, target_kind, confidence,
|
|
186
|
+
source_ids_json, first_seen_period, applied_at, rationale
|
|
187
|
+
FROM analytics_recommendations
|
|
188
|
+
WHERE pillar = ? AND applied_at IS NOT NULL
|
|
189
|
+
""",
|
|
190
|
+
(pillar.value,),
|
|
191
|
+
)
|
|
192
|
+
rows = cur.fetchall()
|
|
193
|
+
|
|
194
|
+
by_action: dict[str, list[tuple[Recommendation, str]]] = defaultdict(list)
|
|
195
|
+
for row in rows:
|
|
196
|
+
rec = _row_to_recommendation(row)
|
|
197
|
+
outcome = outcome_scorer(rec)
|
|
198
|
+
by_action[rec.action].append((rec, outcome))
|
|
199
|
+
|
|
200
|
+
result: dict[str, dict[str, float | int]] = {}
|
|
201
|
+
for action, items in by_action.items():
|
|
202
|
+
n = len(items)
|
|
203
|
+
improved = sum(1 for _, o in items if o == "improved")
|
|
204
|
+
hit_rate = improved / n if n else 0.0
|
|
205
|
+
avg_conf = sum(r.confidence for r, _ in items) / n if n else 0.0
|
|
206
|
+
# high-conf = top half by confidence
|
|
207
|
+
sorted_items = sorted(items, key=lambda t: t[0].confidence, reverse=True)
|
|
208
|
+
high_half = sorted_items[: max(1, n // 2)]
|
|
209
|
+
high_improved = sum(1 for _, o in high_half if o == "improved")
|
|
210
|
+
high_hit = high_improved / len(high_half) if high_half else 0.0
|
|
211
|
+
|
|
212
|
+
result[action] = {
|
|
213
|
+
"applied_count": n,
|
|
214
|
+
"hit_rate": hit_rate,
|
|
215
|
+
"lift_vs_coinflip": hit_rate - 0.5,
|
|
216
|
+
"avg_confidence": avg_conf,
|
|
217
|
+
"high_conf_hit_rate": high_hit,
|
|
218
|
+
}
|
|
219
|
+
return result
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Pillar + TargetKind enums and the (pillar, target_kind) collision guard.
|
|
2
|
+
|
|
3
|
+
Stored as TEXT in SQLite (`analytics_recommendations.pillar` and
|
|
4
|
+
`.target_kind`) but typed at the Python boundary so accidental
|
|
5
|
+
free-form strings are caught at write time, not at calibration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Pillar(str, Enum):
|
|
14
|
+
ARGUS = "argus"
|
|
15
|
+
SEO = "seo"
|
|
16
|
+
GEO = "geo"
|
|
17
|
+
CRO = "cro"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TargetKind(str, Enum):
|
|
21
|
+
CONTENT_ID = "content_id"
|
|
22
|
+
URL = "url"
|
|
23
|
+
KEYWORD = "keyword"
|
|
24
|
+
FUNNEL_STEP = "funnel_step"
|
|
25
|
+
BRAND_QUERY = "brand_query"
|
|
26
|
+
COMPETITOR = "competitor"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Per-pillar allowlists. Cross-cutting kinds (URL is in both SEO + GEO)
|
|
30
|
+
# are the reason we don't just key off pillar alone in the schema.
|
|
31
|
+
_VALID: dict[Pillar, frozenset[TargetKind]] = {
|
|
32
|
+
Pillar.ARGUS: frozenset({TargetKind.CONTENT_ID}),
|
|
33
|
+
Pillar.SEO: frozenset({TargetKind.URL, TargetKind.KEYWORD}),
|
|
34
|
+
Pillar.GEO: frozenset({TargetKind.BRAND_QUERY, TargetKind.URL, TargetKind.COMPETITOR}),
|
|
35
|
+
Pillar.CRO: frozenset({TargetKind.FUNNEL_STEP}),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_target_kind_for_pillar(pillar: Pillar, kind: TargetKind) -> None:
|
|
40
|
+
"""Raise ValueError if `kind` is not a legal target for `pillar`.
|
|
41
|
+
|
|
42
|
+
Called by `persist_recommendation` before INSERT. Keeps the
|
|
43
|
+
cross-pillar query namespace coherent: a `target_kind='url'` row
|
|
44
|
+
is unambiguously SEO or GEO and never anything else.
|
|
45
|
+
"""
|
|
46
|
+
if kind not in _VALID[pillar]:
|
|
47
|
+
valid_names = sorted(k.value for k in _VALID[pillar])
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"target_kind={kind.value!r} not valid for pillar={pillar.value!r}; "
|
|
50
|
+
f"valid kinds: {valid_names}"
|
|
51
|
+
)
|