synkro 0.4.12__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.
- synkro/__init__.py +179 -0
- synkro/advanced.py +186 -0
- synkro/cli.py +128 -0
- synkro/core/__init__.py +7 -0
- synkro/core/checkpoint.py +250 -0
- synkro/core/dataset.py +402 -0
- synkro/core/policy.py +337 -0
- synkro/errors.py +178 -0
- synkro/examples/__init__.py +148 -0
- synkro/factory.py +276 -0
- synkro/formatters/__init__.py +12 -0
- synkro/formatters/qa.py +98 -0
- synkro/formatters/sft.py +90 -0
- synkro/formatters/tool_call.py +127 -0
- synkro/generation/__init__.py +9 -0
- synkro/generation/follow_ups.py +134 -0
- synkro/generation/generator.py +220 -0
- synkro/generation/golden_responses.py +244 -0
- synkro/generation/golden_scenarios.py +276 -0
- synkro/generation/golden_tool_responses.py +416 -0
- synkro/generation/logic_extractor.py +126 -0
- synkro/generation/multiturn_responses.py +177 -0
- synkro/generation/planner.py +131 -0
- synkro/generation/responses.py +189 -0
- synkro/generation/scenarios.py +90 -0
- synkro/generation/tool_responses.py +376 -0
- synkro/generation/tool_simulator.py +114 -0
- synkro/interactive/__init__.py +12 -0
- synkro/interactive/hitl_session.py +77 -0
- synkro/interactive/logic_map_editor.py +173 -0
- synkro/interactive/rich_ui.py +205 -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 +592 -0
- synkro/pipeline/runner.py +424 -0
- synkro/pipelines.py +123 -0
- synkro/prompts/__init__.py +57 -0
- synkro/prompts/base.py +167 -0
- synkro/prompts/golden_templates.py +474 -0
- synkro/prompts/interactive_templates.py +65 -0
- synkro/prompts/multiturn_templates.py +156 -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/golden_refiner.py +163 -0
- synkro/quality/grader.py +153 -0
- synkro/quality/multiturn_grader.py +150 -0
- synkro/quality/refiner.py +137 -0
- synkro/quality/tool_grader.py +126 -0
- synkro/quality/tool_refiner.py +128 -0
- synkro/quality/verifier.py +228 -0
- synkro/reporting.py +537 -0
- synkro/schemas.py +472 -0
- synkro/types/__init__.py +41 -0
- synkro/types/core.py +126 -0
- synkro/types/dataset_type.py +30 -0
- synkro/types/logic_map.py +345 -0
- synkro/types/tool.py +94 -0
- synkro-0.4.12.data/data/examples/__init__.py +148 -0
- synkro-0.4.12.dist-info/METADATA +258 -0
- synkro-0.4.12.dist-info/RECORD +77 -0
- synkro-0.4.12.dist-info/WHEEL +4 -0
- synkro-0.4.12.dist-info/entry_points.txt +2 -0
- synkro-0.4.12.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
|
+
|