authoringfw 0.1.0__tar.gz
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.
- authoringfw-0.1.0/.github/workflows/publish.yml +38 -0
- authoringfw-0.1.0/.gitignore +11 -0
- authoringfw-0.1.0/CHANGELOG.md +19 -0
- authoringfw-0.1.0/PKG-INFO +103 -0
- authoringfw-0.1.0/README.md +76 -0
- authoringfw-0.1.0/pyproject.toml +55 -0
- authoringfw-0.1.0/src/authoringfw/__init__.py +24 -0
- authoringfw-0.1.0/src/authoringfw/adapters/__init__.py +0 -0
- authoringfw-0.1.0/src/authoringfw/adapters/interfaces.py +38 -0
- authoringfw-0.1.0/src/authoringfw/formats/__init__.py +0 -0
- authoringfw-0.1.0/src/authoringfw/formats/base.py +90 -0
- authoringfw-0.1.0/src/authoringfw/schema/__init__.py +0 -0
- authoringfw-0.1.0/src/authoringfw/schema/character.py +31 -0
- authoringfw-0.1.0/src/authoringfw/schema/style.py +30 -0
- authoringfw-0.1.0/src/authoringfw/schema/versioning.py +55 -0
- authoringfw-0.1.0/src/authoringfw/schema/world.py +42 -0
- authoringfw-0.1.0/tests/__init__.py +0 -0
- authoringfw-0.1.0/tests/test_schema.py +99 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.11"
|
|
17
|
+
- name: Install and test
|
|
18
|
+
run: |
|
|
19
|
+
pip install -e ".[dev]"
|
|
20
|
+
pytest tests/ -v
|
|
21
|
+
|
|
22
|
+
publish:
|
|
23
|
+
needs: test
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
environment: pypi
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- uses: actions/setup-python@v5
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.11"
|
|
33
|
+
- name: Build
|
|
34
|
+
run: |
|
|
35
|
+
pip install hatch
|
|
36
|
+
hatch build
|
|
37
|
+
- name: Publish to PyPI
|
|
38
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog — authoringfw
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.1.0] — 2026-02-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Initial release
|
|
9
|
+
- `StyleProfile` — tone, POV, tense, vocabulary, sentence rhythm + `to_constraints()`
|
|
10
|
+
- `CharacterProfile` — name, role, traits, backstory, arc, relationships + `to_context_string()`
|
|
11
|
+
- `WorldContext` + `Location` — world rules, locations, lore + `to_context_string()`
|
|
12
|
+
- `VersionMetadata` — immutable content snapshot with SHA-256 hash, semver, LLM metadata
|
|
13
|
+
- `PhaseSnapshot` — project state at workflow phase boundary
|
|
14
|
+
- `ChangeType` enum (AI_GENERATED, HUMAN_EDITED, MERGED, REVERTED)
|
|
15
|
+
- `FormatProfile` — novel, essay, series, scientific with workflow phases and style constraints
|
|
16
|
+
- `WorkflowPhase` enum (IDEATION → PRODUCTION)
|
|
17
|
+
- `StepConfig` — per-step template and parameter config
|
|
18
|
+
- `get_format()` — lookup built-in format profiles
|
|
19
|
+
- `IStyleAdapter`, `ICharacterAdapter`, `IWorldAdapter` Protocol interfaces (`@runtime_checkable`)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authoringfw
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Authoring Framework — domain schemas for AI-assisted creative writing applications
|
|
5
|
+
Project-URL: Homepage, https://github.com/achimdehnert/platform
|
|
6
|
+
Project-URL: Repository, https://github.com/achimdehnert/platform/tree/main/packages/authoringfw
|
|
7
|
+
Author-email: Achim Dehnert <achim@dehnert.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ai,authoring,creative-writing,llm,pydantic,schema,story
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: pydantic>=2.6
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest-mock>=3.12; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
23
|
+
Provides-Extra: testing
|
|
24
|
+
Requires-Dist: pytest-mock>=3.12; extra == 'testing'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'testing'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# authoringfw — Authoring Framework
|
|
29
|
+
|
|
30
|
+
Domain schemas for AI-assisted creative writing applications.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install authoringfw
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from authoringfw import StyleProfile, CharacterProfile, WorldContext, get_format
|
|
42
|
+
|
|
43
|
+
# Style constraints for prompt injection
|
|
44
|
+
style = StyleProfile(tone="melancholic", pov="third_limited", tense="past")
|
|
45
|
+
constraints = style.to_constraints()
|
|
46
|
+
|
|
47
|
+
# Character context
|
|
48
|
+
alice = CharacterProfile(
|
|
49
|
+
name="Alice",
|
|
50
|
+
role="protagonist",
|
|
51
|
+
personality_traits=["brave", "curious"],
|
|
52
|
+
arc="From fear to courage",
|
|
53
|
+
)
|
|
54
|
+
print(alice.to_context_string())
|
|
55
|
+
|
|
56
|
+
# World context
|
|
57
|
+
world = WorldContext(
|
|
58
|
+
title="The Shattered Realms",
|
|
59
|
+
genre="fantasy",
|
|
60
|
+
world_rules=["Magic costs life force", "Dragons are extinct"],
|
|
61
|
+
)
|
|
62
|
+
print(world.to_context_string())
|
|
63
|
+
|
|
64
|
+
# Format profiles (novel, essay, series, scientific)
|
|
65
|
+
roman = get_format("roman")
|
|
66
|
+
print(roman.style_constraints)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Schemas
|
|
70
|
+
|
|
71
|
+
- **`StyleProfile`** — tone, POV, tense, vocabulary, sentence rhythm
|
|
72
|
+
- **`CharacterProfile`** — name, role, traits, backstory, arc, relationships
|
|
73
|
+
- **`WorldContext`** — title, genre, setting, world rules, locations, lore
|
|
74
|
+
- **`VersionMetadata`** — immutable content snapshot with hash, semver, LLM metadata
|
|
75
|
+
- **`PhaseSnapshot`** — project state at a workflow phase boundary
|
|
76
|
+
|
|
77
|
+
## Format Profiles
|
|
78
|
+
|
|
79
|
+
Built-in formats: `roman`, `essay`, `serie`, `scientific`
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from authoringfw.formats.base import get_format, WorkflowPhase
|
|
83
|
+
|
|
84
|
+
novel = get_format("roman")
|
|
85
|
+
outline_steps = novel.steps_for_phase(WorkflowPhase.OUTLINE)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Adapter Interfaces
|
|
89
|
+
|
|
90
|
+
Protocol-based adapters — no inheritance required:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from authoringfw.adapters.interfaces import IStyleAdapter
|
|
94
|
+
|
|
95
|
+
class MyStyleAdapter:
|
|
96
|
+
async def get_profile(self, style_id): ...
|
|
97
|
+
async def analyze_text(self, text): ...
|
|
98
|
+
def generate_style_constraints(self, profile): ...
|
|
99
|
+
async def score_conformity(self, text, profile): ...
|
|
100
|
+
|
|
101
|
+
adapter = MyStyleAdapter()
|
|
102
|
+
assert isinstance(adapter, IStyleAdapter) # True via @runtime_checkable
|
|
103
|
+
```
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# authoringfw — Authoring Framework
|
|
2
|
+
|
|
3
|
+
Domain schemas for AI-assisted creative writing applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install authoringfw
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from authoringfw import StyleProfile, CharacterProfile, WorldContext, get_format
|
|
15
|
+
|
|
16
|
+
# Style constraints for prompt injection
|
|
17
|
+
style = StyleProfile(tone="melancholic", pov="third_limited", tense="past")
|
|
18
|
+
constraints = style.to_constraints()
|
|
19
|
+
|
|
20
|
+
# Character context
|
|
21
|
+
alice = CharacterProfile(
|
|
22
|
+
name="Alice",
|
|
23
|
+
role="protagonist",
|
|
24
|
+
personality_traits=["brave", "curious"],
|
|
25
|
+
arc="From fear to courage",
|
|
26
|
+
)
|
|
27
|
+
print(alice.to_context_string())
|
|
28
|
+
|
|
29
|
+
# World context
|
|
30
|
+
world = WorldContext(
|
|
31
|
+
title="The Shattered Realms",
|
|
32
|
+
genre="fantasy",
|
|
33
|
+
world_rules=["Magic costs life force", "Dragons are extinct"],
|
|
34
|
+
)
|
|
35
|
+
print(world.to_context_string())
|
|
36
|
+
|
|
37
|
+
# Format profiles (novel, essay, series, scientific)
|
|
38
|
+
roman = get_format("roman")
|
|
39
|
+
print(roman.style_constraints)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Schemas
|
|
43
|
+
|
|
44
|
+
- **`StyleProfile`** — tone, POV, tense, vocabulary, sentence rhythm
|
|
45
|
+
- **`CharacterProfile`** — name, role, traits, backstory, arc, relationships
|
|
46
|
+
- **`WorldContext`** — title, genre, setting, world rules, locations, lore
|
|
47
|
+
- **`VersionMetadata`** — immutable content snapshot with hash, semver, LLM metadata
|
|
48
|
+
- **`PhaseSnapshot`** — project state at a workflow phase boundary
|
|
49
|
+
|
|
50
|
+
## Format Profiles
|
|
51
|
+
|
|
52
|
+
Built-in formats: `roman`, `essay`, `serie`, `scientific`
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from authoringfw.formats.base import get_format, WorkflowPhase
|
|
56
|
+
|
|
57
|
+
novel = get_format("roman")
|
|
58
|
+
outline_steps = novel.steps_for_phase(WorkflowPhase.OUTLINE)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Adapter Interfaces
|
|
62
|
+
|
|
63
|
+
Protocol-based adapters — no inheritance required:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from authoringfw.adapters.interfaces import IStyleAdapter
|
|
67
|
+
|
|
68
|
+
class MyStyleAdapter:
|
|
69
|
+
async def get_profile(self, style_id): ...
|
|
70
|
+
async def analyze_text(self, text): ...
|
|
71
|
+
def generate_style_constraints(self, profile): ...
|
|
72
|
+
async def score_conformity(self, text, profile): ...
|
|
73
|
+
|
|
74
|
+
adapter = MyStyleAdapter()
|
|
75
|
+
assert isinstance(adapter, IStyleAdapter) # True via @runtime_checkable
|
|
76
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "authoringfw"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Authoring Framework — domain schemas for AI-assisted creative writing applications"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Achim Dehnert", email = "achim@dehnert.com"}
|
|
10
|
+
]
|
|
11
|
+
keywords = ["llm", "authoring", "creative-writing", "ai", "story", "schema", "pydantic"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
"Topic :: Text Processing :: Linguistic",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pydantic>=2.6",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=8.0",
|
|
30
|
+
"pytest-mock>=3.12",
|
|
31
|
+
]
|
|
32
|
+
testing = [
|
|
33
|
+
"pytest>=8.0",
|
|
34
|
+
"pytest-mock>=3.12",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/achimdehnert/platform"
|
|
39
|
+
Repository = "https://github.com/achimdehnert/platform/tree/main/packages/authoringfw"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/authoringfw"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
python_files = ["test_*.py"]
|
|
50
|
+
pythonpath = ["."]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 100
|
|
55
|
+
target-version = "py311"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
authoringfw — Authoring Framework
|
|
3
|
+
|
|
4
|
+
Domain schemas for AI-assisted creative writing applications.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from authoringfw.schema.style import StyleProfile
|
|
10
|
+
from authoringfw.schema.character import CharacterProfile
|
|
11
|
+
from authoringfw.schema.world import WorldContext
|
|
12
|
+
from authoringfw.schema.versioning import VersionMetadata, ChangeType
|
|
13
|
+
from authoringfw.formats.base import FormatProfile, WorkflowPhase
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"StyleProfile",
|
|
17
|
+
"CharacterProfile",
|
|
18
|
+
"WorldContext",
|
|
19
|
+
"VersionMetadata",
|
|
20
|
+
"ChangeType",
|
|
21
|
+
"FormatProfile",
|
|
22
|
+
"WorkflowPhase",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Protocol-based adapter interfaces for authoringfw.
|
|
2
|
+
|
|
3
|
+
Implementations use duck typing — no inheritance required.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from authoringfw.schema.character import CharacterProfile
|
|
11
|
+
from authoringfw.schema.style import StyleProfile
|
|
12
|
+
from authoringfw.schema.world import WorldContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class IStyleAdapter(Protocol):
|
|
17
|
+
"""Adapter for reading and scoring style profiles."""
|
|
18
|
+
|
|
19
|
+
async def get_profile(self, style_id: str) -> StyleProfile: ...
|
|
20
|
+
async def analyze_text(self, text: str) -> StyleProfile: ...
|
|
21
|
+
def generate_style_constraints(self, profile: StyleProfile) -> list[str]: ...
|
|
22
|
+
async def score_conformity(self, text: str, profile: StyleProfile) -> float: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@runtime_checkable
|
|
26
|
+
class ICharacterAdapter(Protocol):
|
|
27
|
+
"""Adapter for reading character profiles."""
|
|
28
|
+
|
|
29
|
+
async def get_character(self, character_id: str) -> CharacterProfile: ...
|
|
30
|
+
async def list_characters(self, project_id: str) -> list[CharacterProfile]: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@runtime_checkable
|
|
34
|
+
class IWorldAdapter(Protocol):
|
|
35
|
+
"""Adapter for reading world context."""
|
|
36
|
+
|
|
37
|
+
async def get_world(self, world_id: str) -> WorldContext: ...
|
|
38
|
+
async def list_locations(self, world_id: str) -> list[str]: ...
|
|
File without changes
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Base format profile and workflow phase definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WorkflowPhase(str, Enum):
|
|
12
|
+
IDEATION = "ideation"
|
|
13
|
+
CONCEPT = "concept"
|
|
14
|
+
OUTLINE = "outline"
|
|
15
|
+
FIRST_DRAFT = "first_draft"
|
|
16
|
+
REVISION = "revision"
|
|
17
|
+
CONSISTENCY = "consistency"
|
|
18
|
+
PRODUCTION = "production"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StepConfig(BaseModel):
|
|
22
|
+
"""Configuration for a single workflow step."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
phase: WorkflowPhase
|
|
26
|
+
prompt_template_id: str
|
|
27
|
+
max_tokens: int = 2000
|
|
28
|
+
temperature: float = 0.7
|
|
29
|
+
requires_human_approval: bool = False
|
|
30
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FormatProfile(BaseModel):
|
|
34
|
+
"""Defines the writing format and its workflow phases."""
|
|
35
|
+
|
|
36
|
+
format_type: str
|
|
37
|
+
display_name: str
|
|
38
|
+
description: str = ""
|
|
39
|
+
phases: list[WorkflowPhase] = Field(
|
|
40
|
+
default_factory=lambda: list(WorkflowPhase)
|
|
41
|
+
)
|
|
42
|
+
steps: list[StepConfig] = Field(default_factory=list)
|
|
43
|
+
style_constraints: list[str] = Field(default_factory=list)
|
|
44
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
def steps_for_phase(self, phase: WorkflowPhase) -> list[StepConfig]:
|
|
47
|
+
return [s for s in self.steps if s.phase == phase]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
ROMAN = FormatProfile(
|
|
51
|
+
format_type="roman",
|
|
52
|
+
display_name="Novel",
|
|
53
|
+
description="Long-form fiction with chapters, characters, and story arcs.",
|
|
54
|
+
style_constraints=["Show don't tell", "Consistent POV", "Scene & sequel structure"],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
ESSAY = FormatProfile(
|
|
58
|
+
format_type="essay",
|
|
59
|
+
display_name="Essay",
|
|
60
|
+
description="Argumentative or descriptive non-fiction prose.",
|
|
61
|
+
style_constraints=["Clear thesis", "Logical argumentation", "Evidence-based claims"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
SERIE = FormatProfile(
|
|
65
|
+
format_type="serie",
|
|
66
|
+
display_name="Series",
|
|
67
|
+
description="Multi-volume fiction with shared universe and recurring characters.",
|
|
68
|
+
style_constraints=["Series-wide consistency", "Character continuity", "World-building coherence"],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
SCIENTIFIC = FormatProfile(
|
|
72
|
+
format_type="scientific",
|
|
73
|
+
display_name="Scientific Paper",
|
|
74
|
+
description="Academic writing with citations, methodology, and structured sections.",
|
|
75
|
+
style_constraints=["Objective tone", "Precise terminology", "Citation required"],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
FORMAT_REGISTRY: dict[str, FormatProfile] = {
|
|
79
|
+
"roman": ROMAN,
|
|
80
|
+
"essay": ESSAY,
|
|
81
|
+
"serie": SERIE,
|
|
82
|
+
"scientific": SCIENTIFIC,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_format(format_type: str) -> FormatProfile:
|
|
87
|
+
"""Get a built-in format profile by type string."""
|
|
88
|
+
if format_type not in FORMAT_REGISTRY:
|
|
89
|
+
raise KeyError(f"Unknown format type '{format_type}'. Available: {list(FORMAT_REGISTRY)}")
|
|
90
|
+
return FORMAT_REGISTRY[format_type]
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Character profile schema for AI-assisted authoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CharacterProfile(BaseModel):
|
|
11
|
+
"""Represents a character in a story universe."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
role: str = "supporting"
|
|
15
|
+
description: str = ""
|
|
16
|
+
personality_traits: list[str] = Field(default_factory=list)
|
|
17
|
+
backstory: str = ""
|
|
18
|
+
relationships: dict[str, str] = Field(default_factory=dict)
|
|
19
|
+
arc: str = ""
|
|
20
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
def to_context_string(self) -> str:
|
|
23
|
+
"""Format character for prompt injection."""
|
|
24
|
+
lines = [f"**{self.name}** ({self.role})"]
|
|
25
|
+
if self.description:
|
|
26
|
+
lines.append(self.description)
|
|
27
|
+
if self.personality_traits:
|
|
28
|
+
lines.append(f"Traits: {', '.join(self.personality_traits)}")
|
|
29
|
+
if self.arc:
|
|
30
|
+
lines.append(f"Arc: {self.arc}")
|
|
31
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Style profile schema for AI-assisted authoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StyleProfile(BaseModel):
|
|
11
|
+
"""Defines the stylistic characteristics of a writing project."""
|
|
12
|
+
|
|
13
|
+
tone: str = "neutral"
|
|
14
|
+
pov: str = "third_limited"
|
|
15
|
+
tense: str = "past"
|
|
16
|
+
vocabulary_level: str = "medium"
|
|
17
|
+
sentence_rhythm: str = "varied"
|
|
18
|
+
author_signature: dict[str, Any] = Field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
model_config = {"frozen": False}
|
|
21
|
+
|
|
22
|
+
def to_constraints(self) -> list[str]:
|
|
23
|
+
"""Return a list of natural-language style constraints for prompt injection."""
|
|
24
|
+
return [
|
|
25
|
+
f"Tone: {self.tone}",
|
|
26
|
+
f"Point of view: {self.pov}",
|
|
27
|
+
f"Tense: {self.tense}",
|
|
28
|
+
f"Vocabulary level: {self.vocabulary_level}",
|
|
29
|
+
f"Sentence rhythm: {self.sentence_rhythm}",
|
|
30
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Versioning schema for AI-generated content snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChangeType(str, Enum):
|
|
14
|
+
AI_GENERATED = "ai_generated"
|
|
15
|
+
HUMAN_EDITED = "human_edited"
|
|
16
|
+
MERGED = "merged"
|
|
17
|
+
REVERTED = "reverted"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VersionMetadata(BaseModel):
|
|
21
|
+
"""Immutable metadata snapshot for a content version."""
|
|
22
|
+
|
|
23
|
+
version_id: str
|
|
24
|
+
semver: str = "1.0.0"
|
|
25
|
+
phase: str = ""
|
|
26
|
+
node_id: str = ""
|
|
27
|
+
change_type: ChangeType = ChangeType.AI_GENERATED
|
|
28
|
+
author: str = ""
|
|
29
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
30
|
+
parent_version_id: str | None = None
|
|
31
|
+
content_hash: str = ""
|
|
32
|
+
prompt_template_id: str = ""
|
|
33
|
+
prompt_template_version: str = ""
|
|
34
|
+
llm_model: str = ""
|
|
35
|
+
llm_temperature: float = 0.7
|
|
36
|
+
quality_scores: dict[str, Any] = Field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
model_config = {"frozen": True}
|
|
39
|
+
|
|
40
|
+
def compute_hash(self, content: str) -> "VersionMetadata":
|
|
41
|
+
"""Return a new instance with content_hash filled in."""
|
|
42
|
+
h = hashlib.sha256(content.encode()).hexdigest()[:16]
|
|
43
|
+
return self.model_copy(update={"content_hash": h})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PhaseSnapshot(BaseModel):
|
|
47
|
+
"""A complete snapshot of a project at a workflow phase boundary."""
|
|
48
|
+
|
|
49
|
+
snapshot_id: str
|
|
50
|
+
phase: str
|
|
51
|
+
project_id: str
|
|
52
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
53
|
+
node_versions: dict[str, str] = Field(default_factory=dict)
|
|
54
|
+
approved_by: str = ""
|
|
55
|
+
notes: str = ""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""World context schema for AI-assisted authoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Location(BaseModel):
|
|
11
|
+
"""A location within a story world."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
description: str = ""
|
|
15
|
+
atmosphere: str = ""
|
|
16
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorldContext(BaseModel):
|
|
20
|
+
"""The story universe — locations, lore, rules, and time period."""
|
|
21
|
+
|
|
22
|
+
title: str
|
|
23
|
+
genre: str = ""
|
|
24
|
+
setting: str = ""
|
|
25
|
+
time_period: str = ""
|
|
26
|
+
world_rules: list[str] = Field(default_factory=list)
|
|
27
|
+
locations: list[Location] = Field(default_factory=list)
|
|
28
|
+
lore: str = ""
|
|
29
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
def to_context_string(self) -> str:
|
|
32
|
+
"""Format world context for prompt injection."""
|
|
33
|
+
lines = [f"World: {self.title}"]
|
|
34
|
+
if self.genre:
|
|
35
|
+
lines.append(f"Genre: {self.genre}")
|
|
36
|
+
if self.setting:
|
|
37
|
+
lines.append(f"Setting: {self.setting}")
|
|
38
|
+
if self.time_period:
|
|
39
|
+
lines.append(f"Time period: {self.time_period}")
|
|
40
|
+
if self.world_rules:
|
|
41
|
+
lines.append("Rules: " + "; ".join(self.world_rules))
|
|
42
|
+
return "\n".join(lines)
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Tests for authoringfw schema models."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
|
|
6
|
+
from authoringfw.schema.style import StyleProfile
|
|
7
|
+
from authoringfw.schema.character import CharacterProfile
|
|
8
|
+
from authoringfw.schema.world import WorldContext, Location
|
|
9
|
+
from authoringfw.schema.versioning import VersionMetadata, ChangeType, PhaseSnapshot
|
|
10
|
+
from authoringfw.formats.base import FormatProfile, WorkflowPhase, get_format
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_style_profile_defaults():
|
|
14
|
+
s = StyleProfile()
|
|
15
|
+
assert s.tone == "neutral"
|
|
16
|
+
assert s.pov == "third_limited"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_style_profile_to_constraints():
|
|
20
|
+
s = StyleProfile(tone="dark", pov="first_person", tense="present")
|
|
21
|
+
constraints = s.to_constraints()
|
|
22
|
+
assert any("dark" in c for c in constraints)
|
|
23
|
+
assert any("first_person" in c for c in constraints)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_character_profile_context_string():
|
|
27
|
+
c = CharacterProfile(
|
|
28
|
+
name="Alice",
|
|
29
|
+
role="protagonist",
|
|
30
|
+
personality_traits=["brave", "curious"],
|
|
31
|
+
arc="From fear to courage",
|
|
32
|
+
)
|
|
33
|
+
ctx = c.to_context_string()
|
|
34
|
+
assert "Alice" in ctx
|
|
35
|
+
assert "brave" in ctx
|
|
36
|
+
assert "courage" in ctx
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_world_context_to_string():
|
|
40
|
+
w = WorldContext(
|
|
41
|
+
title="Middle Earth",
|
|
42
|
+
genre="fantasy",
|
|
43
|
+
setting="medieval",
|
|
44
|
+
world_rules=["Magic exists", "Dragons are rare"],
|
|
45
|
+
)
|
|
46
|
+
ctx = w.to_context_string()
|
|
47
|
+
assert "Middle Earth" in ctx
|
|
48
|
+
assert "fantasy" in ctx
|
|
49
|
+
assert "Magic exists" in ctx
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_version_metadata_frozen():
|
|
53
|
+
v = VersionMetadata(version_id="v1", semver="1.0.0")
|
|
54
|
+
with pytest.raises(Exception):
|
|
55
|
+
v.version_id = "v2"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_version_metadata_compute_hash():
|
|
59
|
+
v = VersionMetadata(version_id="v1")
|
|
60
|
+
v2 = v.compute_hash("Hello world content")
|
|
61
|
+
assert v2.content_hash != ""
|
|
62
|
+
assert len(v2.content_hash) == 16
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_change_type_values():
|
|
66
|
+
assert ChangeType.AI_GENERATED == "ai_generated"
|
|
67
|
+
assert ChangeType.HUMAN_EDITED == "human_edited"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_get_format_roman():
|
|
71
|
+
f = get_format("roman")
|
|
72
|
+
assert f.format_type == "roman"
|
|
73
|
+
assert len(f.style_constraints) > 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_get_format_unknown():
|
|
77
|
+
with pytest.raises(KeyError) as exc:
|
|
78
|
+
get_format("nonexistent")
|
|
79
|
+
assert "nonexistent" in str(exc.value)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_workflow_phase_values():
|
|
83
|
+
assert WorkflowPhase.FIRST_DRAFT == "first_draft"
|
|
84
|
+
assert WorkflowPhase.PRODUCTION == "production"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_format_profile_steps_for_phase():
|
|
88
|
+
from authoringfw.formats.base import StepConfig
|
|
89
|
+
f = FormatProfile(
|
|
90
|
+
format_type="test",
|
|
91
|
+
display_name="Test",
|
|
92
|
+
steps=[
|
|
93
|
+
StepConfig(name="s1", phase=WorkflowPhase.OUTLINE, prompt_template_id="t1"),
|
|
94
|
+
StepConfig(name="s2", phase=WorkflowPhase.FIRST_DRAFT, prompt_template_id="t2"),
|
|
95
|
+
],
|
|
96
|
+
)
|
|
97
|
+
outline_steps = f.steps_for_phase(WorkflowPhase.OUTLINE)
|
|
98
|
+
assert len(outline_steps) == 1
|
|
99
|
+
assert outline_steps[0].name == "s1"
|