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.
- synkro/__init__.py +331 -0
- synkro/advanced.py +184 -0
- synkro/cli.py +156 -0
- synkro/core/__init__.py +7 -0
- synkro/core/checkpoint.py +250 -0
- synkro/core/dataset.py +432 -0
- synkro/core/policy.py +337 -0
- synkro/errors.py +178 -0
- synkro/examples/__init__.py +148 -0
- synkro/factory.py +291 -0
- synkro/formatters/__init__.py +18 -0
- synkro/formatters/chatml.py +121 -0
- synkro/formatters/langfuse.py +98 -0
- synkro/formatters/langsmith.py +98 -0
- synkro/formatters/qa.py +112 -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 +314 -0
- synkro/generation/golden_responses.py +269 -0
- synkro/generation/golden_scenarios.py +333 -0
- synkro/generation/golden_tool_responses.py +791 -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 +625 -0
- synkro/generation/tool_simulator.py +114 -0
- synkro/interactive/__init__.py +16 -0
- synkro/interactive/hitl_session.py +205 -0
- synkro/interactive/intent_classifier.py +94 -0
- synkro/interactive/logic_map_editor.py +176 -0
- synkro/interactive/rich_ui.py +459 -0
- synkro/interactive/scenario_editor.py +198 -0
- synkro/llm/__init__.py +7 -0
- synkro/llm/client.py +309 -0
- synkro/llm/rate_limits.py +99 -0
- synkro/models/__init__.py +50 -0
- synkro/models/anthropic.py +26 -0
- synkro/models/google.py +19 -0
- synkro/models/local.py +104 -0
- synkro/models/openai.py +31 -0
- synkro/modes/__init__.py +13 -0
- synkro/modes/config.py +66 -0
- synkro/modes/conversation.py +35 -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 +769 -0
- synkro/pipelines.py +136 -0
- synkro/prompts/__init__.py +57 -0
- synkro/prompts/base.py +167 -0
- synkro/prompts/golden_templates.py +533 -0
- synkro/prompts/interactive_templates.py +198 -0
- synkro/prompts/multiturn_templates.py +156 -0
- synkro/prompts/templates.py +281 -0
- synkro/prompts/tool_templates.py +318 -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 +464 -0
- synkro/schemas.py +521 -0
- synkro/types/__init__.py +43 -0
- synkro/types/core.py +153 -0
- synkro/types/dataset_type.py +33 -0
- synkro/types/logic_map.py +348 -0
- synkro/types/tool.py +94 -0
- synkro-0.4.36.data/data/examples/__init__.py +148 -0
- synkro-0.4.36.dist-info/METADATA +507 -0
- synkro-0.4.36.dist-info/RECORD +81 -0
- synkro-0.4.36.dist-info/WHEEL +4 -0
- synkro-0.4.36.dist-info/entry_points.txt +2 -0
- synkro-0.4.36.dist-info/licenses/LICENSE +21 -0
synkro/formatters/qa.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""QA (Question-Answer) formatter for evaluation datasets."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from synkro.types.core import Trace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class QAFormatter:
|
|
12
|
+
"""
|
|
13
|
+
Format traces for evaluation datasets (Q&A format with ground truth).
|
|
14
|
+
|
|
15
|
+
QA format includes:
|
|
16
|
+
- question: The user's question
|
|
17
|
+
- answer: The assistant's response
|
|
18
|
+
- expected_outcome: Ground truth expected behavior
|
|
19
|
+
- ground_truth_rules: Rule IDs that should be applied
|
|
20
|
+
- difficulty: Scenario type (positive, negative, edge_case, irrelevant)
|
|
21
|
+
- category: Policy category
|
|
22
|
+
- context: Additional context for the scenario
|
|
23
|
+
- passed: Whether the response was graded as correct
|
|
24
|
+
|
|
25
|
+
Example output:
|
|
26
|
+
{
|
|
27
|
+
"question": "Can I submit a $200 expense without a receipt?",
|
|
28
|
+
"answer": "No, all expenses require receipts...",
|
|
29
|
+
"expected_outcome": "Deny - missing receipt violates R003",
|
|
30
|
+
"ground_truth_rules": ["R003", "R005"],
|
|
31
|
+
"difficulty": "negative",
|
|
32
|
+
"category": "Receipt Requirements",
|
|
33
|
+
"context": "Expense: $200, No receipt, Within 30 days",
|
|
34
|
+
"passed": true
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, include_reasoning: bool = False):
|
|
39
|
+
"""
|
|
40
|
+
Initialize the QA formatter.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
include_reasoning: If True, include reasoning chain in output
|
|
44
|
+
"""
|
|
45
|
+
self.include_reasoning = include_reasoning
|
|
46
|
+
|
|
47
|
+
def format(self, traces: list["Trace"]) -> list[dict]:
|
|
48
|
+
"""
|
|
49
|
+
Format traces as QA evaluation examples.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
traces: List of traces to format
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of QA examples with ground truth
|
|
56
|
+
"""
|
|
57
|
+
examples = []
|
|
58
|
+
|
|
59
|
+
for trace in traces:
|
|
60
|
+
example = {
|
|
61
|
+
"question": trace.user_message,
|
|
62
|
+
"answer": trace.assistant_message,
|
|
63
|
+
"expected_outcome": trace.scenario.expected_outcome or "",
|
|
64
|
+
"ground_truth_rules": trace.scenario.target_rule_ids or [],
|
|
65
|
+
"difficulty": trace.scenario.scenario_type or "unknown",
|
|
66
|
+
"category": trace.scenario.category or "",
|
|
67
|
+
"context": trace.scenario.context or "",
|
|
68
|
+
"passed": trace.grade.passed if trace.grade else None,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Optionally include reasoning
|
|
72
|
+
if self.include_reasoning:
|
|
73
|
+
example["reasoning_chain"] = trace.reasoning_chain
|
|
74
|
+
example["rules_applied"] = trace.rules_applied
|
|
75
|
+
example["rules_excluded"] = trace.rules_excluded
|
|
76
|
+
|
|
77
|
+
# Include grading feedback if available
|
|
78
|
+
if trace.grade:
|
|
79
|
+
example["grade_feedback"] = trace.grade.feedback
|
|
80
|
+
example["grade_issues"] = trace.grade.issues
|
|
81
|
+
|
|
82
|
+
examples.append(example)
|
|
83
|
+
|
|
84
|
+
return examples
|
|
85
|
+
|
|
86
|
+
def save(self, traces: list["Trace"], path: str | Path) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Save formatted traces to a JSONL file.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
traces: List of traces to save
|
|
92
|
+
path: Output file path (should end in .jsonl)
|
|
93
|
+
"""
|
|
94
|
+
path = Path(path)
|
|
95
|
+
examples = self.format(traces)
|
|
96
|
+
|
|
97
|
+
with open(path, "w") as f:
|
|
98
|
+
for example in examples:
|
|
99
|
+
f.write(json.dumps(example) + "\n")
|
|
100
|
+
|
|
101
|
+
def to_jsonl(self, traces: list["Trace"]) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Convert traces to JSONL string.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
traces: List of traces to convert
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
JSONL formatted string
|
|
110
|
+
"""
|
|
111
|
+
examples = self.format(traces)
|
|
112
|
+
return "\n".join(json.dumps(e) for e in examples)
|
synkro/formatters/sft.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""SFT (Supervised Fine-Tuning) formatter."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from synkro.types.core import Trace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SFTFormatter:
|
|
12
|
+
"""
|
|
13
|
+
Format traces for Supervised Fine-Tuning (SFT).
|
|
14
|
+
|
|
15
|
+
SFT format is a simple array of conversations, each with messages.
|
|
16
|
+
This is the standard format used by OpenAI, HuggingFace, and most
|
|
17
|
+
fine-tuning platforms.
|
|
18
|
+
|
|
19
|
+
Example output:
|
|
20
|
+
{"messages": [{"role": "system", "content": "..."}, ...]}
|
|
21
|
+
{"messages": [{"role": "system", "content": "..."}, ...]}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, include_metadata: bool = False):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the SFT formatter.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
include_metadata: If True, include trace metadata in output
|
|
30
|
+
"""
|
|
31
|
+
self.include_metadata = include_metadata
|
|
32
|
+
|
|
33
|
+
def format(self, traces: list["Trace"]) -> list[dict]:
|
|
34
|
+
"""
|
|
35
|
+
Format traces as SFT training examples.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
traces: List of traces to format
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of SFT examples (dicts with 'messages' key)
|
|
42
|
+
"""
|
|
43
|
+
examples = []
|
|
44
|
+
|
|
45
|
+
for trace in traces:
|
|
46
|
+
example = {
|
|
47
|
+
"messages": [
|
|
48
|
+
{"role": m.role, "content": m.content} for m in trace.messages
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if self.include_metadata:
|
|
53
|
+
example["metadata"] = {
|
|
54
|
+
"scenario": trace.scenario.description,
|
|
55
|
+
"category": trace.scenario.category,
|
|
56
|
+
"grade": trace.grade.model_dump() if trace.grade else None,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
examples.append(example)
|
|
60
|
+
|
|
61
|
+
return examples
|
|
62
|
+
|
|
63
|
+
def save(self, traces: list["Trace"], path: str | Path) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Save formatted traces to a JSONL file.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
traces: List of traces to save
|
|
69
|
+
path: Output file path (should end in .jsonl)
|
|
70
|
+
"""
|
|
71
|
+
path = Path(path)
|
|
72
|
+
examples = self.format(traces)
|
|
73
|
+
|
|
74
|
+
with open(path, "w") as f:
|
|
75
|
+
for example in examples:
|
|
76
|
+
f.write(json.dumps(example) + "\n")
|
|
77
|
+
|
|
78
|
+
def to_jsonl(self, traces: list["Trace"]) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Convert traces to JSONL string.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
traces: List of traces to convert
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
JSONL formatted string
|
|
87
|
+
"""
|
|
88
|
+
examples = self.format(traces)
|
|
89
|
+
return "\n".join(json.dumps(e) for e in examples)
|
|
90
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Tool Call formatter for training data."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from synkro.types.core import Trace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ToolCallFormatter:
|
|
12
|
+
"""
|
|
13
|
+
Format traces with tool calls for fine-tuning.
|
|
14
|
+
|
|
15
|
+
Outputs OpenAI function calling format compatible with most fine-tuning platforms.
|
|
16
|
+
|
|
17
|
+
Example output:
|
|
18
|
+
{
|
|
19
|
+
"messages": [
|
|
20
|
+
{"role": "system", "content": "You have access to: web_search(query)"},
|
|
21
|
+
{"role": "user", "content": "What's the weather in NYC?"},
|
|
22
|
+
{"role": "assistant", "content": null, "tool_calls": [
|
|
23
|
+
{"id": "call_1", "type": "function", "function": {"name": "web_search", "arguments": "{\\"query\\": \\"weather NYC\\"}"}}
|
|
24
|
+
]},
|
|
25
|
+
{"role": "tool", "tool_call_id": "call_1", "content": "NYC: 72°F, sunny"},
|
|
26
|
+
{"role": "assistant", "content": "The weather in NYC is currently 72°F and sunny."}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, include_metadata: bool = False):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the ToolCallFormatter.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
include_metadata: If True, include trace metadata in output
|
|
37
|
+
"""
|
|
38
|
+
self.include_metadata = include_metadata
|
|
39
|
+
|
|
40
|
+
def format(self, traces: list["Trace"]) -> list[dict]:
|
|
41
|
+
"""
|
|
42
|
+
Format traces as tool-calling training examples.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
traces: List of traces to format
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of formatted examples with tool calls
|
|
49
|
+
"""
|
|
50
|
+
examples = []
|
|
51
|
+
|
|
52
|
+
for trace in traces:
|
|
53
|
+
messages = []
|
|
54
|
+
|
|
55
|
+
for m in trace.messages:
|
|
56
|
+
msg = {"role": m.role}
|
|
57
|
+
|
|
58
|
+
# Handle content (can be None for tool-calling assistant messages)
|
|
59
|
+
if m.content is not None:
|
|
60
|
+
msg["content"] = m.content
|
|
61
|
+
elif m.role == "assistant" and m.tool_calls:
|
|
62
|
+
msg["content"] = None
|
|
63
|
+
else:
|
|
64
|
+
msg["content"] = ""
|
|
65
|
+
|
|
66
|
+
# Handle tool calls
|
|
67
|
+
if m.tool_calls:
|
|
68
|
+
msg["tool_calls"] = [
|
|
69
|
+
{
|
|
70
|
+
"id": tc.id,
|
|
71
|
+
"type": tc.type,
|
|
72
|
+
"function": {
|
|
73
|
+
"name": tc.function.name,
|
|
74
|
+
"arguments": tc.function.arguments,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for tc in m.tool_calls
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
# Handle tool response
|
|
81
|
+
if m.tool_call_id:
|
|
82
|
+
msg["tool_call_id"] = m.tool_call_id
|
|
83
|
+
|
|
84
|
+
messages.append(msg)
|
|
85
|
+
|
|
86
|
+
example = {"messages": messages}
|
|
87
|
+
|
|
88
|
+
if self.include_metadata:
|
|
89
|
+
example["metadata"] = {
|
|
90
|
+
"scenario": trace.scenario.description,
|
|
91
|
+
"category": trace.scenario.category,
|
|
92
|
+
"grade": trace.grade.model_dump() if trace.grade else None,
|
|
93
|
+
"has_tool_calls": trace.has_tool_calls,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
examples.append(example)
|
|
97
|
+
|
|
98
|
+
return examples
|
|
99
|
+
|
|
100
|
+
def save(self, traces: list["Trace"], path: str | Path) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Save formatted traces to a JSONL file.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
traces: List of traces to save
|
|
106
|
+
path: Output file path (should end in .jsonl)
|
|
107
|
+
"""
|
|
108
|
+
path = Path(path)
|
|
109
|
+
examples = self.format(traces)
|
|
110
|
+
|
|
111
|
+
with open(path, "w") as f:
|
|
112
|
+
for example in examples:
|
|
113
|
+
f.write(json.dumps(example) + "\n")
|
|
114
|
+
|
|
115
|
+
def to_jsonl(self, traces: list["Trace"]) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Convert traces to JSONL string.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
traces: List of traces to convert
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
JSONL formatted string
|
|
124
|
+
"""
|
|
125
|
+
examples = self.format(traces)
|
|
126
|
+
return "\n".join(json.dumps(e) for e in examples)
|
|
127
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Generation components for creating training data."""
|
|
2
|
+
|
|
3
|
+
from synkro.generation.generator import Generator
|
|
4
|
+
from synkro.generation.scenarios import ScenarioGenerator
|
|
5
|
+
from synkro.generation.responses import ResponseGenerator
|
|
6
|
+
from synkro.generation.planner import Planner
|
|
7
|
+
|
|
8
|
+
__all__ = ["Generator", "ScenarioGenerator", "ResponseGenerator", "Planner"]
|
|
9
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Follow-up question generation for multi-turn conversations."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from synkro.llm.client import LLM
|
|
6
|
+
from synkro.models import Model, OpenAI
|
|
7
|
+
from synkro.types.core import Message
|
|
8
|
+
from synkro.prompts.multiturn_templates import FOLLOW_UP_GENERATION_PROMPT
|
|
9
|
+
from synkro.schemas import FollowUpQuestion
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
QuestionType = Literal["clarification", "edge_case", "what_if", "specificity", "challenge"]
|
|
13
|
+
|
|
14
|
+
# Question type progression for multi-turn conversations
|
|
15
|
+
# Earlier turns focus on clarification, later turns probe deeper
|
|
16
|
+
QUESTION_TYPE_BY_TURN = {
|
|
17
|
+
1: "clarification",
|
|
18
|
+
2: "specificity",
|
|
19
|
+
3: "edge_case",
|
|
20
|
+
4: "what_if",
|
|
21
|
+
5: "challenge",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FollowUpGenerator:
|
|
26
|
+
"""
|
|
27
|
+
Generates follow-up questions for multi-turn conversations.
|
|
28
|
+
|
|
29
|
+
Uses different question types based on turn index:
|
|
30
|
+
- Turn 1: clarification - Ask for more details
|
|
31
|
+
- Turn 2: specificity - Drill into specifics
|
|
32
|
+
- Turn 3: edge_case - Probe boundary conditions
|
|
33
|
+
- Turn 4: what_if - Explore hypotheticals
|
|
34
|
+
- Turn 5+: challenge - Question reasoning
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
>>> gen = FollowUpGenerator()
|
|
38
|
+
>>> follow_up = await gen.generate(policy_text, messages, turn_index=2)
|
|
39
|
+
>>> print(follow_up.question)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, llm: LLM | None = None, model: Model = OpenAI.GPT_4O_MINI):
|
|
43
|
+
"""
|
|
44
|
+
Initialize the follow-up generator.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
llm: LLM client to use (creates one if not provided)
|
|
48
|
+
model: Model to use if creating LLM
|
|
49
|
+
"""
|
|
50
|
+
self.llm = llm or LLM(model=model)
|
|
51
|
+
|
|
52
|
+
def _select_question_type(self, turn_index: int) -> QuestionType:
|
|
53
|
+
"""
|
|
54
|
+
Select question type based on turn index.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
turn_index: Which turn this is (1-based, counting user-assistant exchanges)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Appropriate question type for this turn
|
|
61
|
+
"""
|
|
62
|
+
if turn_index in QUESTION_TYPE_BY_TURN:
|
|
63
|
+
return QUESTION_TYPE_BY_TURN[turn_index]
|
|
64
|
+
# For turns beyond 5, cycle through challenging questions
|
|
65
|
+
return "challenge"
|
|
66
|
+
|
|
67
|
+
def _format_conversation(self, messages: list[Message]) -> str:
|
|
68
|
+
"""Format conversation messages for prompt inclusion."""
|
|
69
|
+
formatted = []
|
|
70
|
+
for msg in messages:
|
|
71
|
+
role = msg.role.upper()
|
|
72
|
+
content = msg.content or "[No content]"
|
|
73
|
+
formatted.append(f"{role}: {content}")
|
|
74
|
+
return "\n\n".join(formatted)
|
|
75
|
+
|
|
76
|
+
async def generate(
|
|
77
|
+
self,
|
|
78
|
+
policy_text: str,
|
|
79
|
+
messages: list[Message],
|
|
80
|
+
turn_index: int,
|
|
81
|
+
question_type: QuestionType | None = None,
|
|
82
|
+
scenario_index: int = 0,
|
|
83
|
+
) -> FollowUpQuestion:
|
|
84
|
+
"""
|
|
85
|
+
Generate a follow-up question for the conversation.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
policy_text: The policy text for context
|
|
89
|
+
messages: Conversation messages so far
|
|
90
|
+
turn_index: Which turn this is (1-based)
|
|
91
|
+
question_type: Override auto-selected question type
|
|
92
|
+
scenario_index: Index for the scenario (default 0)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
FollowUpQuestion with the generated question
|
|
96
|
+
"""
|
|
97
|
+
# Select question type if not specified
|
|
98
|
+
if question_type is None:
|
|
99
|
+
question_type = self._select_question_type(turn_index)
|
|
100
|
+
|
|
101
|
+
# Format conversation for prompt
|
|
102
|
+
conversation = self._format_conversation(messages)
|
|
103
|
+
|
|
104
|
+
# Build prompt
|
|
105
|
+
prompt = FOLLOW_UP_GENERATION_PROMPT.format(
|
|
106
|
+
question_type=question_type,
|
|
107
|
+
conversation=conversation,
|
|
108
|
+
policy=policy_text,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
# Generate the follow-up question
|
|
113
|
+
response = await self.llm.generate(prompt)
|
|
114
|
+
question_text = response.strip()
|
|
115
|
+
|
|
116
|
+
return FollowUpQuestion(
|
|
117
|
+
index=scenario_index,
|
|
118
|
+
question=question_text,
|
|
119
|
+
question_type=question_type,
|
|
120
|
+
)
|
|
121
|
+
except Exception:
|
|
122
|
+
# Fallback generic follow-up
|
|
123
|
+
fallback_questions = {
|
|
124
|
+
"clarification": "Can you clarify that further?",
|
|
125
|
+
"edge_case": "What about edge cases?",
|
|
126
|
+
"what_if": "What if the situation changes?",
|
|
127
|
+
"specificity": "Can you be more specific?",
|
|
128
|
+
"challenge": "Why is that the best approach?",
|
|
129
|
+
}
|
|
130
|
+
return FollowUpQuestion(
|
|
131
|
+
index=scenario_index,
|
|
132
|
+
question=fallback_questions.get(question_type, "Can you elaborate?"),
|
|
133
|
+
question_type=question_type,
|
|
134
|
+
)
|