content-creator 0.6.0__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.
- content_creator/__init__.py +9 -0
- content_creator/agent_resources.py +171 -0
- content_creator/attribution.py +129 -0
- content_creator/cli.py +1273 -0
- content_creator/configuration.py +158 -0
- content_creator/context.py +106 -0
- content_creator/coordinator.py +514 -0
- content_creator/corpus.py +47 -0
- content_creator/domain.py +279 -0
- content_creator/evaluation.py +188 -0
- content_creator/experience.py +94 -0
- content_creator/health.py +36 -0
- content_creator/ingestion.py +95 -0
- content_creator/intake.py +86 -0
- content_creator/learning.py +85 -0
- content_creator/linguistics.py +292 -0
- content_creator/orchestrator.py +627 -0
- content_creator/overlap.py +18 -0
- content_creator/packs.py +139 -0
- content_creator/perspective_assessment.py +108 -0
- content_creator/perspective_evaluation.py +77 -0
- content_creator/perspectives.py +763 -0
- content_creator/prompting.py +186 -0
- content_creator/providers/__init__.py +5 -0
- content_creator/providers/anthropic.py +61 -0
- content_creator/providers/base.py +17 -0
- content_creator/providers/claude_native.py +140 -0
- content_creator/providers/codex_native.py +139 -0
- content_creator/providers/fake.py +37 -0
- content_creator/providers/native_cli.py +101 -0
- content_creator/providers/openai.py +60 -0
- content_creator/providers/registry.py +43 -0
- content_creator/quality.py +57 -0
- content_creator/resource_paths.py +46 -0
- content_creator/resources/__init__.py +0 -0
- content_creator/resources/agent-templates/standard/agents/README.md +23 -0
- content_creator/resources/agent-templates/standard/agents/attribution-reviewer.md +8 -0
- content_creator/resources/agent-templates/standard/agents/briefing-agent.md +34 -0
- content_creator/resources/agent-templates/standard/agents/critic-learnings.md +5 -0
- content_creator/resources/agent-templates/standard/agents/critic.md +38 -0
- content_creator/resources/agent-templates/standard/agents/learning-extractor.md +86 -0
- content_creator/resources/agent-templates/standard/agents/perspective-extractor.md +39 -0
- content_creator/resources/agent-templates/standard/agents/profile-critic.md +18 -0
- content_creator/resources/agent-templates/standard/agents/researcher-learnings.md +5 -0
- content_creator/resources/agent-templates/standard/agents/researcher.md +48 -0
- content_creator/resources/agent-templates/standard/agents/template.json +6 -0
- content_creator/resources/agent-templates/standard/agents/voice-analyst.md +33 -0
- content_creator/resources/agent-templates/standard/agents/voice-evaluator.md +16 -0
- content_creator/resources/agent-templates/standard/agents/writer-learnings.md +5 -0
- content_creator/resources/agent-templates/standard/agents/writer.md +51 -0
- content_creator/resources/config/models.yaml +87 -0
- content_creator/resources/contracts/agent-harness.md +19 -0
- content_creator/resources/contracts/roles/attribution-reviewer.md +5 -0
- content_creator/resources/contracts/roles/briefing-agent.md +10 -0
- content_creator/resources/contracts/roles/critic.md +6 -0
- content_creator/resources/contracts/roles/learning-extractor.md +6 -0
- content_creator/resources/contracts/roles/perspective-extractor.md +5 -0
- content_creator/resources/contracts/roles/profile-critic.md +5 -0
- content_creator/resources/contracts/roles/researcher.md +6 -0
- content_creator/resources/contracts/roles/voice-analyst.md +5 -0
- content_creator/resources/contracts/roles/voice-evaluator.md +5 -0
- content_creator/resources/contracts/roles/writer.md +6 -0
- content_creator/resources/evals/cases/route-matrix.yaml +49 -0
- content_creator/resources/packs/general-text/pack.json +41 -0
- content_creator/resources/packs/general-text/rubric.yaml +18 -0
- content_creator/resources/packs/linkedin-article/pack.json +11 -0
- content_creator/resources/packs/linkedin-article/rubric.yaml +16 -0
- content_creator/resources/packs/linkedin-post/pack.json +11 -0
- content_creator/resources/packs/linkedin-post/rubric.yaml +16 -0
- content_creator/resources/profiles/default/learnings/memory.json +4 -0
- content_creator/resources/profiles/default/voice.md +13 -0
- content_creator/resources/profiles/starter/clear-professional.md +23 -0
- content_creator/resources/rubrics/core.yaml +35 -0
- content_creator/resources/rubrics/research-deep.yaml +15 -0
- content_creator/resources/rubrics/research-light.yaml +12 -0
- content_creator/resources/rubrics/research-none.yaml +12 -0
- content_creator/resources/skills/content-creator/SKILL.md +117 -0
- content_creator/resources/skills/content-creator/agents/openai.yaml +4 -0
- content_creator/resources/skills/voice-builder/SKILL.md +95 -0
- content_creator/resources/skills/voice-builder/agents/openai.yaml +4 -0
- content_creator/routing.py +52 -0
- content_creator/runner.py +73 -0
- content_creator/storage.py +86 -0
- content_creator/upgrade.py +225 -0
- content_creator/validation.py +69 -0
- content_creator/version.py +1 -0
- content_creator/voice_builder.py +691 -0
- content_creator/voice_evaluation.py +44 -0
- content_creator/voices.py +618 -0
- content_creator/workspace.py +759 -0
- content_creator-0.6.0.dist-info/METADATA +181 -0
- content_creator-0.6.0.dist-info/RECORD +97 -0
- content_creator-0.6.0.dist-info/WHEEL +5 -0
- content_creator-0.6.0.dist-info/entry_points.txt +2 -0
- content_creator-0.6.0.dist-info/licenses/LICENSE.md +661 -0
- content_creator-0.6.0.dist-info/licenses/NOTICE +14 -0
- content_creator-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
from .resource_paths import ResourceResolver
|
|
9
|
+
from .storage import RunStore
|
|
10
|
+
|
|
11
|
+
ROLE_FILES = {
|
|
12
|
+
"briefing-agent": "briefing-agent.md",
|
|
13
|
+
"researcher": "researcher.md",
|
|
14
|
+
"writer": "writer.md",
|
|
15
|
+
"critic": "critic.md",
|
|
16
|
+
"learning-extractor": "learning-extractor.md",
|
|
17
|
+
"voice-analyst": "voice-analyst.md",
|
|
18
|
+
"profile-critic": "profile-critic.md",
|
|
19
|
+
"attribution-reviewer": "attribution-reviewer.md",
|
|
20
|
+
"voice-evaluator": "voice-evaluator.md",
|
|
21
|
+
"perspective-extractor": "perspective-extractor.md",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
LEARNING_FILES = {
|
|
25
|
+
"researcher": "researcher-learnings.md",
|
|
26
|
+
"writer": "writer-learnings.md",
|
|
27
|
+
"critic": "critic-learnings.md",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
STANDARD_TEMPLATE = "standard"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _digest(path: Path) -> str:
|
|
34
|
+
return "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentWorkspace:
|
|
38
|
+
"""Scaffold and inspect repository-owned agent instructions."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, root: Path):
|
|
41
|
+
self.root = root.resolve()
|
|
42
|
+
self.resources = ResourceResolver(self.root)
|
|
43
|
+
self.agents = self.root / "agents"
|
|
44
|
+
self.learnings = self.root / "learnings" / "memory.json"
|
|
45
|
+
|
|
46
|
+
def template_root(self, template: str = STANDARD_TEMPLATE) -> Path:
|
|
47
|
+
path = (
|
|
48
|
+
self.resources.core
|
|
49
|
+
/ "agent-templates"
|
|
50
|
+
/ template
|
|
51
|
+
/ "agents"
|
|
52
|
+
)
|
|
53
|
+
if not path.is_dir():
|
|
54
|
+
raise ValueError("Unknown agent template: {}".format(template))
|
|
55
|
+
return path
|
|
56
|
+
|
|
57
|
+
def template_metadata(
|
|
58
|
+
self, template: str = STANDARD_TEMPLATE
|
|
59
|
+
) -> Dict[str, Any]:
|
|
60
|
+
return json.loads(
|
|
61
|
+
(self.template_root(template) / "template.json").read_text(
|
|
62
|
+
encoding="utf-8"
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def scaffold(self, template: str = STANDARD_TEMPLATE) -> Dict[str, Any]:
|
|
67
|
+
template_root = self.template_root(template)
|
|
68
|
+
self.agents.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
created: List[str] = []
|
|
70
|
+
preserved: List[str] = []
|
|
71
|
+
for source in sorted(template_root.iterdir()):
|
|
72
|
+
if not source.is_file():
|
|
73
|
+
continue
|
|
74
|
+
destination = self.agents / source.name
|
|
75
|
+
if destination.exists():
|
|
76
|
+
preserved.append(source.name)
|
|
77
|
+
continue
|
|
78
|
+
RunStore._atomic_text(
|
|
79
|
+
destination,
|
|
80
|
+
source.read_text(encoding="utf-8").rstrip("\n"),
|
|
81
|
+
)
|
|
82
|
+
created.append(source.name)
|
|
83
|
+
if not self.learnings.exists():
|
|
84
|
+
RunStore._atomic_text(
|
|
85
|
+
self.learnings,
|
|
86
|
+
json.dumps({"version": 1, "records": []}, indent=2),
|
|
87
|
+
)
|
|
88
|
+
created.append("learnings/memory.json")
|
|
89
|
+
else:
|
|
90
|
+
preserved.append("learnings/memory.json")
|
|
91
|
+
return {
|
|
92
|
+
"template": template,
|
|
93
|
+
"template_metadata": self.template_metadata(template),
|
|
94
|
+
"created": created,
|
|
95
|
+
"preserved": preserved,
|
|
96
|
+
"status": self.status(template),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def status(self, template: str = STANDARD_TEMPLATE) -> Dict[str, Any]:
|
|
100
|
+
self.template_root(template)
|
|
101
|
+
expected = sorted(set(ROLE_FILES.values()) | set(LEARNING_FILES.values()))
|
|
102
|
+
missing = [
|
|
103
|
+
name for name in expected if not (self.agents / name).is_file()
|
|
104
|
+
]
|
|
105
|
+
return {
|
|
106
|
+
"template": template,
|
|
107
|
+
"complete": not missing and self.learnings.is_file(),
|
|
108
|
+
"missing": missing,
|
|
109
|
+
"template_provenance": (
|
|
110
|
+
self.agents / "template.json"
|
|
111
|
+
).is_file(),
|
|
112
|
+
"repository_learning_memory": self.learnings.is_file(),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
def diff_template(
|
|
116
|
+
self, template: str = STANDARD_TEMPLATE
|
|
117
|
+
) -> Dict[str, Any]:
|
|
118
|
+
template_root = self.template_root(template)
|
|
119
|
+
expected = {
|
|
120
|
+
path.name: path
|
|
121
|
+
for path in template_root.iterdir()
|
|
122
|
+
if path.is_file()
|
|
123
|
+
}
|
|
124
|
+
workspace = {
|
|
125
|
+
path.name: path
|
|
126
|
+
for path in self.agents.iterdir()
|
|
127
|
+
if path.is_file()
|
|
128
|
+
} if self.agents.is_dir() else {}
|
|
129
|
+
changed = [
|
|
130
|
+
name
|
|
131
|
+
for name in sorted(expected.keys() & workspace.keys())
|
|
132
|
+
if _digest(expected[name]) != _digest(workspace[name])
|
|
133
|
+
]
|
|
134
|
+
return {
|
|
135
|
+
"template": template,
|
|
136
|
+
"changed": changed,
|
|
137
|
+
"missing": sorted(expected.keys() - workspace.keys()),
|
|
138
|
+
"unchanged": sorted(
|
|
139
|
+
name
|
|
140
|
+
for name in expected.keys() & workspace.keys()
|
|
141
|
+
if name not in changed
|
|
142
|
+
),
|
|
143
|
+
"additional": sorted(workspace.keys() - expected.keys()),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def role_path(self, role: str) -> Path:
|
|
147
|
+
path = self.agents / ROLE_FILES[role]
|
|
148
|
+
if not path.is_file():
|
|
149
|
+
raise FileNotFoundError(
|
|
150
|
+
"Repository agent is missing: {}. Run "
|
|
151
|
+
"'content-creator --workspace . agents scaffold'.".format(path)
|
|
152
|
+
)
|
|
153
|
+
return path
|
|
154
|
+
|
|
155
|
+
def learning_instructions_path(self, role: str) -> Path:
|
|
156
|
+
path = self.agents / LEARNING_FILES[role]
|
|
157
|
+
if not path.is_file():
|
|
158
|
+
raise FileNotFoundError(
|
|
159
|
+
"Repository learning instructions are missing: {}. Run "
|
|
160
|
+
"'content-creator --workspace . agents scaffold'.".format(path)
|
|
161
|
+
)
|
|
162
|
+
return path
|
|
163
|
+
|
|
164
|
+
def harness_path(self) -> Path:
|
|
165
|
+
return self.resources.core / "contracts" / "agent-harness.md"
|
|
166
|
+
|
|
167
|
+
def contract_path(self, role: str) -> Path:
|
|
168
|
+
path = self.resources.core / "contracts" / "roles" / ROLE_FILES[role]
|
|
169
|
+
if not path.is_file():
|
|
170
|
+
raise FileNotFoundError("Core role contract is missing: {}".format(path))
|
|
171
|
+
return path
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Iterable, List, Optional
|
|
5
|
+
|
|
6
|
+
from .voices import AttributionResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _names(author_name: str, aliases: Optional[Iterable[str]]) -> List[str]:
|
|
10
|
+
return list(
|
|
11
|
+
dict.fromkeys(
|
|
12
|
+
name.strip()
|
|
13
|
+
for name in [author_name, *(aliases or [])]
|
|
14
|
+
if name and name.strip()
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def classify_attribution(
|
|
20
|
+
text: str,
|
|
21
|
+
author_name: str,
|
|
22
|
+
kind: str,
|
|
23
|
+
aliases: Optional[Iterable[str]] = None,
|
|
24
|
+
) -> AttributionResult:
|
|
25
|
+
names = _names(author_name, aliases)
|
|
26
|
+
for name in names:
|
|
27
|
+
escaped = re.escape(name)
|
|
28
|
+
if re.search(
|
|
29
|
+
r"written by[^\n.]*" + escaped + r"[^\n.]*(?:and|&)",
|
|
30
|
+
text,
|
|
31
|
+
re.I,
|
|
32
|
+
):
|
|
33
|
+
return AttributionResult(
|
|
34
|
+
classification="co_authored",
|
|
35
|
+
confidence=0.8,
|
|
36
|
+
voice_weight=0.65,
|
|
37
|
+
evidence=["Co-authorship marker includes {}".format(name)],
|
|
38
|
+
)
|
|
39
|
+
if re.search(r"(?:by|author[:\s]+)\s*" + escaped, text, re.I):
|
|
40
|
+
return AttributionResult(
|
|
41
|
+
classification="directly_authored",
|
|
42
|
+
confidence=0.95,
|
|
43
|
+
voice_weight=1.0,
|
|
44
|
+
evidence=["Visible author or byline matches {}".format(name)],
|
|
45
|
+
)
|
|
46
|
+
if kind == "transcript":
|
|
47
|
+
first_names = [re.escape(name.split()[0]) for name in names]
|
|
48
|
+
if any(
|
|
49
|
+
re.search(r"(?:^|\s)" + first + r"\s*:", text, re.I)
|
|
50
|
+
for first in first_names
|
|
51
|
+
):
|
|
52
|
+
return AttributionResult(
|
|
53
|
+
classification="interview",
|
|
54
|
+
confidence=0.85,
|
|
55
|
+
voice_weight=0.5,
|
|
56
|
+
evidence=["Transcript contains speaker-labelled contributions"],
|
|
57
|
+
)
|
|
58
|
+
if any(re.search(re.escape(name), text, re.I) for name in names):
|
|
59
|
+
return AttributionResult(
|
|
60
|
+
classification="person_as_subject",
|
|
61
|
+
confidence=0.65,
|
|
62
|
+
voice_weight=0.0,
|
|
63
|
+
evidence=["Person is mentioned but authorship is not established"],
|
|
64
|
+
needs_human_review=True,
|
|
65
|
+
)
|
|
66
|
+
return AttributionResult(
|
|
67
|
+
classification="uncertain",
|
|
68
|
+
confidence=0.2,
|
|
69
|
+
voice_weight=0.0,
|
|
70
|
+
evidence=["No reliable authorship evidence was found"],
|
|
71
|
+
needs_human_review=True,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def isolate_attributed_text(
|
|
76
|
+
text: str,
|
|
77
|
+
author_name: str,
|
|
78
|
+
attribution: AttributionResult,
|
|
79
|
+
kind: str,
|
|
80
|
+
aliases: Optional[Iterable[str]] = None,
|
|
81
|
+
) -> tuple[str, str]:
|
|
82
|
+
"""Conservatively isolate analysable language without claiming authorship."""
|
|
83
|
+
|
|
84
|
+
cleaned = text.strip()
|
|
85
|
+
names = _names(author_name, aliases)
|
|
86
|
+
if attribution.classification == "directly_authored":
|
|
87
|
+
name_pattern = "(?:{})".format(
|
|
88
|
+
"|".join(re.escape(name) for name in names)
|
|
89
|
+
)
|
|
90
|
+
cleaned = re.sub(
|
|
91
|
+
r"^\s*(?:written\s+by|by|author[:\s]+)\s*"
|
|
92
|
+
+ name_pattern
|
|
93
|
+
+ r"\s*[.,:;—-]*\s*",
|
|
94
|
+
"",
|
|
95
|
+
cleaned,
|
|
96
|
+
count=1,
|
|
97
|
+
flags=re.I,
|
|
98
|
+
)
|
|
99
|
+
return cleaned, "full-source-with-byline-removed"
|
|
100
|
+
|
|
101
|
+
if kind == "transcript" and attribution.classification == "interview":
|
|
102
|
+
speaker_names = {
|
|
103
|
+
value
|
|
104
|
+
for name in names
|
|
105
|
+
for value in (name.lower(), name.split()[0].lower())
|
|
106
|
+
}
|
|
107
|
+
selected = []
|
|
108
|
+
active = False
|
|
109
|
+
saw_speaker_label = False
|
|
110
|
+
for line in cleaned.splitlines():
|
|
111
|
+
match = re.match(r"^\s*([^:\n]{1,80})\s*:\s*(.*)$", line)
|
|
112
|
+
if match:
|
|
113
|
+
saw_speaker_label = True
|
|
114
|
+
active = match.group(1).strip().lower() in speaker_names
|
|
115
|
+
if active and match.group(2).strip():
|
|
116
|
+
selected.append(match.group(2).strip())
|
|
117
|
+
elif active and line.strip():
|
|
118
|
+
selected.append(line.strip())
|
|
119
|
+
if selected:
|
|
120
|
+
return "\n\n".join(selected), "speaker-turns-only"
|
|
121
|
+
if saw_speaker_label:
|
|
122
|
+
return "", "no-attributed-speaker-turns"
|
|
123
|
+
|
|
124
|
+
scope = (
|
|
125
|
+
"shared-source-weighted"
|
|
126
|
+
if attribution.classification == "co_authored"
|
|
127
|
+
else "full-source"
|
|
128
|
+
)
|
|
129
|
+
return cleaned, scope
|