simit 0.1.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.
simit/skills/base.py ADDED
@@ -0,0 +1,172 @@
1
+ """The ``Skill`` interface: how a textual image description becomes an image.
2
+
3
+ A skill turns a description into a compact specification written by the
4
+ model (``prompt`` -> model -> ``parse``), then draws it deterministically
5
+ (``render``). Parser/renderer errors are fed back to the model for repair.
6
+ Subclass ``Skill`` to add a new visual domain::
7
+
8
+ class SheetMusic(simit.Skill):
9
+ name = "sheet_music"
10
+ route_hint = "musical notation: staves, notes, chords"
11
+ examples = [("Draw a C major scale", "NOTES: C4 D4 E4 F4 G4 A4 B4 C5")]
12
+ def parse(self, text): ... # raise SpecError(msg); msg is fed back
13
+ def render(self, spec): ... # -> PIL.Image
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from dataclasses import dataclass, field
20
+ from typing import TYPE_CHECKING, Any, Optional, Sequence
21
+
22
+ from PIL import Image
23
+
24
+ from ..utils import clean_response
25
+
26
+ if TYPE_CHECKING:
27
+ from .context import SkillContext
28
+
29
+
30
+ class SpecError(ValueError):
31
+ """Raise from ``Skill.parse``/``Skill.render`` when a specification is
32
+ unusable. The message is shown to the model in the repair prompt, so make
33
+ it specific ("no NODE: lines found", "undefined point 'C'")."""
34
+
35
+
36
+ @dataclass
37
+ class Realization:
38
+ image: Image.Image
39
+ skill: str
40
+ spec_text: str = "" # the model output the image was rendered from
41
+ attempts: int = 1
42
+ extra: dict = field(default_factory=dict)
43
+
44
+
45
+ # Completion-style few-shot framing ("Request: ... / Specification:"): models
46
+ # follow a custom format far more reliably when the prompt ends where the spec
47
+ # starts (BAGEL: 15/15 parseable vs 8/15 with a trailing instruction).
48
+ _DEFAULT_PROMPT = """You are a precise {title} specification writer. Given a description of \
49
+ {hint}, you output ONLY its specification, in exactly the format shown in the examples below \
50
+ -- nothing else (no explanations, no prose).
51
+
52
+ {examples}{note}Request: {{request}}
53
+ Specification:
54
+ """
55
+
56
+ _REPAIR_NOTE = """A previous specification for the next request could not be used ({reason}):
57
+ {previous}
58
+
59
+ Write a corrected one.
60
+
61
+ """
62
+
63
+ _CUSTOM_PROMPT_REPAIR = """{prompt}
64
+
65
+ Your previous answer could not be used ({reason}). It was:
66
+ {previous}
67
+
68
+ Output ONLY a corrected specification, nothing else."""
69
+
70
+
71
+ class Skill:
72
+ """Base class for a rendering skill. See the module docstring."""
73
+
74
+ #: Unique identifier; the router answers with ``name.upper()``.
75
+ name: str = ""
76
+ #: Category definition shown to the router ("when should I be used?").
77
+ route_hint: str = ""
78
+ #: Example requests that belong to this skill (router few-shots).
79
+ #: Defaults to the requests in ``examples``.
80
+ route_examples: Sequence[str] = ()
81
+ #: ``(request, spec_text)`` few-shot pairs for the auto-built spec prompt.
82
+ examples: Sequence[tuple[str, str]] = ()
83
+ #: Full spec-prompt template containing ``{request}``; auto-built from
84
+ #: ``examples`` when left as ``None``.
85
+ prompt: Optional[str] = None
86
+ max_new_tokens: int = 512
87
+ #: Decoding temperature of the first attempt and of repair attempts
88
+ #: (greedy repairs on a near-identical context repeat the same mistake).
89
+ first_temperature: float = 0.0
90
+ retry_temperature: float = 0.5
91
+ #: Critic rubric: "strict" (every detail must match, for photos) or
92
+ #: "structured" (right kind of visual, readable, ignore exact values).
93
+ verify_rubric: str = "structured"
94
+ #: Render in a worker process (True) or a thread of the main process.
95
+ #: Process rendering needs the skill to be importable/picklable.
96
+ render_in_subprocess: bool = True
97
+
98
+ # ------------------------------------------------------------------ prompts
99
+ @property
100
+ def label(self) -> str:
101
+ return self.name.upper()
102
+
103
+ def build_prompt(self, request: str) -> str:
104
+ template = self.prompt if self.prompt is not None else self._default_prompt()
105
+ return template.format(request=request)
106
+
107
+ def build_repair_prompt(self, request: str, previous: str, reason: str) -> str:
108
+ if self.prompt is not None:
109
+ return _CUSTOM_PROMPT_REPAIR.format(prompt=self.build_prompt(request), previous=previous, reason=reason)
110
+ note = _REPAIR_NOTE.format(reason=reason, previous=previous.strip())
111
+ return self._default_prompt(note).format(request=request)
112
+
113
+ def _default_prompt(self, note: str = "") -> str:
114
+ blocks = [f"Request: {req}\nSpecification:\n{spec.strip()}\n\n" for req, spec in self.examples]
115
+ title = self.name.replace("_", " ")
116
+ hint = self.route_hint.strip() or f"a {title} image"
117
+
118
+ def esc(text): # user/model text must survive the final .format(request=...)
119
+ return text.replace("{", "{{").replace("}", "}}")
120
+ return _DEFAULT_PROMPT.format(title=title, hint=esc(hint), examples=esc("".join(blocks)), note=esc(note))
121
+
122
+ def get_route_examples(self) -> list[str]:
123
+ return list(self.route_examples) or [req for req, _ in self.examples]
124
+
125
+ # ------------------------------------------------------------- spec -> image
126
+ def shortcut(self, request: str) -> Any:
127
+ """Return a ready spec for ``request`` to skip the model, or None."""
128
+ return None
129
+
130
+ def clean(self, text: str) -> str:
131
+ return clean_response(text).strip()
132
+
133
+ def parse(self, text: str) -> Any:
134
+ raise NotImplementedError
135
+
136
+ def render(self, spec: Any) -> Image.Image:
137
+ raise NotImplementedError
138
+
139
+ # ------------------------------------------------------------- realization
140
+ async def realize(self, ctx: "SkillContext", request: str, retries: int = 2) -> Optional[Realization]:
141
+ """prompt -> model -> parse -> render, repairing on errors. Returns
142
+ None when every attempt failed."""
143
+ shortcut = self.shortcut(request)
144
+ if shortcut is not None:
145
+ try:
146
+ image = await ctx.render(self, shortcut)
147
+ return Realization(image, self.name, spec_text=str(shortcut), attempts=0)
148
+ except SpecError:
149
+ pass
150
+ prompt = self.build_prompt(request)
151
+ for attempt in range(retries + 1):
152
+ temperature = self.first_temperature if attempt == 0 else self.retry_temperature
153
+ text = self.clean(await ctx.generate(prompt, max_new_tokens=self.max_new_tokens,
154
+ temperature=temperature))
155
+ try:
156
+ spec = self.parse(text)
157
+ image = await ctx.render(self, spec)
158
+ except SpecError as e:
159
+ ctx.log(f"[{self.name}] attempt {attempt}: {e}")
160
+ prompt = self.build_repair_prompt(request, text, str(e))
161
+ continue
162
+ return Realization(image, self.name, spec_text=text, attempts=attempt + 1)
163
+ return None
164
+
165
+
166
+ _FENCE_RE = re.compile(r"```(?:[\w-]+)?\s*\n(.*?)```", re.DOTALL)
167
+
168
+
169
+ def extract_fenced_block(text: str) -> Optional[str]:
170
+ """Content of the first fenced code block, or None."""
171
+ m = _FENCE_RE.search(text)
172
+ return m.group(1).strip() if m else None