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.
Files changed (97) hide show
  1. content_creator/__init__.py +9 -0
  2. content_creator/agent_resources.py +171 -0
  3. content_creator/attribution.py +129 -0
  4. content_creator/cli.py +1273 -0
  5. content_creator/configuration.py +158 -0
  6. content_creator/context.py +106 -0
  7. content_creator/coordinator.py +514 -0
  8. content_creator/corpus.py +47 -0
  9. content_creator/domain.py +279 -0
  10. content_creator/evaluation.py +188 -0
  11. content_creator/experience.py +94 -0
  12. content_creator/health.py +36 -0
  13. content_creator/ingestion.py +95 -0
  14. content_creator/intake.py +86 -0
  15. content_creator/learning.py +85 -0
  16. content_creator/linguistics.py +292 -0
  17. content_creator/orchestrator.py +627 -0
  18. content_creator/overlap.py +18 -0
  19. content_creator/packs.py +139 -0
  20. content_creator/perspective_assessment.py +108 -0
  21. content_creator/perspective_evaluation.py +77 -0
  22. content_creator/perspectives.py +763 -0
  23. content_creator/prompting.py +186 -0
  24. content_creator/providers/__init__.py +5 -0
  25. content_creator/providers/anthropic.py +61 -0
  26. content_creator/providers/base.py +17 -0
  27. content_creator/providers/claude_native.py +140 -0
  28. content_creator/providers/codex_native.py +139 -0
  29. content_creator/providers/fake.py +37 -0
  30. content_creator/providers/native_cli.py +101 -0
  31. content_creator/providers/openai.py +60 -0
  32. content_creator/providers/registry.py +43 -0
  33. content_creator/quality.py +57 -0
  34. content_creator/resource_paths.py +46 -0
  35. content_creator/resources/__init__.py +0 -0
  36. content_creator/resources/agent-templates/standard/agents/README.md +23 -0
  37. content_creator/resources/agent-templates/standard/agents/attribution-reviewer.md +8 -0
  38. content_creator/resources/agent-templates/standard/agents/briefing-agent.md +34 -0
  39. content_creator/resources/agent-templates/standard/agents/critic-learnings.md +5 -0
  40. content_creator/resources/agent-templates/standard/agents/critic.md +38 -0
  41. content_creator/resources/agent-templates/standard/agents/learning-extractor.md +86 -0
  42. content_creator/resources/agent-templates/standard/agents/perspective-extractor.md +39 -0
  43. content_creator/resources/agent-templates/standard/agents/profile-critic.md +18 -0
  44. content_creator/resources/agent-templates/standard/agents/researcher-learnings.md +5 -0
  45. content_creator/resources/agent-templates/standard/agents/researcher.md +48 -0
  46. content_creator/resources/agent-templates/standard/agents/template.json +6 -0
  47. content_creator/resources/agent-templates/standard/agents/voice-analyst.md +33 -0
  48. content_creator/resources/agent-templates/standard/agents/voice-evaluator.md +16 -0
  49. content_creator/resources/agent-templates/standard/agents/writer-learnings.md +5 -0
  50. content_creator/resources/agent-templates/standard/agents/writer.md +51 -0
  51. content_creator/resources/config/models.yaml +87 -0
  52. content_creator/resources/contracts/agent-harness.md +19 -0
  53. content_creator/resources/contracts/roles/attribution-reviewer.md +5 -0
  54. content_creator/resources/contracts/roles/briefing-agent.md +10 -0
  55. content_creator/resources/contracts/roles/critic.md +6 -0
  56. content_creator/resources/contracts/roles/learning-extractor.md +6 -0
  57. content_creator/resources/contracts/roles/perspective-extractor.md +5 -0
  58. content_creator/resources/contracts/roles/profile-critic.md +5 -0
  59. content_creator/resources/contracts/roles/researcher.md +6 -0
  60. content_creator/resources/contracts/roles/voice-analyst.md +5 -0
  61. content_creator/resources/contracts/roles/voice-evaluator.md +5 -0
  62. content_creator/resources/contracts/roles/writer.md +6 -0
  63. content_creator/resources/evals/cases/route-matrix.yaml +49 -0
  64. content_creator/resources/packs/general-text/pack.json +41 -0
  65. content_creator/resources/packs/general-text/rubric.yaml +18 -0
  66. content_creator/resources/packs/linkedin-article/pack.json +11 -0
  67. content_creator/resources/packs/linkedin-article/rubric.yaml +16 -0
  68. content_creator/resources/packs/linkedin-post/pack.json +11 -0
  69. content_creator/resources/packs/linkedin-post/rubric.yaml +16 -0
  70. content_creator/resources/profiles/default/learnings/memory.json +4 -0
  71. content_creator/resources/profiles/default/voice.md +13 -0
  72. content_creator/resources/profiles/starter/clear-professional.md +23 -0
  73. content_creator/resources/rubrics/core.yaml +35 -0
  74. content_creator/resources/rubrics/research-deep.yaml +15 -0
  75. content_creator/resources/rubrics/research-light.yaml +12 -0
  76. content_creator/resources/rubrics/research-none.yaml +12 -0
  77. content_creator/resources/skills/content-creator/SKILL.md +117 -0
  78. content_creator/resources/skills/content-creator/agents/openai.yaml +4 -0
  79. content_creator/resources/skills/voice-builder/SKILL.md +95 -0
  80. content_creator/resources/skills/voice-builder/agents/openai.yaml +4 -0
  81. content_creator/routing.py +52 -0
  82. content_creator/runner.py +73 -0
  83. content_creator/storage.py +86 -0
  84. content_creator/upgrade.py +225 -0
  85. content_creator/validation.py +69 -0
  86. content_creator/version.py +1 -0
  87. content_creator/voice_builder.py +691 -0
  88. content_creator/voice_evaluation.py +44 -0
  89. content_creator/voices.py +618 -0
  90. content_creator/workspace.py +759 -0
  91. content_creator-0.6.0.dist-info/METADATA +181 -0
  92. content_creator-0.6.0.dist-info/RECORD +97 -0
  93. content_creator-0.6.0.dist-info/WHEEL +5 -0
  94. content_creator-0.6.0.dist-info/entry_points.txt +2 -0
  95. content_creator-0.6.0.dist-info/licenses/LICENSE.md +661 -0
  96. content_creator-0.6.0.dist-info/licenses/NOTICE +14 -0
  97. content_creator-0.6.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,9 @@
1
+ """Provider-neutral content creation workflow."""
2
+
3
+ from .domain import WorkOrder
4
+ from .orchestrator import Orchestrator
5
+ from .version import VERSION
6
+
7
+ __version__ = VERSION
8
+
9
+ __all__ = ["Orchestrator", "VERSION", "WorkOrder", "__version__"]
@@ -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