gaard-core 0.2.4__tar.gz → 0.2.7__tar.gz
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.
- {gaard_core-0.2.4 → gaard_core-0.2.7}/PKG-INFO +2 -2
- {gaard_core-0.2.4 → gaard_core-0.2.7}/pyproject.toml +2 -2
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/conversation_context/llm_classifier.py +65 -3
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/conversation_context/mock_classifier.py +4 -0
- gaard_core-0.2.7/src/gaard_core/prompt_compiler/conversation_context_prompt.py +121 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/query_pipeline/models.py +2 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core.egg-info/PKG-INFO +2 -2
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core.egg-info/requires.txt +1 -1
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_conversation_context_classifier.py +55 -0
- gaard_core-0.2.4/src/gaard_core/prompt_compiler/conversation_context_prompt.py +0 -68
- {gaard_core-0.2.4 → gaard_core-0.2.7}/README.md +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/setup.cfg +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/__init__.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/conversation_context/__init__.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/errors.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/execution/__init__.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/execution/mock_executor.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/json_utils.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/llm_output.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/__init__.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/intent_classification_prompt.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/models.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/result_classification_prompt.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/result_interpretation_prompt.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/schema_formatter.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/sql_generation_prompt.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/py.typed +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/query_intent/__init__.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/query_intent/llm_classifier.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/query_intent/mock_classifier.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/query_pipeline/__init__.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/query_pipeline/llm_sql_generator.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/query_pipeline/mock_sql_generator.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/query_pipeline/pipeline.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/result_classifier/__init__.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/result_classifier/llm_classifier.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/result_classifier/mock_classifier.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/result_interpreter/__init__.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/result_interpreter/llm_interpreter.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/result_interpreter/mock_interpreter.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/schema/__init__.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/schema/cache.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/schema/context.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/schema/models.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/sql_validator/__init__.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/sql_validator/select_only.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core.egg-info/SOURCES.txt +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core.egg-info/dependency_links.txt +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core.egg-info/top_level.txt +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_json_utils.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_llm_output.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_llm_query_intent_classifier.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_llm_result_classifier.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_llm_result_interpreter.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_llm_sql_generator.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_query_pipeline.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_result_classification_prompt_compiler.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_result_interpretation_prompt_compiler.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_schema_context_cache.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_schema_context_service.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_schema_prompt_formatter.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_sql_generation_prompt_compiler.py +0 -0
- {gaard_core-0.2.4 → gaard_core-0.2.7}/tests/test_sql_validator.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gaard-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Core GAARD query pipeline, prompt compiler, policies and SQL validation
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.
|
|
7
|
+
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.7
|
|
8
8
|
Requires-Dist: pydantic>=2.7.0
|
|
9
9
|
Requires-Dist: sqlglot>=25.0.0
|
|
10
10
|
Provides-Extra: dev
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gaard-core"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.7"
|
|
8
8
|
description = "Core GAARD query pipeline, prompt compiler, policies and SQL validation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"gaard-plugin-api>=0.2.
|
|
12
|
+
"gaard-plugin-api>=0.2.7,<0.3.0",
|
|
13
13
|
"pydantic>=2.7.0",
|
|
14
14
|
"sqlglot>=25.0.0",
|
|
15
15
|
]
|
|
@@ -58,7 +58,32 @@ class LlmConversationContextClassifier:
|
|
|
58
58
|
)
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
classification = parse_conversation_context_classification(response.content)
|
|
62
|
+
if (
|
|
63
|
+
classification.decision == ConversationContextDecision.NEW_TOPIC
|
|
64
|
+
and not classification.standalone_question
|
|
65
|
+
):
|
|
66
|
+
classification = classification.model_copy(
|
|
67
|
+
update={"standalone_question": request.question}
|
|
68
|
+
)
|
|
69
|
+
if (
|
|
70
|
+
classification.decision == ConversationContextDecision.FOLLOW_UP
|
|
71
|
+
and not classification.standalone_question
|
|
72
|
+
and classification.model_response.get("current_question_is_standalone") is True
|
|
73
|
+
):
|
|
74
|
+
classification = classification.model_copy(
|
|
75
|
+
update={"standalone_question": request.question}
|
|
76
|
+
)
|
|
77
|
+
return classification.model_copy(
|
|
78
|
+
update={
|
|
79
|
+
"prompt": {
|
|
80
|
+
"system_prompt": compiled_prompt.system_prompt,
|
|
81
|
+
"user_prompt": compiled_prompt.user_prompt,
|
|
82
|
+
"metadata": compiled_prompt.metadata,
|
|
83
|
+
},
|
|
84
|
+
"source": "llm",
|
|
85
|
+
}
|
|
86
|
+
)
|
|
62
87
|
|
|
63
88
|
|
|
64
89
|
def parse_conversation_context_classification(
|
|
@@ -80,8 +105,12 @@ def parse_conversation_context_classification(
|
|
|
80
105
|
if not isinstance(payload, dict):
|
|
81
106
|
return ConversationContextClassification()
|
|
82
107
|
|
|
108
|
+
decision = parse_conversation_context_decision(
|
|
109
|
+
payload.get("decision"),
|
|
110
|
+
continuation_value=payload.get("is_continuation"),
|
|
111
|
+
)
|
|
83
112
|
return ConversationContextClassification(
|
|
84
|
-
decision=
|
|
113
|
+
decision=decision,
|
|
85
114
|
confidence=parse_confidence(payload.get("confidence")),
|
|
86
115
|
standalone_question=str(payload.get("standalone_question") or "").strip(),
|
|
87
116
|
reason=str(payload.get("reason") or ""),
|
|
@@ -89,15 +118,30 @@ def parse_conversation_context_classification(
|
|
|
89
118
|
)
|
|
90
119
|
|
|
91
120
|
|
|
92
|
-
def parse_conversation_context_decision(
|
|
121
|
+
def parse_conversation_context_decision(
|
|
122
|
+
value: object,
|
|
123
|
+
*,
|
|
124
|
+
continuation_value: object | None = None,
|
|
125
|
+
) -> ConversationContextDecision:
|
|
126
|
+
if value is None:
|
|
127
|
+
continuation_decision = parse_continuation_decision(continuation_value)
|
|
128
|
+
if continuation_decision is not None:
|
|
129
|
+
return continuation_decision
|
|
130
|
+
|
|
93
131
|
if not isinstance(value, str):
|
|
94
132
|
return ConversationContextDecision.AMBIGUOUS
|
|
95
133
|
|
|
96
134
|
normalized = value.strip().lower().replace(" ", "_").replace("-", "_")
|
|
97
135
|
aliases = {
|
|
136
|
+
"no": ConversationContextDecision.NEW_TOPIC,
|
|
137
|
+
"nie": ConversationContextDecision.NEW_TOPIC,
|
|
138
|
+
"false": ConversationContextDecision.NEW_TOPIC,
|
|
98
139
|
"new": ConversationContextDecision.NEW_TOPIC,
|
|
99
140
|
"new_question": ConversationContextDecision.NEW_TOPIC,
|
|
100
141
|
"newtopic": ConversationContextDecision.NEW_TOPIC,
|
|
142
|
+
"yes": ConversationContextDecision.FOLLOW_UP,
|
|
143
|
+
"tak": ConversationContextDecision.FOLLOW_UP,
|
|
144
|
+
"true": ConversationContextDecision.FOLLOW_UP,
|
|
101
145
|
"continue": ConversationContextDecision.FOLLOW_UP,
|
|
102
146
|
"continuation": ConversationContextDecision.FOLLOW_UP,
|
|
103
147
|
"followup": ConversationContextDecision.FOLLOW_UP,
|
|
@@ -115,6 +159,24 @@ def parse_conversation_context_decision(value: object) -> ConversationContextDec
|
|
|
115
159
|
return ConversationContextDecision.AMBIGUOUS
|
|
116
160
|
|
|
117
161
|
|
|
162
|
+
def parse_continuation_decision(value: object) -> ConversationContextDecision | None:
|
|
163
|
+
if isinstance(value, bool):
|
|
164
|
+
return (
|
|
165
|
+
ConversationContextDecision.FOLLOW_UP
|
|
166
|
+
if value
|
|
167
|
+
else ConversationContextDecision.NEW_TOPIC
|
|
168
|
+
)
|
|
169
|
+
if not isinstance(value, str):
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
normalized = value.strip().lower()
|
|
173
|
+
if normalized in {"yes", "y", "true", "tak", "t"}:
|
|
174
|
+
return ConversationContextDecision.FOLLOW_UP
|
|
175
|
+
if normalized in {"no", "n", "false", "nie"}:
|
|
176
|
+
return ConversationContextDecision.NEW_TOPIC
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
118
180
|
def parse_confidence(value: object) -> float:
|
|
119
181
|
try:
|
|
120
182
|
confidence = float(cast(Any, value))
|
{gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/conversation_context/mock_classifier.py
RENAMED
|
@@ -73,6 +73,7 @@ class MockConversationContextClassifier:
|
|
|
73
73
|
confidence=1.0,
|
|
74
74
|
standalone_question=request.question,
|
|
75
75
|
reason="No previous turns are available.",
|
|
76
|
+
source="mock",
|
|
76
77
|
)
|
|
77
78
|
|
|
78
79
|
question = request.question.strip()
|
|
@@ -85,6 +86,7 @@ class MockConversationContextClassifier:
|
|
|
85
86
|
confidence=0.8,
|
|
86
87
|
standalone_question=standalone,
|
|
87
88
|
reason="The question appears to continue the previous data question.",
|
|
89
|
+
source="mock",
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
if normalized in AMBIGUOUS_TERMS or re.fullmatch(
|
|
@@ -95,6 +97,7 @@ class MockConversationContextClassifier:
|
|
|
95
97
|
confidence=0.45,
|
|
96
98
|
standalone_question="",
|
|
97
99
|
reason="The question is too short to safely resolve from context.",
|
|
100
|
+
source="mock",
|
|
98
101
|
)
|
|
99
102
|
|
|
100
103
|
return ConversationContextClassification(
|
|
@@ -102,6 +105,7 @@ class MockConversationContextClassifier:
|
|
|
102
105
|
confidence=0.9,
|
|
103
106
|
standalone_question=question,
|
|
104
107
|
reason="The question is self-contained.",
|
|
108
|
+
source="mock",
|
|
105
109
|
)
|
|
106
110
|
|
|
107
111
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from gaard_core.json_utils import json_dumps
|
|
4
|
+
from gaard_core.prompt_compiler.models import CompiledPrompt
|
|
5
|
+
from gaard_core.query_pipeline.models import ConversationContextDecision, QueryRequest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConversationContextPromptCompiler:
|
|
9
|
+
def compile(
|
|
10
|
+
self,
|
|
11
|
+
request: QueryRequest,
|
|
12
|
+
conversation_context: dict[str, Any],
|
|
13
|
+
) -> CompiledPrompt:
|
|
14
|
+
recent_turns = self._recent_turns(conversation_context)
|
|
15
|
+
payload = {
|
|
16
|
+
"turn_t_minus_2": recent_turns[0] if len(recent_turns) == 2 else {},
|
|
17
|
+
"turn_t_minus_1": recent_turns[-1] if recent_turns else {},
|
|
18
|
+
"turn_t": {
|
|
19
|
+
"question": request.question,
|
|
20
|
+
"datasource_id": request.datasource_id,
|
|
21
|
+
"datasource_ids": request.datasource_ids,
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return CompiledPrompt(
|
|
26
|
+
system_prompt=self._build_system_prompt(),
|
|
27
|
+
user_prompt=self._build_user_prompt(payload),
|
|
28
|
+
metadata={
|
|
29
|
+
"allowed_decisions": [item.value for item in ConversationContextDecision],
|
|
30
|
+
"decision_task": "logical_continuation_yes_no",
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def _recent_turns(self, conversation_context: dict[str, Any]) -> list[dict[str, Any]]:
|
|
35
|
+
turns = [
|
|
36
|
+
turn for turn in conversation_context.get("turns", []) if isinstance(turn, dict)
|
|
37
|
+
][-2:]
|
|
38
|
+
labels = ["t-2", "t-1"] if len(turns) == 2 else ["t-1"]
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
"label": label,
|
|
42
|
+
"question": str(turn.get("question") or ""),
|
|
43
|
+
"standalone_question": str(turn.get("standalone_question") or ""),
|
|
44
|
+
"answer": str(turn.get("answer") or ""),
|
|
45
|
+
"sql": str(turn.get("sql") or ""),
|
|
46
|
+
"context_decision": str(turn.get("context_decision") or ""),
|
|
47
|
+
"context_reason": str(turn.get("context_reason") or ""),
|
|
48
|
+
}
|
|
49
|
+
for label, turn in zip(labels, turns, strict=False)
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
def _build_system_prompt(self) -> str:
|
|
53
|
+
return """You are GAARD Conversation Context Classification.
|
|
54
|
+
|
|
55
|
+
Your task is to decide whether the current user data question (turn t) is a
|
|
56
|
+
logical continuation of the recent conversation turns (t-2 and t-1).
|
|
57
|
+
|
|
58
|
+
Use the previous question-answer pairs as conversation evidence. Do not classify
|
|
59
|
+
by rigid prefix or keyword rules. Decide semantically whether turn t depends on,
|
|
60
|
+
compares with, narrows, broadens, or otherwise continues the immediately
|
|
61
|
+
preceding analytical thread.
|
|
62
|
+
|
|
63
|
+
Allowed decisions:
|
|
64
|
+
- new_topic: answer "no" to logical continuation. The question starts a new analytical thread.
|
|
65
|
+
- follow_up: answer "yes" to logical continuation. The question continues the
|
|
66
|
+
recent thread and can be executed safely.
|
|
67
|
+
- ambiguous: the question appears to continue the thread, but required entities,
|
|
68
|
+
filters, date ranges, or datasource scope cannot be inferred safely.
|
|
69
|
+
|
|
70
|
+
Decision rules:
|
|
71
|
+
1. First answer the yes/no question: is turn t a logical continuation of t-1/t-2?
|
|
72
|
+
2. A question can be a logical continuation even when it is already
|
|
73
|
+
self-contained. In that case use follow_up, set
|
|
74
|
+
current_question_is_standalone to true, and set standalone_question to the
|
|
75
|
+
current question.
|
|
76
|
+
3. If the answer is no, use new_topic and set standalone_question to the current question.
|
|
77
|
+
4. If the answer is yes and the current question is elliptical, rewrite it as a
|
|
78
|
+
standalone data question using t-1/t-2.
|
|
79
|
+
5. Use ambiguous only when the answer is yes but the continuation cannot be
|
|
80
|
+
rewritten or executed safely without asking the user.
|
|
81
|
+
6. Treat detail/projection requests about the previous result as follow_up when
|
|
82
|
+
the previous turn defines the result set. If the previous question counted,
|
|
83
|
+
grouped, or filtered records and the user now asks for descriptions, names,
|
|
84
|
+
statuses, fields, details, or values for those same records, rewrite by
|
|
85
|
+
preserving the previous filters/date range/datasource and changing only the
|
|
86
|
+
returned fields.
|
|
87
|
+
7. Do not mark a projection/detail follow-up ambiguous merely because the
|
|
88
|
+
previous answer did not expose row ids. The previous standalone question and
|
|
89
|
+
SQL are enough context for the next SQL generation step.
|
|
90
|
+
8. Never include rows or sensitive data in the standalone question.
|
|
91
|
+
|
|
92
|
+
Output rules:
|
|
93
|
+
- Return only a JSON object.
|
|
94
|
+
- Do not include markdown.
|
|
95
|
+
- Do not include reasoning outside the JSON.
|
|
96
|
+
- Do not include <think> blocks.
|
|
97
|
+
- Use exactly this JSON shape:
|
|
98
|
+
{
|
|
99
|
+
"is_continuation": false,
|
|
100
|
+
"decision": "new_topic",
|
|
101
|
+
"current_question_is_standalone": true,
|
|
102
|
+
"confidence": 0.0,
|
|
103
|
+
"standalone_question": "rewritten or current question",
|
|
104
|
+
"reason": "short reason"
|
|
105
|
+
}
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def _build_user_prompt(self, payload: dict[str, Any]) -> str:
|
|
109
|
+
return f"""Decide whether turn t is a logical continuation of turns t-2 and t-1.
|
|
110
|
+
|
|
111
|
+
Input JSON:
|
|
112
|
+
{json_dumps(payload, ensure_ascii=False, indent=2)}
|
|
113
|
+
|
|
114
|
+
Return one JSON object with:
|
|
115
|
+
- is_continuation: boolean yes/no answer to the logical-continuation question
|
|
116
|
+
- decision: one of {", ".join(item.value for item in ConversationContextDecision)}
|
|
117
|
+
- current_question_is_standalone: boolean
|
|
118
|
+
- confidence: number from 0 to 1
|
|
119
|
+
- standalone_question: required for follow_up and new_topic; empty only for ambiguous
|
|
120
|
+
- reason: short explanation
|
|
121
|
+
"""
|
|
@@ -44,6 +44,8 @@ class ConversationContextClassification(BaseModel):
|
|
|
44
44
|
standalone_question: str = ""
|
|
45
45
|
reason: str = ""
|
|
46
46
|
model_response: dict[str, Any] = Field(default_factory=dict)
|
|
47
|
+
prompt: dict[str, Any] = Field(default_factory=dict)
|
|
48
|
+
source: str = ""
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
class QueryRequest(BaseModel):
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gaard-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Core GAARD query pipeline, prompt compiler, policies and SQL validation
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.
|
|
7
|
+
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.7
|
|
8
8
|
Requires-Dist: pydantic>=2.7.0
|
|
9
9
|
Requires-Dist: sqlglot>=25.0.0
|
|
10
10
|
Provides-Extra: dev
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from gaard_core.conversation_context.llm_classifier import (
|
|
2
|
+
LlmConversationContextClassifier,
|
|
2
3
|
parse_conversation_context_classification,
|
|
3
4
|
)
|
|
4
5
|
from gaard_core.conversation_context.mock_classifier import MockConversationContextClassifier
|
|
5
6
|
from gaard_core.query_pipeline.models import ConversationContextDecision, QueryRequest
|
|
7
|
+
from gaard_llm.providers.models import ChatCompletionResponse
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def test_parse_conversation_context_classification_handles_aliases_and_invalid_values() -> None:
|
|
@@ -17,6 +19,59 @@ def test_parse_conversation_context_classification_handles_aliases_and_invalid_v
|
|
|
17
19
|
parse_conversation_context_classification('{"decision":"surprising"}').decision
|
|
18
20
|
== ConversationContextDecision.AMBIGUOUS
|
|
19
21
|
)
|
|
22
|
+
assert (
|
|
23
|
+
parse_conversation_context_classification('{"is_continuation":false}').decision
|
|
24
|
+
== ConversationContextDecision.NEW_TOPIC
|
|
25
|
+
)
|
|
26
|
+
assert (
|
|
27
|
+
parse_conversation_context_classification('{"is_continuation":true}').decision
|
|
28
|
+
== ConversationContextDecision.FOLLOW_UP
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_llm_conversation_context_classifier_exposes_prompt_and_standalone_follow_up() -> None:
|
|
33
|
+
class FakeClient:
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
self.request = None
|
|
36
|
+
|
|
37
|
+
def create_chat_completion(self, request):
|
|
38
|
+
self.request = request
|
|
39
|
+
return ChatCompletionResponse(
|
|
40
|
+
content=(
|
|
41
|
+
'{"is_continuation":true,"decision":"follow_up",'
|
|
42
|
+
'"current_question_is_standalone":true,"confidence":0.91,'
|
|
43
|
+
'"standalone_question":"","reason":"Same metric, new period."}'
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
client = FakeClient()
|
|
48
|
+
classifier = LlmConversationContextClassifier(
|
|
49
|
+
client=client,
|
|
50
|
+
model="test-model",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
classification = classifier.classify(
|
|
54
|
+
QueryRequest(question="ilu pacjentów przyjęto w tym tygodniu"),
|
|
55
|
+
{
|
|
56
|
+
"turns": [
|
|
57
|
+
{
|
|
58
|
+
"question": "ilu pacjentów było przyjętych tydzień temu",
|
|
59
|
+
"answer": "12",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"question": "a dwa tygodnie temu?",
|
|
63
|
+
"standalone_question": "ilu pacjentów było przyjętych dwa tygodnie temu",
|
|
64
|
+
"answer": "9",
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert classification.decision == ConversationContextDecision.FOLLOW_UP
|
|
71
|
+
assert classification.standalone_question == "ilu pacjentów przyjęto w tym tygodniu"
|
|
72
|
+
assert classification.source == "llm"
|
|
73
|
+
assert "turn_t_minus_1" in classification.prompt["user_prompt"]
|
|
74
|
+
assert "logical continuation" in classification.prompt["system_prompt"]
|
|
20
75
|
|
|
21
76
|
|
|
22
77
|
def test_mock_conversation_context_classifier_rewrites_simple_follow_up() -> None:
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
from gaard_core.json_utils import json_dumps
|
|
4
|
-
from gaard_core.prompt_compiler.models import CompiledPrompt
|
|
5
|
-
from gaard_core.query_pipeline.models import ConversationContextDecision, QueryRequest
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ConversationContextPromptCompiler:
|
|
9
|
-
def compile(
|
|
10
|
-
self,
|
|
11
|
-
request: QueryRequest,
|
|
12
|
-
conversation_context: dict[str, Any],
|
|
13
|
-
) -> CompiledPrompt:
|
|
14
|
-
payload = {
|
|
15
|
-
"question": request.question,
|
|
16
|
-
"datasource_id": request.datasource_id,
|
|
17
|
-
"datasource_ids": request.datasource_ids,
|
|
18
|
-
"conversation_context": conversation_context,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return CompiledPrompt(
|
|
22
|
-
system_prompt=self._build_system_prompt(),
|
|
23
|
-
user_prompt=self._build_user_prompt(payload),
|
|
24
|
-
metadata={
|
|
25
|
-
"allowed_decisions": [item.value for item in ConversationContextDecision],
|
|
26
|
-
},
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
def _build_system_prompt(self) -> str:
|
|
30
|
-
return """You are GAARD Conversation Context Classification.
|
|
31
|
-
|
|
32
|
-
Your task is to decide whether a user's new data question starts a new topic, continues the current conversation, or is too ambiguous to safely continue.
|
|
33
|
-
|
|
34
|
-
Allowed decisions:
|
|
35
|
-
- new_topic: the question stands on its own or changes topic.
|
|
36
|
-
- follow_up: the question clearly depends on previous turns and can be rewritten as a standalone data question.
|
|
37
|
-
- ambiguous: the question appears to depend on prior context but cannot be rewritten safely.
|
|
38
|
-
|
|
39
|
-
Decision rules:
|
|
40
|
-
1. Use follow_up only when the current question can be resolved from the supplied compact conversation context.
|
|
41
|
-
2. Use new_topic when the question is already self-contained.
|
|
42
|
-
3. Use ambiguous instead of guessing when key entities, metrics, filters, date ranges, or datasource scope cannot be inferred.
|
|
43
|
-
4. Treat detail/projection requests about the previous result as follow_up when the previous turn defines the result set. If the previous question counted, grouped, or filtered records and the user now asks for descriptions, names, statuses, fields, details, or values for those same records, rewrite by preserving the previous filters/date range/datasource and changing only the returned fields.
|
|
44
|
-
5. Do not mark a projection/detail follow-up ambiguous merely because the previous answer did not expose row ids. The previous standalone question and SQL are enough context for the next SQL generation step.
|
|
45
|
-
6. Use ambiguous when the user asks for one specific record but the context indicates multiple records and no selector is provided.
|
|
46
|
-
7. Never include rows or sensitive data in the standalone question.
|
|
47
|
-
|
|
48
|
-
Output rules:
|
|
49
|
-
- Return only a JSON object.
|
|
50
|
-
- Do not include markdown.
|
|
51
|
-
- Do not include reasoning outside the JSON.
|
|
52
|
-
- Do not include <think> blocks.
|
|
53
|
-
- Use exactly this JSON shape:
|
|
54
|
-
{"decision":"new_topic","confidence":0.0,"standalone_question":"rewritten question or empty","reason":"short reason"}
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
def _build_user_prompt(self, payload: dict[str, Any]) -> str:
|
|
58
|
-
return f"""Classify this new user question against the compact conversation context.
|
|
59
|
-
|
|
60
|
-
Input JSON:
|
|
61
|
-
{json_dumps(payload, ensure_ascii=False, indent=2)}
|
|
62
|
-
|
|
63
|
-
Return one JSON object with:
|
|
64
|
-
- decision: one of {", ".join(item.value for item in ConversationContextDecision)}
|
|
65
|
-
- confidence: number from 0 to 1
|
|
66
|
-
- standalone_question: required for follow_up, otherwise empty
|
|
67
|
-
- reason: short explanation
|
|
68
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/intent_classification_prompt.py
RENAMED
|
File without changes
|
|
File without changes
|
{gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/result_classification_prompt.py
RENAMED
|
File without changes
|
{gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/result_interpretation_prompt.py
RENAMED
|
File without changes
|
|
File without changes
|
{gaard_core-0.2.4 → gaard_core-0.2.7}/src/gaard_core/prompt_compiler/sql_generation_prompt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|