synkro 0.4.5__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.
Potentially problematic release.
This version of synkro might be problematic. Click here for more details.
- synkro/__init__.py +165 -0
- synkro/cli.py +120 -0
- synkro/core/__init__.py +7 -0
- synkro/core/dataset.py +233 -0
- synkro/core/policy.py +337 -0
- synkro/errors.py +178 -0
- synkro/examples/__init__.py +148 -0
- synkro/factory.py +160 -0
- synkro/formatters/__init__.py +12 -0
- synkro/formatters/qa.py +85 -0
- synkro/formatters/sft.py +90 -0
- synkro/formatters/tool_call.py +127 -0
- synkro/generation/__init__.py +9 -0
- synkro/generation/generator.py +163 -0
- synkro/generation/planner.py +87 -0
- synkro/generation/responses.py +160 -0
- synkro/generation/scenarios.py +90 -0
- synkro/generation/tool_responses.py +370 -0
- synkro/generation/tool_simulator.py +114 -0
- synkro/llm/__init__.py +7 -0
- synkro/llm/client.py +235 -0
- synkro/llm/rate_limits.py +95 -0
- synkro/models/__init__.py +43 -0
- synkro/models/anthropic.py +26 -0
- synkro/models/google.py +19 -0
- synkro/models/openai.py +31 -0
- synkro/modes/__init__.py +15 -0
- synkro/modes/config.py +66 -0
- synkro/modes/qa.py +18 -0
- synkro/modes/sft.py +18 -0
- synkro/modes/tool_call.py +18 -0
- synkro/parsers.py +442 -0
- synkro/pipeline/__init__.py +20 -0
- synkro/pipeline/phases.py +237 -0
- synkro/pipeline/runner.py +198 -0
- synkro/pipelines.py +105 -0
- synkro/prompts/__init__.py +44 -0
- synkro/prompts/base.py +167 -0
- synkro/prompts/qa_templates.py +97 -0
- synkro/prompts/templates.py +281 -0
- synkro/prompts/tool_templates.py +201 -0
- synkro/quality/__init__.py +14 -0
- synkro/quality/grader.py +130 -0
- synkro/quality/refiner.py +137 -0
- synkro/quality/tool_grader.py +126 -0
- synkro/quality/tool_refiner.py +128 -0
- synkro/reporting.py +213 -0
- synkro/schemas.py +325 -0
- synkro/types/__init__.py +41 -0
- synkro/types/core.py +113 -0
- synkro/types/dataset_type.py +30 -0
- synkro/types/tool.py +94 -0
- synkro-0.4.5.data/data/examples/__init__.py +148 -0
- synkro-0.4.5.dist-info/METADATA +221 -0
- synkro-0.4.5.dist-info/RECORD +58 -0
- synkro-0.4.5.dist-info/WHEEL +4 -0
- synkro-0.4.5.dist-info/entry_points.txt +2 -0
- synkro-0.4.5.dist-info/licenses/LICENSE +21 -0
synkro/core/policy.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Policy document handling with multi-format support."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Sequence
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from synkro.errors import FileNotFoundError as SynkroFileNotFoundError, PolicyTooShortError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MIN_POLICY_WORDS = 10 # Minimum words for meaningful generation
|
|
12
|
+
|
|
13
|
+
# Supported file extensions
|
|
14
|
+
SUPPORTED_EXTENSIONS = {".txt", ".md", ".pdf", ".docx"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Policy(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
A policy document to generate training data from.
|
|
20
|
+
|
|
21
|
+
Supports loading from multiple formats:
|
|
22
|
+
- Plain text (.txt, .md)
|
|
23
|
+
- PDF documents (.pdf) - via marker-pdf
|
|
24
|
+
- Word documents (.docx) - via mammoth
|
|
25
|
+
|
|
26
|
+
Can load from:
|
|
27
|
+
- Single file: Policy.from_file("compliance.pdf")
|
|
28
|
+
- Folder of files: Policy.from_file("policies/")
|
|
29
|
+
- Multiple files: Policy.from_files(["doc1.pdf", "doc2.docx"])
|
|
30
|
+
- URL: Policy.from_url("https://example.com/policy")
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
>>> # From text
|
|
34
|
+
>>> policy = Policy(text="All expenses over $50 require approval")
|
|
35
|
+
|
|
36
|
+
>>> # From single file
|
|
37
|
+
>>> policy = Policy.from_file("compliance.pdf")
|
|
38
|
+
|
|
39
|
+
>>> # From folder (loads all supported files)
|
|
40
|
+
>>> policy = Policy.from_file("policies/")
|
|
41
|
+
|
|
42
|
+
>>> # From multiple files
|
|
43
|
+
>>> policy = Policy.from_files(["doc1.pdf", "doc2.docx", "doc3.txt"])
|
|
44
|
+
|
|
45
|
+
>>> # From URL
|
|
46
|
+
>>> policy = Policy.from_url("https://example.com/policy")
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
text: str = Field(description="Full policy text in markdown format")
|
|
50
|
+
source: str | None = Field(default=None, description="Source file path or URL")
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_file(cls, path: str | Path) -> "Policy":
|
|
54
|
+
"""
|
|
55
|
+
Load policy from a file or folder.
|
|
56
|
+
|
|
57
|
+
Supports: .txt, .md, .pdf, .docx
|
|
58
|
+
|
|
59
|
+
If path is a directory, loads all supported files from that directory.
|
|
60
|
+
If path is a file, loads that single file.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
path: Path to the policy file or folder containing policy files
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Policy object with extracted text (combined if multiple files)
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
>>> # Single file
|
|
70
|
+
>>> policy = Policy.from_file("compliance.pdf")
|
|
71
|
+
>>> len(policy.text) > 0
|
|
72
|
+
True
|
|
73
|
+
|
|
74
|
+
>>> # Folder of documents
|
|
75
|
+
>>> policy = Policy.from_file("policies/")
|
|
76
|
+
>>> len(policy.text) > 0
|
|
77
|
+
True
|
|
78
|
+
"""
|
|
79
|
+
path = Path(path)
|
|
80
|
+
|
|
81
|
+
if not path.exists():
|
|
82
|
+
# Find similar files to suggest
|
|
83
|
+
similar = []
|
|
84
|
+
if path.parent.exists():
|
|
85
|
+
for ext in SUPPORTED_EXTENSIONS:
|
|
86
|
+
similar.extend(path.parent.glob(f"*{ext}"))
|
|
87
|
+
similar_names = [str(f.name) for f in similar[:5]]
|
|
88
|
+
raise SynkroFileNotFoundError(str(path), similar_names if similar_names else None)
|
|
89
|
+
|
|
90
|
+
# If it's a directory, load all supported files
|
|
91
|
+
if path.is_dir():
|
|
92
|
+
return cls._from_folder(path)
|
|
93
|
+
|
|
94
|
+
# Otherwise, load single file
|
|
95
|
+
return cls._load_single_file(path)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def _load_single_file(cls, path: Path) -> "Policy":
|
|
99
|
+
"""
|
|
100
|
+
Load a single policy file.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
path: Path to a single file
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Policy object with extracted text
|
|
107
|
+
"""
|
|
108
|
+
suffix = path.suffix.lower()
|
|
109
|
+
|
|
110
|
+
if suffix in (".txt", ".md"):
|
|
111
|
+
return cls(text=path.read_text(), source=str(path))
|
|
112
|
+
|
|
113
|
+
if suffix == ".pdf":
|
|
114
|
+
return cls._from_pdf(path)
|
|
115
|
+
|
|
116
|
+
if suffix == ".docx":
|
|
117
|
+
return cls._from_docx(path)
|
|
118
|
+
|
|
119
|
+
raise ValueError(f"Unsupported file type: {suffix}. Use .txt, .md, .pdf, or .docx")
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def _from_folder(cls, folder_path: Path) -> "Policy":
|
|
123
|
+
"""
|
|
124
|
+
Load all supported files from a folder.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
folder_path: Path to folder containing policy files
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Policy object with combined text from all files
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
ValueError: If no supported files found in folder
|
|
134
|
+
"""
|
|
135
|
+
# Find all supported files in folder (non-recursive)
|
|
136
|
+
files = [
|
|
137
|
+
f for f in folder_path.iterdir()
|
|
138
|
+
if f.is_file() and f.suffix.lower() in SUPPORTED_EXTENSIONS
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
if not files:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"No supported policy files found in {folder_path}. "
|
|
144
|
+
f"Supported formats: {', '.join(SUPPORTED_EXTENSIONS)}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Sort files for consistent ordering
|
|
148
|
+
files.sort(key=lambda f: f.name)
|
|
149
|
+
|
|
150
|
+
# Load each file and combine
|
|
151
|
+
return cls.from_files(files, source_prefix=str(folder_path))
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_files(
|
|
155
|
+
cls,
|
|
156
|
+
paths: Sequence[str | Path],
|
|
157
|
+
separator: str = "\n\n---\n\n",
|
|
158
|
+
source_prefix: str | None = None,
|
|
159
|
+
) -> "Policy":
|
|
160
|
+
"""
|
|
161
|
+
Load and combine multiple policy documents.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
paths: List of paths to policy files
|
|
165
|
+
separator: String to separate documents (default: "\\n\\n---\\n\\n")
|
|
166
|
+
source_prefix: Optional prefix for source description (e.g., folder path)
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Policy object with combined text from all files
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
ValueError: If paths list is empty
|
|
173
|
+
FileNotFoundError: If any file doesn't exist
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
>>> # Multiple files
|
|
177
|
+
>>> policy = Policy.from_files(["doc1.pdf", "doc2.docx", "doc3.txt"])
|
|
178
|
+
>>> len(policy.text) > 0
|
|
179
|
+
True
|
|
180
|
+
|
|
181
|
+
>>> # With custom separator
|
|
182
|
+
>>> policy = Policy.from_files(
|
|
183
|
+
... ["part1.txt", "part2.txt"],
|
|
184
|
+
... separator="\\n\\n=== NEXT DOCUMENT ===\\n\\n"
|
|
185
|
+
... )
|
|
186
|
+
"""
|
|
187
|
+
if not paths:
|
|
188
|
+
raise ValueError("paths list cannot be empty")
|
|
189
|
+
|
|
190
|
+
texts: list[str] = []
|
|
191
|
+
sources: list[str] = []
|
|
192
|
+
|
|
193
|
+
for path in paths:
|
|
194
|
+
path_obj = Path(path)
|
|
195
|
+
|
|
196
|
+
if not path_obj.exists():
|
|
197
|
+
raise SynkroFileNotFoundError(str(path_obj), None)
|
|
198
|
+
|
|
199
|
+
if not path_obj.is_file():
|
|
200
|
+
raise ValueError(f"Path is not a file: {path_obj}")
|
|
201
|
+
|
|
202
|
+
# Load the file
|
|
203
|
+
policy = cls._load_single_file(path_obj)
|
|
204
|
+
texts.append(policy.text)
|
|
205
|
+
sources.append(str(path_obj))
|
|
206
|
+
|
|
207
|
+
# Combine texts
|
|
208
|
+
combined_text = separator.join(texts)
|
|
209
|
+
|
|
210
|
+
# Create source description
|
|
211
|
+
if source_prefix:
|
|
212
|
+
combined_source = f"{source_prefix} ({len(sources)} files: {', '.join(Path(s).name for s in sources)})"
|
|
213
|
+
else:
|
|
214
|
+
combined_source = f"multiple_files ({len(sources)} files: {', '.join(Path(s).name for s in sources)})"
|
|
215
|
+
|
|
216
|
+
return cls(text=combined_text, source=combined_source)
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def _from_pdf(cls, path: Path) -> "Policy":
|
|
220
|
+
"""
|
|
221
|
+
Parse PDF to markdown using marker-pdf.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
path: Path to PDF file
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Policy with extracted markdown text
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
from marker.convert import convert_single_pdf
|
|
231
|
+
from marker.models import load_all_models
|
|
232
|
+
|
|
233
|
+
models = load_all_models()
|
|
234
|
+
markdown, _, _ = convert_single_pdf(str(path), models)
|
|
235
|
+
return cls(text=markdown, source=str(path))
|
|
236
|
+
except ImportError:
|
|
237
|
+
raise ImportError(
|
|
238
|
+
"marker-pdf is required for PDF support. "
|
|
239
|
+
"Install with: pip install marker-pdf"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
@classmethod
|
|
243
|
+
def _from_docx(cls, path: Path) -> "Policy":
|
|
244
|
+
"""
|
|
245
|
+
Parse DOCX to markdown using mammoth.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
path: Path to DOCX file
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Policy with extracted markdown text
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
import mammoth
|
|
255
|
+
|
|
256
|
+
with open(path, "rb") as f:
|
|
257
|
+
result = mammoth.convert_to_markdown(f)
|
|
258
|
+
return cls(text=result.value, source=str(path))
|
|
259
|
+
except ImportError:
|
|
260
|
+
raise ImportError(
|
|
261
|
+
"mammoth is required for DOCX support. "
|
|
262
|
+
"Install with: pip install mammoth"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
@classmethod
|
|
266
|
+
def from_url(cls, url: str) -> "Policy":
|
|
267
|
+
"""
|
|
268
|
+
Fetch and parse policy from a URL.
|
|
269
|
+
|
|
270
|
+
Extracts main content and converts to markdown.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
url: URL to fetch
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Policy with extracted content
|
|
277
|
+
|
|
278
|
+
Example:
|
|
279
|
+
>>> policy = Policy.from_url("https://example.com/terms")
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
import httpx
|
|
283
|
+
from bs4 import BeautifulSoup
|
|
284
|
+
import html2text
|
|
285
|
+
|
|
286
|
+
response = httpx.get(url, follow_redirects=True)
|
|
287
|
+
response.raise_for_status()
|
|
288
|
+
|
|
289
|
+
soup = BeautifulSoup(response.text, "html.parser")
|
|
290
|
+
|
|
291
|
+
# Remove scripts, styles, nav, footer
|
|
292
|
+
for tag in soup(["script", "style", "nav", "footer", "header"]):
|
|
293
|
+
tag.decompose()
|
|
294
|
+
|
|
295
|
+
# Convert to markdown
|
|
296
|
+
h = html2text.HTML2Text()
|
|
297
|
+
h.ignore_links = False
|
|
298
|
+
h.ignore_images = True
|
|
299
|
+
markdown = h.handle(str(soup))
|
|
300
|
+
|
|
301
|
+
return cls(text=markdown, source=url)
|
|
302
|
+
except ImportError as e:
|
|
303
|
+
missing = str(e).split("'")[1] if "'" in str(e) else "required packages"
|
|
304
|
+
raise ImportError(
|
|
305
|
+
f"{missing} is required for URL support. "
|
|
306
|
+
"This should be installed automatically with synkro. "
|
|
307
|
+
"If you see this error, try: pip install --upgrade synkro"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def word_count(self) -> int:
|
|
312
|
+
"""Get the word count of the policy."""
|
|
313
|
+
return len(self.text.split())
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def char_count(self) -> int:
|
|
317
|
+
"""Get the character count of the policy."""
|
|
318
|
+
return len(self.text)
|
|
319
|
+
|
|
320
|
+
def validate_length(self) -> None:
|
|
321
|
+
"""
|
|
322
|
+
Validate that the policy has enough content for meaningful generation.
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
PolicyTooShortError: If policy is too short
|
|
326
|
+
"""
|
|
327
|
+
if self.word_count < MIN_POLICY_WORDS:
|
|
328
|
+
raise PolicyTooShortError(self.word_count)
|
|
329
|
+
|
|
330
|
+
def __str__(self) -> str:
|
|
331
|
+
"""String representation showing source and length."""
|
|
332
|
+
source = self.source or "inline"
|
|
333
|
+
return f"Policy(source={source}, words={self.word_count})"
|
|
334
|
+
|
|
335
|
+
def __repr__(self) -> str:
|
|
336
|
+
return self.__str__()
|
|
337
|
+
|
synkro/errors.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Friendly error messages with fix suggestions."""
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SynkroError(Exception):
|
|
10
|
+
"""Base exception for Synkro errors."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str, suggestion: str = ""):
|
|
13
|
+
self.message = message
|
|
14
|
+
self.suggestion = suggestion
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
|
|
17
|
+
def print_friendly(self):
|
|
18
|
+
"""Print a user-friendly error message."""
|
|
19
|
+
content = f"[red bold]{self.message}[/red bold]"
|
|
20
|
+
if self.suggestion:
|
|
21
|
+
content += f"\n\n[dim]{self.suggestion}[/dim]"
|
|
22
|
+
console.print(Panel(content, title="[red]Error[/red]", border_style="red"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class APIKeyError(SynkroError):
|
|
26
|
+
"""Raised when API key is missing or invalid."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, provider: str = "OpenAI"):
|
|
29
|
+
provider_info = {
|
|
30
|
+
"openai": {
|
|
31
|
+
"env_var": "OPENAI_API_KEY",
|
|
32
|
+
"url": "https://platform.openai.com/api-keys",
|
|
33
|
+
},
|
|
34
|
+
"anthropic": {
|
|
35
|
+
"env_var": "ANTHROPIC_API_KEY",
|
|
36
|
+
"url": "https://console.anthropic.com/settings/keys",
|
|
37
|
+
},
|
|
38
|
+
"google": {
|
|
39
|
+
"env_var": "GEMINI_API_KEY",
|
|
40
|
+
"url": "https://aistudio.google.com/app/apikey",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
info = provider_info.get(provider.lower(), provider_info["openai"])
|
|
45
|
+
|
|
46
|
+
message = f"{provider} API key not found or invalid"
|
|
47
|
+
suggestion = f"""To fix this, either:
|
|
48
|
+
|
|
49
|
+
1. Set environment variable:
|
|
50
|
+
export {info['env_var']}="your-key-here"
|
|
51
|
+
|
|
52
|
+
2. Pass directly:
|
|
53
|
+
synkro.generate(..., generation_model=LLM(api_key="..."))
|
|
54
|
+
|
|
55
|
+
Get an API key at: {info['url']}"""
|
|
56
|
+
|
|
57
|
+
super().__init__(message, suggestion)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class FileNotFoundError(SynkroError):
|
|
61
|
+
"""Raised when a policy file cannot be found."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, filepath: str, similar_files: list[str] | None = None):
|
|
64
|
+
message = f"Could not find file: {filepath}"
|
|
65
|
+
|
|
66
|
+
suggestion_parts = []
|
|
67
|
+
if similar_files:
|
|
68
|
+
suggestion_parts.append("Did you mean one of these?")
|
|
69
|
+
for f in similar_files[:3]:
|
|
70
|
+
suggestion_parts.append(f" → {f}")
|
|
71
|
+
suggestion_parts.append("")
|
|
72
|
+
|
|
73
|
+
suggestion_parts.append("Or pass text directly:")
|
|
74
|
+
suggestion_parts.append(' synkro.generate("Your policy text here...")')
|
|
75
|
+
|
|
76
|
+
super().__init__(message, "\n".join(suggestion_parts))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class RateLimitError(SynkroError):
|
|
80
|
+
"""Raised when hitting API rate limits."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, provider: str = "OpenAI", retry_after: int | None = None):
|
|
83
|
+
message = f"Rate limited by {provider}"
|
|
84
|
+
if retry_after:
|
|
85
|
+
message += f" (retry after {retry_after}s)"
|
|
86
|
+
|
|
87
|
+
suggestion = """Tip: Reduce the number of traces or wait and retry:
|
|
88
|
+
|
|
89
|
+
synkro.generate(..., traces=10)
|
|
90
|
+
|
|
91
|
+
Or use a different provider:
|
|
92
|
+
synkro.generate(..., generation_model=Google.GEMINI_25_FLASH)"""
|
|
93
|
+
|
|
94
|
+
super().__init__(message, suggestion)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PolicyTooShortError(SynkroError):
|
|
98
|
+
"""Raised when policy text is too short to generate meaningful data."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, word_count: int):
|
|
101
|
+
message = f"Policy too short ({word_count} words)"
|
|
102
|
+
suggestion = """Your policy needs more content to generate diverse training data.
|
|
103
|
+
|
|
104
|
+
Minimum recommended: 50+ words with clear rules or guidelines.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
synkro.generate('''
|
|
108
|
+
All expenses over $50 require manager approval.
|
|
109
|
+
Expenses over $500 require VP approval.
|
|
110
|
+
Receipts are required for all purchases over $25.
|
|
111
|
+
''')"""
|
|
112
|
+
|
|
113
|
+
super().__init__(message, suggestion)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ModelNotFoundError(SynkroError):
|
|
117
|
+
"""Raised when specified model doesn't exist."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, model: str):
|
|
120
|
+
message = f"Model not found: {model}"
|
|
121
|
+
suggestion = """Available models:
|
|
122
|
+
|
|
123
|
+
OpenAI: OpenAI.GPT_4O_MINI, OpenAI.GPT_4O
|
|
124
|
+
Anthropic: Anthropic.CLAUDE_35_SONNET, Anthropic.CLAUDE_35_HAIKU
|
|
125
|
+
Google: Google.GEMINI_25_FLASH, Google.GEMINI_25_PRO
|
|
126
|
+
|
|
127
|
+
Or pass any model string: "gpt-4o-mini", "claude-3-5-sonnet-20241022", etc."""
|
|
128
|
+
|
|
129
|
+
super().__init__(message, suggestion)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _detect_provider(error_str: str) -> str:
|
|
133
|
+
"""Detect the provider from the error message."""
|
|
134
|
+
error_lower = error_str.lower()
|
|
135
|
+
if "gemini" in error_lower or "googleapis" in error_lower or "google" in error_lower:
|
|
136
|
+
return "Google"
|
|
137
|
+
if "anthropic" in error_lower or "claude" in error_lower:
|
|
138
|
+
return "Anthropic"
|
|
139
|
+
return "OpenAI"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def handle_error(func):
|
|
143
|
+
"""Decorator to catch and display friendly errors."""
|
|
144
|
+
import sys
|
|
145
|
+
|
|
146
|
+
def wrapper(*args, **kwargs):
|
|
147
|
+
try:
|
|
148
|
+
return func(*args, **kwargs)
|
|
149
|
+
except SynkroError as e:
|
|
150
|
+
e.print_friendly()
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
# Try to convert common errors to friendly ones
|
|
154
|
+
error_str = str(e)
|
|
155
|
+
error_lower = error_str.lower()
|
|
156
|
+
|
|
157
|
+
if "api key" in error_lower or "authentication" in error_lower or "unauthorized" in error_lower:
|
|
158
|
+
provider = _detect_provider(error_str)
|
|
159
|
+
friendly = APIKeyError(provider)
|
|
160
|
+
friendly.print_friendly()
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
|
|
163
|
+
if "rate limit" in error_lower or "429" in error_lower:
|
|
164
|
+
provider = _detect_provider(error_str)
|
|
165
|
+
friendly = RateLimitError(provider)
|
|
166
|
+
friendly.print_friendly()
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
if "not found" in error_lower and "model" in error_lower:
|
|
170
|
+
friendly = ModelNotFoundError(str(e))
|
|
171
|
+
friendly.print_friendly()
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
|
|
174
|
+
# Re-raise unknown errors
|
|
175
|
+
raise
|
|
176
|
+
|
|
177
|
+
return wrapper
|
|
178
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Built-in example policies for instant demos."""
|
|
2
|
+
|
|
3
|
+
EXPENSE_POLICY = """# Company Expense Policy
|
|
4
|
+
|
|
5
|
+
## Approval Thresholds
|
|
6
|
+
- Expenses under $50: No approval required
|
|
7
|
+
- Expenses $50-$500: Manager approval required
|
|
8
|
+
- Expenses over $500: VP approval required
|
|
9
|
+
|
|
10
|
+
## Receipt Requirements
|
|
11
|
+
- All expenses over $25 must have a receipt
|
|
12
|
+
- Digital receipts are acceptable
|
|
13
|
+
- Missing receipts require written justification within 48 hours
|
|
14
|
+
|
|
15
|
+
## Categories
|
|
16
|
+
- Travel: Flights, hotels, ground transportation, meals while traveling
|
|
17
|
+
- Meals: Client meals, team events (max $75/person)
|
|
18
|
+
- Software: Must be on pre-approved list, exceptions need IT approval
|
|
19
|
+
- Equipment: Must be on asset tracking list if over $200
|
|
20
|
+
- Office Supplies: Under $100 can be purchased directly
|
|
21
|
+
|
|
22
|
+
## Reimbursement Timeline
|
|
23
|
+
- Submit expenses within 30 days of purchase
|
|
24
|
+
- Reimbursements processed within 14 business days
|
|
25
|
+
- Late submissions require manager exception approval
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
HR_HANDBOOK = """# Employee Handbook
|
|
29
|
+
|
|
30
|
+
## Work Hours
|
|
31
|
+
- Standard work week is 40 hours, Monday through Friday
|
|
32
|
+
- Core hours are 10am to 3pm when all employees should be available
|
|
33
|
+
- Flexible scheduling allowed with manager approval
|
|
34
|
+
|
|
35
|
+
## Time Off
|
|
36
|
+
- Full-time employees receive 15 days PTO per year
|
|
37
|
+
- PTO accrues monthly (1.25 days per month)
|
|
38
|
+
- Unused PTO can roll over up to 5 days
|
|
39
|
+
- PTO requests must be submitted 2 weeks in advance for 3+ days
|
|
40
|
+
|
|
41
|
+
## Remote Work
|
|
42
|
+
- Hybrid schedule: minimum 2 days in office per week
|
|
43
|
+
- Fully remote requires director approval
|
|
44
|
+
- Home office stipend of $500 for remote workers
|
|
45
|
+
|
|
46
|
+
## Performance Reviews
|
|
47
|
+
- Annual reviews conducted in December
|
|
48
|
+
- Mid-year check-ins in June
|
|
49
|
+
- Goals set at start of fiscal year
|
|
50
|
+
- Promotions considered during annual review cycle only
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
REFUND_POLICY = """# Return and Refund Policy
|
|
54
|
+
|
|
55
|
+
## Eligibility
|
|
56
|
+
- Items can be returned within 30 days of purchase
|
|
57
|
+
- Items must be unused and in original packaging
|
|
58
|
+
- Receipt or proof of purchase required
|
|
59
|
+
|
|
60
|
+
## Exceptions
|
|
61
|
+
- Final sale items cannot be returned
|
|
62
|
+
- Personalized items cannot be returned
|
|
63
|
+
- Perishable goods cannot be returned after 7 days
|
|
64
|
+
|
|
65
|
+
## Refund Process
|
|
66
|
+
- Refunds issued to original payment method
|
|
67
|
+
- Processing takes 5-10 business days
|
|
68
|
+
- Shipping costs are non-refundable unless item was defective
|
|
69
|
+
|
|
70
|
+
## Exchanges
|
|
71
|
+
- Exchanges available within 30 days
|
|
72
|
+
- Size exchanges free of charge
|
|
73
|
+
- Different item exchanges treated as return + new purchase
|
|
74
|
+
|
|
75
|
+
## Defective Items
|
|
76
|
+
- Report defects within 14 days
|
|
77
|
+
- Photos required for defect claims
|
|
78
|
+
- Replacement or full refund offered for confirmed defects
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
SUPPORT_GUIDELINES = """# Customer Support Guidelines
|
|
82
|
+
|
|
83
|
+
## Response Times
|
|
84
|
+
- Chat: Respond within 2 minutes
|
|
85
|
+
- Email: Respond within 4 hours during business hours
|
|
86
|
+
- Phone: Answer within 30 seconds, max hold time 3 minutes
|
|
87
|
+
|
|
88
|
+
## Escalation Tiers
|
|
89
|
+
- Tier 1: General questions, password resets, basic troubleshooting
|
|
90
|
+
- Tier 2: Technical issues, billing disputes, account problems
|
|
91
|
+
- Tier 3: Complex technical issues, executive escalations
|
|
92
|
+
|
|
93
|
+
## Refund Authority
|
|
94
|
+
- Tier 1 can issue refunds up to $50
|
|
95
|
+
- Tier 2 can issue refunds up to $200
|
|
96
|
+
- Tier 3 or manager approval needed for refunds over $200
|
|
97
|
+
|
|
98
|
+
## Documentation
|
|
99
|
+
- Log all customer interactions in CRM
|
|
100
|
+
- Include customer sentiment and issue category
|
|
101
|
+
- Note any promised follow-ups with deadlines
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
SECURITY_POLICY = """# Information Security Policy
|
|
105
|
+
|
|
106
|
+
## Password Requirements
|
|
107
|
+
- Minimum 12 characters
|
|
108
|
+
- Must include uppercase, lowercase, number, and symbol
|
|
109
|
+
- Change every 90 days
|
|
110
|
+
- Cannot reuse last 10 passwords
|
|
111
|
+
|
|
112
|
+
## Access Control
|
|
113
|
+
- Principle of least privilege applies
|
|
114
|
+
- Access requests require manager approval
|
|
115
|
+
- Quarterly access reviews mandatory
|
|
116
|
+
- Terminate access within 24 hours of employee departure
|
|
117
|
+
|
|
118
|
+
## Data Classification
|
|
119
|
+
- Public: Marketing materials, job postings
|
|
120
|
+
- Internal: Company announcements, policies
|
|
121
|
+
- Confidential: Customer data, financials
|
|
122
|
+
- Restricted: PII, payment info, credentials
|
|
123
|
+
|
|
124
|
+
## Incident Response
|
|
125
|
+
- Report security incidents within 1 hour
|
|
126
|
+
- Do not attempt to investigate independently
|
|
127
|
+
- Preserve evidence (don't delete logs or files)
|
|
128
|
+
- Security team leads all incident response
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
# All policies available as a list
|
|
132
|
+
ALL_POLICIES = [
|
|
133
|
+
("expense", EXPENSE_POLICY),
|
|
134
|
+
("hr", HR_HANDBOOK),
|
|
135
|
+
("refund", REFUND_POLICY),
|
|
136
|
+
("support", SUPPORT_GUIDELINES),
|
|
137
|
+
("security", SECURITY_POLICY),
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
__all__ = [
|
|
141
|
+
"EXPENSE_POLICY",
|
|
142
|
+
"HR_HANDBOOK",
|
|
143
|
+
"REFUND_POLICY",
|
|
144
|
+
"SUPPORT_GUIDELINES",
|
|
145
|
+
"SECURITY_POLICY",
|
|
146
|
+
"ALL_POLICIES",
|
|
147
|
+
]
|
|
148
|
+
|