synkro 0.4.36__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.

Files changed (81) hide show
  1. synkro/__init__.py +331 -0
  2. synkro/advanced.py +184 -0
  3. synkro/cli.py +156 -0
  4. synkro/core/__init__.py +7 -0
  5. synkro/core/checkpoint.py +250 -0
  6. synkro/core/dataset.py +432 -0
  7. synkro/core/policy.py +337 -0
  8. synkro/errors.py +178 -0
  9. synkro/examples/__init__.py +148 -0
  10. synkro/factory.py +291 -0
  11. synkro/formatters/__init__.py +18 -0
  12. synkro/formatters/chatml.py +121 -0
  13. synkro/formatters/langfuse.py +98 -0
  14. synkro/formatters/langsmith.py +98 -0
  15. synkro/formatters/qa.py +112 -0
  16. synkro/formatters/sft.py +90 -0
  17. synkro/formatters/tool_call.py +127 -0
  18. synkro/generation/__init__.py +9 -0
  19. synkro/generation/follow_ups.py +134 -0
  20. synkro/generation/generator.py +314 -0
  21. synkro/generation/golden_responses.py +269 -0
  22. synkro/generation/golden_scenarios.py +333 -0
  23. synkro/generation/golden_tool_responses.py +791 -0
  24. synkro/generation/logic_extractor.py +126 -0
  25. synkro/generation/multiturn_responses.py +177 -0
  26. synkro/generation/planner.py +131 -0
  27. synkro/generation/responses.py +189 -0
  28. synkro/generation/scenarios.py +90 -0
  29. synkro/generation/tool_responses.py +625 -0
  30. synkro/generation/tool_simulator.py +114 -0
  31. synkro/interactive/__init__.py +16 -0
  32. synkro/interactive/hitl_session.py +205 -0
  33. synkro/interactive/intent_classifier.py +94 -0
  34. synkro/interactive/logic_map_editor.py +176 -0
  35. synkro/interactive/rich_ui.py +459 -0
  36. synkro/interactive/scenario_editor.py +198 -0
  37. synkro/llm/__init__.py +7 -0
  38. synkro/llm/client.py +309 -0
  39. synkro/llm/rate_limits.py +99 -0
  40. synkro/models/__init__.py +50 -0
  41. synkro/models/anthropic.py +26 -0
  42. synkro/models/google.py +19 -0
  43. synkro/models/local.py +104 -0
  44. synkro/models/openai.py +31 -0
  45. synkro/modes/__init__.py +13 -0
  46. synkro/modes/config.py +66 -0
  47. synkro/modes/conversation.py +35 -0
  48. synkro/modes/tool_call.py +18 -0
  49. synkro/parsers.py +442 -0
  50. synkro/pipeline/__init__.py +20 -0
  51. synkro/pipeline/phases.py +592 -0
  52. synkro/pipeline/runner.py +769 -0
  53. synkro/pipelines.py +136 -0
  54. synkro/prompts/__init__.py +57 -0
  55. synkro/prompts/base.py +167 -0
  56. synkro/prompts/golden_templates.py +533 -0
  57. synkro/prompts/interactive_templates.py +198 -0
  58. synkro/prompts/multiturn_templates.py +156 -0
  59. synkro/prompts/templates.py +281 -0
  60. synkro/prompts/tool_templates.py +318 -0
  61. synkro/quality/__init__.py +14 -0
  62. synkro/quality/golden_refiner.py +163 -0
  63. synkro/quality/grader.py +153 -0
  64. synkro/quality/multiturn_grader.py +150 -0
  65. synkro/quality/refiner.py +137 -0
  66. synkro/quality/tool_grader.py +126 -0
  67. synkro/quality/tool_refiner.py +128 -0
  68. synkro/quality/verifier.py +228 -0
  69. synkro/reporting.py +464 -0
  70. synkro/schemas.py +521 -0
  71. synkro/types/__init__.py +43 -0
  72. synkro/types/core.py +153 -0
  73. synkro/types/dataset_type.py +33 -0
  74. synkro/types/logic_map.py +348 -0
  75. synkro/types/tool.py +94 -0
  76. synkro-0.4.36.data/data/examples/__init__.py +148 -0
  77. synkro-0.4.36.dist-info/METADATA +507 -0
  78. synkro-0.4.36.dist-info/RECORD +81 -0
  79. synkro-0.4.36.dist-info/WHEEL +4 -0
  80. synkro-0.4.36.dist-info/entry_points.txt +2 -0
  81. synkro-0.4.36.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,126 @@
1
+ """Logic Extractor - The Cartographer.
2
+
3
+ Extracts a Logic Map (DAG of rules) from a policy document.
4
+ This is Stage 1 of the Golden Trace pipeline.
5
+ """
6
+
7
+ from synkro.llm.client import LLM
8
+ from synkro.models import Model, OpenAI
9
+ from synkro.schemas import LogicMapOutput
10
+ from synkro.types.logic_map import LogicMap, Rule, RuleCategory
11
+ from synkro.prompts.golden_templates import LOGIC_EXTRACTION_PROMPT
12
+
13
+
14
+ class LogicExtractor:
15
+ """
16
+ The Cartographer - Extracts a Logic Map from policy documents.
17
+
18
+ The Logic Map is a Directed Acyclic Graph (DAG) of rules that enables:
19
+ - Grounded scenario generation with rule references
20
+ - Chain-of-thought reasoning with rule citations
21
+ - Verification that traces don't skip or hallucinate rules
22
+
23
+ Examples:
24
+ >>> extractor = LogicExtractor(llm=LLM(model=OpenAI.GPT_4O))
25
+ >>> logic_map = await extractor.extract(policy_text)
26
+ >>> print(f"Extracted {len(logic_map.rules)} rules")
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ llm: LLM | None = None,
32
+ model: Model = OpenAI.GPT_4O,
33
+ ):
34
+ """
35
+ Initialize the Logic Extractor.
36
+
37
+ Args:
38
+ llm: LLM client to use (creates one if not provided)
39
+ model: Model to use if creating LLM (default: GPT-4O for accuracy)
40
+ """
41
+ self.llm = llm or LLM(model=model, temperature=0.3)
42
+
43
+ async def extract(self, policy_text: str) -> LogicMap:
44
+ """
45
+ Extract a Logic Map from a policy document.
46
+
47
+ Args:
48
+ policy_text: The policy document text
49
+
50
+ Returns:
51
+ LogicMap with extracted rules as a DAG
52
+
53
+ Raises:
54
+ ValueError: If extraction fails or produces invalid DAG
55
+ """
56
+ # Format the prompt
57
+ prompt = LOGIC_EXTRACTION_PROMPT.format(policy_text=policy_text)
58
+
59
+ # Generate structured output
60
+ result = await self.llm.generate_structured(prompt, LogicMapOutput)
61
+
62
+ # Convert to domain model
63
+ logic_map = self._convert_to_logic_map(result)
64
+
65
+ # Validate DAG properties
66
+ if not logic_map.validate_dag():
67
+ raise ValueError("Extracted rules contain circular dependencies")
68
+
69
+ return logic_map
70
+
71
+ def _convert_to_logic_map(self, output: LogicMapOutput) -> LogicMap:
72
+ """Convert schema output to domain model."""
73
+ rules = []
74
+ for rule_out in output.rules:
75
+ # Convert category string to enum
76
+ category = RuleCategory(rule_out.category)
77
+
78
+ rule = Rule(
79
+ rule_id=rule_out.rule_id,
80
+ text=rule_out.text,
81
+ condition=rule_out.condition,
82
+ action=rule_out.action,
83
+ dependencies=rule_out.dependencies,
84
+ category=category,
85
+ )
86
+ rules.append(rule)
87
+
88
+ return LogicMap(
89
+ rules=rules,
90
+ root_rules=output.root_rules,
91
+ )
92
+
93
+ async def extract_with_retry(
94
+ self,
95
+ policy_text: str,
96
+ max_retries: int = 2,
97
+ ) -> LogicMap:
98
+ """
99
+ Extract with retry on validation failure.
100
+
101
+ Args:
102
+ policy_text: The policy document text
103
+ max_retries: Maximum retry attempts
104
+
105
+ Returns:
106
+ LogicMap with extracted rules
107
+
108
+ Raises:
109
+ ValueError: If extraction fails after all retries
110
+ """
111
+ last_error = None
112
+ for attempt in range(max_retries + 1):
113
+ try:
114
+ return await self.extract(policy_text)
115
+ except ValueError as e:
116
+ last_error = e
117
+ if attempt < max_retries:
118
+ # Add hint for next attempt
119
+ continue
120
+
121
+ raise ValueError(
122
+ f"Failed to extract valid Logic Map after {max_retries + 1} attempts: {last_error}"
123
+ )
124
+
125
+
126
+ __all__ = ["LogicExtractor"]
@@ -0,0 +1,177 @@
1
+ """Multi-turn response generation for complex conversations."""
2
+
3
+ from synkro.llm.client import LLM
4
+ from synkro.models import Model, OpenAI
5
+ from synkro.types.core import Scenario, Trace, Message
6
+ from synkro.prompts.multiturn_templates import (
7
+ MULTI_TURN_INITIAL_PROMPT,
8
+ MULTI_TURN_RESPONSE_PROMPT,
9
+ )
10
+ from synkro.prompts.templates import SYSTEM_PROMPT
11
+ from synkro.generation.follow_ups import FollowUpGenerator
12
+
13
+
14
+ class MultiTurnResponseGenerator:
15
+ """
16
+ Generates multi-turn conversations based on policy complexity.
17
+
18
+ Turn allocation:
19
+ - Simple (1-2 turns): Single query -> Straight answer
20
+ - Conditional (3 turns): Query -> Clarification -> Verdict
21
+ - Complex (5+ turns): Multiple rounds of exploration
22
+
23
+ Examples:
24
+ >>> gen = MultiTurnResponseGenerator()
25
+ >>> trace = await gen.generate_single(policy_text, scenario, target_turns=3)
26
+ >>> print(len([m for m in trace.messages if m.role == "assistant"]))
27
+ 3
28
+ """
29
+
30
+ def __init__(self, llm: LLM | None = None, model: Model = OpenAI.GPT_4O_MINI):
31
+ """
32
+ Initialize the multi-turn response generator.
33
+
34
+ Args:
35
+ llm: LLM client to use (creates one if not provided)
36
+ model: Model to use if creating LLM
37
+ """
38
+ self.llm = llm or LLM(model=model)
39
+ self.follow_up_gen = FollowUpGenerator(llm=self.llm)
40
+
41
+ def _format_conversation(self, messages: list[Message]) -> str:
42
+ """Format conversation messages for prompt inclusion."""
43
+ formatted = []
44
+ for msg in messages:
45
+ role = msg.role.upper()
46
+ content = msg.content or "[No content]"
47
+ formatted.append(f"{role}: {content}")
48
+ return "\n\n".join(formatted)
49
+
50
+ async def _generate_initial_response(
51
+ self,
52
+ policy_text: str,
53
+ scenario: Scenario,
54
+ target_turns: int,
55
+ ) -> list[Message]:
56
+ """
57
+ Generate the initial exchange (system + user + assistant).
58
+
59
+ Args:
60
+ policy_text: The policy text
61
+ scenario: The scenario to respond to
62
+ target_turns: Total target turns for context
63
+
64
+ Returns:
65
+ List of initial messages [system, user, assistant]
66
+ """
67
+ prompt = MULTI_TURN_INITIAL_PROMPT.format(
68
+ target_turns=target_turns,
69
+ scenario=scenario.description,
70
+ context=scenario.context,
71
+ policy=policy_text,
72
+ )
73
+
74
+ response = await self.llm.generate(prompt)
75
+
76
+ return [
77
+ Message(role="system", content=SYSTEM_PROMPT),
78
+ Message(role="user", content=f"{scenario.description}\n\nContext: {scenario.context}"),
79
+ Message(role="assistant", content=response.strip()),
80
+ ]
81
+
82
+ async def _generate_response(
83
+ self,
84
+ policy_text: str,
85
+ messages: list[Message],
86
+ question: str,
87
+ ) -> str:
88
+ """
89
+ Generate a response to a follow-up question.
90
+
91
+ Args:
92
+ policy_text: The policy text
93
+ messages: Conversation history
94
+ question: The follow-up question to answer
95
+
96
+ Returns:
97
+ Assistant response text
98
+ """
99
+ conversation = self._format_conversation(messages)
100
+
101
+ prompt = MULTI_TURN_RESPONSE_PROMPT.format(
102
+ conversation=conversation,
103
+ question=question,
104
+ policy=policy_text,
105
+ )
106
+
107
+ response = await self.llm.generate(prompt)
108
+ return response.strip()
109
+
110
+ async def generate_single(
111
+ self,
112
+ policy_text: str,
113
+ scenario: Scenario,
114
+ target_turns: int,
115
+ ) -> Trace:
116
+ """
117
+ Generate a multi-turn trace for one scenario.
118
+
119
+ Args:
120
+ policy_text: The policy text
121
+ scenario: The scenario to generate for
122
+ target_turns: Number of user-assistant exchanges
123
+
124
+ Returns:
125
+ Trace with multi-turn conversation
126
+ """
127
+ # Generate initial exchange
128
+ messages = await self._generate_initial_response(
129
+ policy_text, scenario, target_turns
130
+ )
131
+
132
+ # Generate follow-up turns
133
+ for turn in range(1, target_turns):
134
+ # Generate follow-up question
135
+ follow_up = await self.follow_up_gen.generate(
136
+ policy_text=policy_text,
137
+ messages=messages,
138
+ turn_index=turn,
139
+ )
140
+
141
+ # Add user message with follow-up question
142
+ messages.append(Message(role="user", content=follow_up.question))
143
+
144
+ # Generate assistant response
145
+ response = await self._generate_response(
146
+ policy_text=policy_text,
147
+ messages=messages,
148
+ question=follow_up.question,
149
+ )
150
+
151
+ # Add assistant response
152
+ messages.append(Message(role="assistant", content=response))
153
+
154
+ return Trace(messages=messages, scenario=scenario)
155
+
156
+ async def generate(
157
+ self,
158
+ policy_text: str,
159
+ scenarios: list[Scenario],
160
+ target_turns: int,
161
+ ) -> list[Trace]:
162
+ """
163
+ Generate multi-turn traces for multiple scenarios.
164
+
165
+ Args:
166
+ policy_text: The policy text
167
+ scenarios: List of scenarios to generate for
168
+ target_turns: Number of turns per trace
169
+
170
+ Returns:
171
+ List of traces with multi-turn conversations
172
+ """
173
+ traces = []
174
+ for scenario in scenarios:
175
+ trace = await self.generate_single(policy_text, scenario, target_turns)
176
+ traces.append(trace)
177
+ return traces
@@ -0,0 +1,131 @@
1
+ """Planning for trace generation across categories."""
2
+
3
+ from synkro.llm.client import LLM
4
+ from synkro.models import Model, OpenAI
5
+ from synkro.types.core import Plan, Category
6
+ from synkro.prompts.templates import POLICY_PLANNING_PROMPT, POLICY_COMPLEXITY_PROMPT
7
+ from synkro.schemas import PolicyPlan, PolicyComplexity
8
+
9
+
10
+ class Planner:
11
+ """
12
+ Plans how to distribute trace generation across categories.
13
+
14
+ The planner analyzes the policy and creates an optimal distribution
15
+ of scenarios across different categories to ensure comprehensive
16
+ coverage.
17
+
18
+ Examples:
19
+ >>> planner = Planner()
20
+ >>> plan = await planner.plan(policy, target_traces=100)
21
+ >>> for cat in plan.categories:
22
+ ... print(f"{cat.name}: {cat.traces} traces")
23
+ """
24
+
25
+ def __init__(self, llm: LLM | None = None, model: Model = OpenAI.GPT_4O):
26
+ """
27
+ Initialize the planner.
28
+
29
+ Args:
30
+ llm: LLM client to use (creates one if not provided)
31
+ model: Model to use if creating LLM
32
+ """
33
+ self.llm = llm or LLM(model=model)
34
+
35
+ async def analyze_complexity(self, policy_text: str) -> PolicyComplexity:
36
+ """
37
+ Analyze policy complexity to determine optimal conversation turns.
38
+
39
+ Uses the PolicyComplexity schema to assess:
40
+ - Variable count (rules, conditions, exceptions)
41
+ - Complexity level (simple, conditional, complex)
42
+ - Recommended turns (1-6)
43
+
44
+ Args:
45
+ policy_text: The policy text to analyze
46
+
47
+ Returns:
48
+ PolicyComplexity with recommended turns and complexity level
49
+ """
50
+ prompt = f"""{POLICY_COMPLEXITY_PROMPT}
51
+
52
+ POLICY:
53
+ {policy_text}
54
+
55
+ Analyze the policy complexity and recommend conversation turns."""
56
+
57
+ try:
58
+ return await self.llm.generate_structured(prompt, PolicyComplexity)
59
+ except Exception:
60
+ # Default to simple single-turn
61
+ return PolicyComplexity(
62
+ variable_count=1,
63
+ complexity_level="simple",
64
+ recommended_turns=1,
65
+ reasoning="Default - unable to analyze complexity",
66
+ )
67
+
68
+ async def plan(self, policy_text: str, target_traces: int, analyze_turns: bool = True) -> Plan:
69
+ """
70
+ Create a generation plan for the policy.
71
+
72
+ Analyzes the policy and determines optimal category distribution
73
+ and conversation turn count.
74
+
75
+ Args:
76
+ policy_text: The policy text to analyze
77
+ target_traces: Target number of traces to generate
78
+ analyze_turns: Whether to analyze policy for turn recommendation
79
+
80
+ Returns:
81
+ Plan object with categories, reasoning, and turn recommendations
82
+ """
83
+ prompt = f"""{POLICY_PLANNING_PROMPT}
84
+
85
+ POLICY:
86
+ {policy_text}
87
+
88
+ TARGET TRACES: {target_traces}
89
+
90
+ Analyze the policy and create a plan with categories for generating training data."""
91
+
92
+ # Analyze complexity for turn recommendations
93
+ complexity = None
94
+ if analyze_turns:
95
+ complexity = await self.analyze_complexity(policy_text)
96
+
97
+ try:
98
+ # Use structured output for reliable planning
99
+ parsed = await self.llm.generate_structured(prompt, PolicyPlan)
100
+
101
+ # Convert to typed objects
102
+ categories = [
103
+ Category(
104
+ name=c.name,
105
+ description=c.description,
106
+ count=c.traces,
107
+ )
108
+ for c in parsed.categories
109
+ ]
110
+
111
+ return Plan(
112
+ categories=categories,
113
+ reasoning=parsed.reasoning,
114
+ recommended_turns=complexity.recommended_turns if complexity else 1,
115
+ complexity_level=complexity.complexity_level if complexity else "simple",
116
+ )
117
+ except Exception:
118
+ # Fallback plan
119
+ third = target_traces // 3
120
+ remainder = target_traces - (third * 3)
121
+ return Plan(
122
+ categories=[
123
+ Category(name="Happy Path", description="Clear success cases", count=third),
124
+ Category(name="Edge Cases", description="Ambiguous situations", count=third),
125
+ Category(name="Violations", description="Clear failure cases", count=third + remainder),
126
+ ],
127
+ reasoning="Default plan - unable to parse LLM response",
128
+ recommended_turns=complexity.recommended_turns if complexity else 1,
129
+ complexity_level=complexity.complexity_level if complexity else "simple",
130
+ )
131
+
@@ -0,0 +1,189 @@
1
+ """Response generation for scenarios."""
2
+
3
+ from synkro.llm.client import LLM
4
+ from synkro.models import Model, OpenAI
5
+ from synkro.types.core import Scenario, Trace, Message
6
+ from synkro.prompts.templates import BATCHED_RESPONSE_PROMPT, SYSTEM_PROMPT
7
+ from synkro.schemas import SingleResponse
8
+ from synkro.parsers import parse_batched_responses, extract_content
9
+ from synkro.generation.multiturn_responses import MultiTurnResponseGenerator
10
+
11
+
12
+ class ResponseGenerator:
13
+ """
14
+ Generates expert responses for scenarios.
15
+
16
+ Creates comprehensive, policy-grounded responses that demonstrate
17
+ deep domain understanding.
18
+
19
+ Examples:
20
+ >>> gen = ResponseGenerator()
21
+ >>> traces = await gen.generate(policy.text, scenarios)
22
+ """
23
+
24
+ def __init__(self, llm: LLM | None = None, model: Model = OpenAI.GPT_4O_MINI):
25
+ """
26
+ Initialize the response generator.
27
+
28
+ Args:
29
+ llm: LLM client to use (creates one if not provided)
30
+ model: Model to use if creating LLM
31
+ """
32
+ self.llm = llm or LLM(model=model)
33
+ self._multi_turn_gen: MultiTurnResponseGenerator | None = None
34
+
35
+ @property
36
+ def multi_turn_gen(self) -> MultiTurnResponseGenerator:
37
+ """Lazy initialization of multi-turn generator."""
38
+ if self._multi_turn_gen is None:
39
+ self._multi_turn_gen = MultiTurnResponseGenerator(llm=self.llm)
40
+ return self._multi_turn_gen
41
+
42
+ async def generate(
43
+ self,
44
+ policy_text: str,
45
+ scenarios: list[Scenario],
46
+ target_turns: int = 1,
47
+ ) -> list[Trace]:
48
+ """
49
+ Generate responses for scenarios.
50
+
51
+ Args:
52
+ policy_text: The policy text
53
+ scenarios: List of scenarios to respond to
54
+ target_turns: Number of conversation turns (1 for single-turn)
55
+
56
+ Returns:
57
+ List of traces with generated responses
58
+ """
59
+ traces = []
60
+
61
+ # Generate responses one at a time for better quality
62
+ for scenario in scenarios:
63
+ trace = await self._generate_single(policy_text, scenario, target_turns)
64
+ traces.append(trace)
65
+
66
+ return traces
67
+
68
+ async def _generate_single(
69
+ self,
70
+ policy_text: str,
71
+ scenario: Scenario,
72
+ target_turns: int = 1,
73
+ ) -> Trace:
74
+ """
75
+ Generate a single trace for one scenario.
76
+
77
+ Args:
78
+ policy_text: The policy text
79
+ scenario: The scenario to respond to
80
+ target_turns: Number of conversation turns (1 for single-turn)
81
+
82
+ Returns:
83
+ Trace with generated messages
84
+ """
85
+ # Delegate to multi-turn generator for multi-turn traces
86
+ if target_turns > 1:
87
+ return await self.multi_turn_gen.generate_single(
88
+ policy_text, scenario, target_turns
89
+ )
90
+
91
+ # Single-turn generation
92
+ prompt = f"""You are a domain expert generating a training example.
93
+
94
+ Given the scenario and policy below, create a complete training example.
95
+
96
+ The assistant response must:
97
+ - Start with <reasoning> tags showing your thought process
98
+ - Cite specific policy sections that apply
99
+ - Give specific, actionable recommendations
100
+ - Address all aspects of the scenario
101
+ - Acknowledge edge cases and complications
102
+
103
+ SCENARIO:
104
+ {scenario.description}
105
+
106
+ CONTEXT:
107
+ {scenario.context}
108
+
109
+ POLICY:
110
+ {policy_text}
111
+
112
+ Generate exactly 3 messages: system, user, and assistant."""
113
+
114
+ # Use structured output for reliable JSON
115
+ parsed = await self.llm.generate_structured(prompt, SingleResponse)
116
+ messages = [
117
+ Message(role=m.role, content=m.content) for m in parsed.messages
118
+ ]
119
+
120
+ return Trace(messages=messages, scenario=scenario)
121
+
122
+ async def generate_batch(
123
+ self,
124
+ policy_text: str,
125
+ scenarios: list[Scenario],
126
+ batch_size: int = 10,
127
+ ) -> list[Trace]:
128
+ """
129
+ Generate responses in batches.
130
+
131
+ More efficient than single generation for large numbers of scenarios.
132
+
133
+ Args:
134
+ policy_text: The policy text
135
+ scenarios: List of scenarios to respond to
136
+ batch_size: Number of scenarios per batch
137
+
138
+ Returns:
139
+ List of traces with generated responses
140
+ """
141
+ traces = []
142
+
143
+ for i in range(0, len(scenarios), batch_size):
144
+ batch = scenarios[i : i + batch_size]
145
+ batch_traces = await self._generate_batch(policy_text, batch)
146
+ traces.extend(batch_traces)
147
+
148
+ return traces
149
+
150
+ async def _generate_batch(
151
+ self,
152
+ policy_text: str,
153
+ scenarios: list[Scenario],
154
+ ) -> list[Trace]:
155
+ """Generate traces for a batch of scenarios."""
156
+ scenarios_text = "\n\n".join(
157
+ f"SCENARIO {i}:\n{s.description}\n\nCONTEXT:\n{s.context}"
158
+ for i, s in enumerate(scenarios)
159
+ )
160
+
161
+ prompt = f"""{BATCHED_RESPONSE_PROMPT}
162
+
163
+ SYSTEM PROMPT TO USE:
164
+ {SYSTEM_PROMPT}
165
+
166
+ POLICY:
167
+ {policy_text}
168
+
169
+ SCENARIOS:
170
+ {scenarios_text}"""
171
+
172
+ response = await self.llm.generate(prompt)
173
+ from synkro.schemas import ScenarioOutput
174
+
175
+ scenario_outputs = [
176
+ ScenarioOutput(scenario=s.description, context=s.context) for s in scenarios
177
+ ]
178
+ parsed = parse_batched_responses(response, len(scenarios), scenario_outputs)
179
+
180
+ traces = []
181
+ for i, p in enumerate(parsed):
182
+ scenario = scenarios[min(p["index"], len(scenarios) - 1)]
183
+ messages = [
184
+ Message(role=m.role, content=m.content) for m in p["messages"]
185
+ ]
186
+ traces.append(Trace(messages=messages, scenario=scenario))
187
+
188
+ return traces
189
+