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,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)
@@ -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
+ )