gaard-api 0.2.6__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_api-0.2.6/src/gaard_api.egg-info → gaard_api-0.2.7}/PKG-INFO +5 -5
- {gaard_api-0.2.6 → gaard_api-0.2.7}/pyproject.toml +5 -5
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin/defaults.py +79 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin/prompt_runtime.py +74 -1
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/api/v1/dashboards.py +57 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/api/v1/query.py +30 -13
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/conversations.py +8 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/main.py +1 -1
- {gaard_api-0.2.6 → gaard_api-0.2.7/src/gaard_api.egg-info}/PKG-INFO +5 -5
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api.egg-info/requires.txt +4 -4
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_admin_api.py +65 -1
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_conversation_api.py +77 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_prompt_runtime.py +39 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/MANIFEST.in +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/README.md +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/setup.cfg +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/__init__.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin/__init__.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin/database.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin/models.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin/security.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin/services.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin-web/assets/getgaard.svg +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin-web/assets/main.js +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin-web/assets/styles.css +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack-all.js +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack.min.css +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin-web/index.html +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin-web/package.json +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/api/__init__.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/api/v1/__init__.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/api/v1/admin.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/api/v1/analysis.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/api/v1/prompts.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/api/v1/schema.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/api_registry.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/auth_dependencies.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/auth_hooks.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/cli.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/cli_commands.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/core/__init__.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/core/error_handlers.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/core/schema_cache.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/core/settings.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/example_data/__init__.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/example_database.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/extension_services.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/extensions.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/license.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/package_updates.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/py.typed +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/query_hooks.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/server_cli.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/siem.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/tls_http.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api.egg-info/SOURCES.txt +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api.egg-info/dependency_links.txt +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api.egg-info/entry_points.txt +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api.egg-info/top_level.txt +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_analysis_api.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_api_registry.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_error_handlers.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_license_api.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_license_service.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_query_error_messages.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_server_cli.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_settings.py +0 -0
- {gaard_api-0.2.6 → gaard_api-0.2.7}/tests/test_tls_http.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gaard-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: GAARD backend web services providing admin interface
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: gaard-core==0.2.
|
|
8
|
-
Requires-Dist: gaard-connectors==0.2.
|
|
9
|
-
Requires-Dist: gaard-llm==0.2.
|
|
10
|
-
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.
|
|
7
|
+
Requires-Dist: gaard-core==0.2.7
|
|
8
|
+
Requires-Dist: gaard-connectors==0.2.7
|
|
9
|
+
Requires-Dist: gaard-llm==0.2.7
|
|
10
|
+
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.7
|
|
11
11
|
Requires-Dist: fastapi>=0.111.0
|
|
12
12
|
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
13
13
|
Requires-Dist: pydantic>=2.7.0
|
|
@@ -4,16 +4,16 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gaard-api"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.7"
|
|
8
8
|
description = "GAARD backend web services providing admin interface"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
|
|
12
12
|
dependencies = [
|
|
13
|
-
"gaard-core==0.2.
|
|
14
|
-
"gaard-connectors==0.2.
|
|
15
|
-
"gaard-llm==0.2.
|
|
16
|
-
"gaard-plugin-api>=0.2.
|
|
13
|
+
"gaard-core==0.2.7",
|
|
14
|
+
"gaard-connectors==0.2.7",
|
|
15
|
+
"gaard-llm==0.2.7",
|
|
16
|
+
"gaard-plugin-api>=0.2.7,<0.3.0",
|
|
17
17
|
"fastapi>=0.111.0",
|
|
18
18
|
"uvicorn[standard]>=0.30.0",
|
|
19
19
|
"pydantic>=2.7.0",
|
|
@@ -86,6 +86,75 @@ Return one JSON object with:
|
|
|
86
86
|
- reason: short explanation
|
|
87
87
|
"""
|
|
88
88
|
|
|
89
|
+
DEFAULT_CONVERSATION_CONTEXT_CLASSIFICATION_SYSTEM_PROMPT = """You are GAARD Conversation Context Classification.
|
|
90
|
+
|
|
91
|
+
Your task is to decide whether the current user data question (turn t) is a
|
|
92
|
+
logical continuation of the recent conversation turns (t-2 and t-1).
|
|
93
|
+
|
|
94
|
+
Use the previous question-answer pairs as conversation evidence. Do not classify
|
|
95
|
+
by rigid prefix or keyword rules. Decide semantically whether turn t depends on,
|
|
96
|
+
compares with, narrows, broadens, or otherwise continues the immediately
|
|
97
|
+
preceding analytical thread.
|
|
98
|
+
|
|
99
|
+
Allowed decisions:
|
|
100
|
+
- new_topic: answer "no" to logical continuation. The question starts a new analytical thread.
|
|
101
|
+
- follow_up: answer "yes" to logical continuation. The question continues the
|
|
102
|
+
recent thread and can be executed safely.
|
|
103
|
+
- ambiguous: the question appears to continue the thread, but required entities,
|
|
104
|
+
filters, date ranges, or datasource scope cannot be inferred safely.
|
|
105
|
+
|
|
106
|
+
Decision rules:
|
|
107
|
+
1. First answer the yes/no question: is turn t a logical continuation of t-1/t-2?
|
|
108
|
+
2. A question can be a logical continuation even when it is already
|
|
109
|
+
self-contained. In that case use follow_up, set
|
|
110
|
+
current_question_is_standalone to true, and set standalone_question to the
|
|
111
|
+
current question.
|
|
112
|
+
3. If the answer is no, use new_topic and set standalone_question to the current question.
|
|
113
|
+
4. If the answer is yes and the current question is elliptical, rewrite it as a
|
|
114
|
+
standalone data question using t-1/t-2.
|
|
115
|
+
5. Use ambiguous only when the answer is yes but the continuation cannot be
|
|
116
|
+
rewritten or executed safely without asking the user.
|
|
117
|
+
6. Treat detail/projection requests about the previous result as follow_up when
|
|
118
|
+
the previous turn defines the result set. If the previous question counted,
|
|
119
|
+
grouped, or filtered records and the user now asks for descriptions, names,
|
|
120
|
+
statuses, fields, details, or values for those same records, rewrite by
|
|
121
|
+
preserving the previous filters/date range/datasource and changing only the
|
|
122
|
+
returned fields.
|
|
123
|
+
7. Do not mark a projection/detail follow-up ambiguous merely because the
|
|
124
|
+
previous answer did not expose row ids. The previous standalone question and
|
|
125
|
+
SQL are enough context for the next SQL generation step.
|
|
126
|
+
8. Never include rows or sensitive data in the standalone question.
|
|
127
|
+
|
|
128
|
+
Output rules:
|
|
129
|
+
- Return only a JSON object.
|
|
130
|
+
- Do not include markdown.
|
|
131
|
+
- Do not include reasoning outside the JSON.
|
|
132
|
+
- Do not include <think> blocks.
|
|
133
|
+
- Use exactly this JSON shape:
|
|
134
|
+
{
|
|
135
|
+
"is_continuation": false,
|
|
136
|
+
"decision": "new_topic",
|
|
137
|
+
"current_question_is_standalone": true,
|
|
138
|
+
"confidence": 0.0,
|
|
139
|
+
"standalone_question": "rewritten or current question",
|
|
140
|
+
"reason": "short reason"
|
|
141
|
+
}
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
DEFAULT_CONVERSATION_CONTEXT_CLASSIFICATION_USER_PROMPT = """Decide whether turn t is a logical continuation of turns t-2 and t-1.
|
|
145
|
+
|
|
146
|
+
Input JSON:
|
|
147
|
+
{payload}
|
|
148
|
+
|
|
149
|
+
Return one JSON object with:
|
|
150
|
+
- is_continuation: boolean yes/no answer to the logical-continuation question
|
|
151
|
+
- decision: one of new_topic, follow_up, ambiguous
|
|
152
|
+
- current_question_is_standalone: boolean
|
|
153
|
+
- confidence: number from 0 to 1
|
|
154
|
+
- standalone_question: required for follow_up and new_topic; empty only for ambiguous
|
|
155
|
+
- reason: short explanation
|
|
156
|
+
"""
|
|
157
|
+
|
|
89
158
|
DEFAULT_RESULT_INTERPRETATION_SYSTEM_PROMPT = """You are GAARD Data Result Interpreter.
|
|
90
159
|
|
|
91
160
|
Your task is to explain SQL query results to the user.
|
|
@@ -187,6 +256,16 @@ DEFAULT_PROMPTS = [
|
|
|
187
256
|
"system_prompt": DEFAULT_SQL_GENERATION_SYSTEM_PROMPT,
|
|
188
257
|
"user_prompt_template": DEFAULT_SQL_GENERATION_USER_PROMPT,
|
|
189
258
|
},
|
|
259
|
+
{
|
|
260
|
+
"prompt_key": "conversation_context_classification",
|
|
261
|
+
"name": "Conversation context classification",
|
|
262
|
+
"description": (
|
|
263
|
+
"Decides whether the current question is a logical continuation "
|
|
264
|
+
"of recent conversation turns."
|
|
265
|
+
),
|
|
266
|
+
"system_prompt": DEFAULT_CONVERSATION_CONTEXT_CLASSIFICATION_SYSTEM_PROMPT,
|
|
267
|
+
"user_prompt_template": DEFAULT_CONVERSATION_CONTEXT_CLASSIFICATION_USER_PROMPT,
|
|
268
|
+
},
|
|
190
269
|
{
|
|
191
270
|
"prompt_key": "result_interpretation",
|
|
192
271
|
"name": "Result interpretation",
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
1
3
|
from gaard_core.errors import ConfigurationError
|
|
2
4
|
from gaard_core.json_utils import json_dumps
|
|
3
5
|
from gaard_core.prompt_compiler.models import CompiledPrompt, SqlGenerationPromptRequest
|
|
4
6
|
from gaard_core.prompt_compiler.schema_formatter import SchemaPromptFormatter
|
|
5
|
-
from gaard_core.query_pipeline.models import
|
|
7
|
+
from gaard_core.query_pipeline.models import (
|
|
8
|
+
ConversationContextDecision,
|
|
9
|
+
QueryRequest,
|
|
10
|
+
QueryResult,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
13
|
from gaard_api.admin.models import PromptTemplate
|
|
8
14
|
from gaard_api.admin.services import get_active_prompt_template_safe
|
|
@@ -85,6 +91,62 @@ class MetadataIntentClassificationPromptCompiler:
|
|
|
85
91
|
)
|
|
86
92
|
|
|
87
93
|
|
|
94
|
+
class MetadataConversationContextPromptCompiler:
|
|
95
|
+
def __init__(self, prompt_template: PromptTemplate) -> None:
|
|
96
|
+
self.prompt_template = prompt_template
|
|
97
|
+
|
|
98
|
+
def compile(
|
|
99
|
+
self,
|
|
100
|
+
request: QueryRequest,
|
|
101
|
+
conversation_context: dict[str, Any],
|
|
102
|
+
) -> CompiledPrompt:
|
|
103
|
+
recent_turns = self._recent_turns(conversation_context)
|
|
104
|
+
payload = {
|
|
105
|
+
"turn_t_minus_2": recent_turns[0] if len(recent_turns) == 2 else {},
|
|
106
|
+
"turn_t_minus_1": recent_turns[-1] if recent_turns else {},
|
|
107
|
+
"turn_t": {
|
|
108
|
+
"question": request.question,
|
|
109
|
+
"datasource_id": request.datasource_id,
|
|
110
|
+
"datasource_ids": request.datasource_ids,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
payload_json = json_dumps(payload, ensure_ascii=False, indent=2)
|
|
114
|
+
|
|
115
|
+
return CompiledPrompt(
|
|
116
|
+
system_prompt=self.prompt_template.system_prompt,
|
|
117
|
+
user_prompt=self.prompt_template.user_prompt_template.format(
|
|
118
|
+
payload=payload_json,
|
|
119
|
+
question=request.question,
|
|
120
|
+
datasource_id=request.datasource_id,
|
|
121
|
+
datasource_ids=json_dumps(request.datasource_ids, ensure_ascii=False),
|
|
122
|
+
),
|
|
123
|
+
metadata={
|
|
124
|
+
"allowed_decisions": [item.value for item in ConversationContextDecision],
|
|
125
|
+
"decision_task": "logical_continuation_yes_no",
|
|
126
|
+
"prompt_key": self.prompt_template.prompt_key,
|
|
127
|
+
"prompt_version": self.prompt_template.version,
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _recent_turns(self, conversation_context: dict[str, Any]) -> list[dict[str, Any]]:
|
|
132
|
+
turns = [
|
|
133
|
+
turn for turn in conversation_context.get("turns", []) if isinstance(turn, dict)
|
|
134
|
+
][-2:]
|
|
135
|
+
labels = ["t-2", "t-1"] if len(turns) == 2 else ["t-1"]
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
"label": label,
|
|
139
|
+
"question": str(turn.get("question") or ""),
|
|
140
|
+
"standalone_question": str(turn.get("standalone_question") or ""),
|
|
141
|
+
"answer": str(turn.get("answer") or ""),
|
|
142
|
+
"sql": str(turn.get("sql") or ""),
|
|
143
|
+
"context_decision": str(turn.get("context_decision") or ""),
|
|
144
|
+
"context_reason": str(turn.get("context_reason") or ""),
|
|
145
|
+
}
|
|
146
|
+
for label, turn in zip(labels, turns, strict=False)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
|
|
88
150
|
class MetadataResultInterpretationPromptCompiler:
|
|
89
151
|
def __init__(self, prompt_template: PromptTemplate) -> None:
|
|
90
152
|
self.prompt_template = prompt_template
|
|
@@ -170,6 +232,17 @@ def get_intent_classification_prompt_compiler() -> (
|
|
|
170
232
|
return MetadataIntentClassificationPromptCompiler(prompt_template=prompt_template)
|
|
171
233
|
|
|
172
234
|
|
|
235
|
+
def get_conversation_context_prompt_compiler() -> (
|
|
236
|
+
MetadataConversationContextPromptCompiler | None
|
|
237
|
+
):
|
|
238
|
+
prompt_template = get_active_prompt_template_safe("conversation_context_classification")
|
|
239
|
+
|
|
240
|
+
if prompt_template is None:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
return MetadataConversationContextPromptCompiler(prompt_template=prompt_template)
|
|
244
|
+
|
|
245
|
+
|
|
173
246
|
def get_result_interpretation_prompt_compiler() -> (
|
|
174
247
|
MetadataResultInterpretationPromptCompiler | None
|
|
175
248
|
):
|
|
@@ -232,6 +232,19 @@ def get_saved_metric_for_owner(
|
|
|
232
232
|
)
|
|
233
233
|
|
|
234
234
|
|
|
235
|
+
def get_saved_metric_link_for_owner(
|
|
236
|
+
session: Session,
|
|
237
|
+
widget_key: str,
|
|
238
|
+
owner_user_id: str,
|
|
239
|
+
) -> UserSavedMetric | None:
|
|
240
|
+
return session.scalar(
|
|
241
|
+
select(UserSavedMetric).where(
|
|
242
|
+
UserSavedMetric.widget_key == widget_key,
|
|
243
|
+
UserSavedMetric.owner_user_id == owner_user_id,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
235
248
|
def list_saved_metrics_for_owner(
|
|
236
249
|
session: Session,
|
|
237
250
|
owner_user_id: str,
|
|
@@ -360,6 +373,50 @@ def update_saved_dashboard_metric(
|
|
|
360
373
|
return {"item": serialize_saved_metric(session, metric, include_result=False)}
|
|
361
374
|
|
|
362
375
|
|
|
376
|
+
@router.delete("/dashboards/metrics/{widget_key}")
|
|
377
|
+
def delete_saved_dashboard_metric(
|
|
378
|
+
widget_key: str,
|
|
379
|
+
principal: AuthenticatedSession = Depends(get_current_api_user),
|
|
380
|
+
session: Session = Depends(get_session),
|
|
381
|
+
) -> dict[str, Any]:
|
|
382
|
+
owner_user_id = dashboard_owner_user_id(principal)
|
|
383
|
+
metric = get_saved_metric_for_owner(session, widget_key, owner_user_id)
|
|
384
|
+
metric_link = get_saved_metric_link_for_owner(session, widget_key, owner_user_id)
|
|
385
|
+
if metric is None or metric_link is None:
|
|
386
|
+
raise HTTPException(
|
|
387
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
388
|
+
detail="Saved metric not found.",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
dashboard_widgets = list(
|
|
392
|
+
session.scalars(
|
|
393
|
+
select(DashboardWidget).where(
|
|
394
|
+
DashboardWidget.metric_widget_key == widget_key,
|
|
395
|
+
DashboardWidget.owner_user_id == owner_user_id,
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
for widget in dashboard_widgets:
|
|
400
|
+
session.delete(widget)
|
|
401
|
+
|
|
402
|
+
session.delete(metric_link)
|
|
403
|
+
session.flush()
|
|
404
|
+
|
|
405
|
+
remaining_links = session.scalar(
|
|
406
|
+
select(UserSavedMetric.id).where(UserSavedMetric.widget_key == widget_key)
|
|
407
|
+
)
|
|
408
|
+
if remaining_links is None:
|
|
409
|
+
session.delete(metric)
|
|
410
|
+
|
|
411
|
+
session.commit()
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
"status": "deleted",
|
|
415
|
+
"widget_key": widget_key,
|
|
416
|
+
"removed_dashboard_widgets": len(dashboard_widgets),
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
363
420
|
@router.post("/dashboards")
|
|
364
421
|
def create_dashboard(
|
|
365
422
|
request: DashboardCreateRequest,
|
|
@@ -38,6 +38,7 @@ from gaard_core.sql_validator.select_only import SelectOnlySqlValidator
|
|
|
38
38
|
from gaard_llm.openai_compatible.client import OpenAICompatibleClient
|
|
39
39
|
|
|
40
40
|
from gaard_api.admin.prompt_runtime import (
|
|
41
|
+
get_conversation_context_prompt_compiler,
|
|
41
42
|
get_intent_classification_prompt_compiler,
|
|
42
43
|
get_result_classification_prompt_compiler,
|
|
43
44
|
get_result_interpretation_prompt_compiler,
|
|
@@ -230,6 +231,7 @@ def create_conversation_context_classifier(
|
|
|
230
231
|
client=create_llm_client(llm_config),
|
|
231
232
|
model=llm_config.model,
|
|
232
233
|
extra_body=llm_config.extra_body,
|
|
234
|
+
prompt_compiler=get_conversation_context_prompt_compiler(),
|
|
233
235
|
)
|
|
234
236
|
|
|
235
237
|
raise ConfigurationError(
|
|
@@ -799,20 +801,21 @@ def resolve_request_conversation(
|
|
|
799
801
|
),
|
|
800
802
|
)
|
|
801
803
|
|
|
802
|
-
deterministic = deterministic_follow_up_classification(request, context)
|
|
803
|
-
if deterministic is not None:
|
|
804
|
-
return (
|
|
805
|
-
conversation,
|
|
806
|
-
deterministic,
|
|
807
|
-
request.model_copy(
|
|
808
|
-
update={
|
|
809
|
-
"question": deterministic.standalone_question,
|
|
810
|
-
"conversation_id": conversation.conversation_id,
|
|
811
|
-
}
|
|
812
|
-
),
|
|
813
|
-
)
|
|
814
|
-
|
|
815
804
|
context_mode = resolve_intent_classification_mode()
|
|
805
|
+
if context_mode != "llm":
|
|
806
|
+
deterministic = deterministic_follow_up_classification(request, context)
|
|
807
|
+
if deterministic is not None:
|
|
808
|
+
return (
|
|
809
|
+
conversation,
|
|
810
|
+
deterministic,
|
|
811
|
+
request.model_copy(
|
|
812
|
+
update={
|
|
813
|
+
"question": deterministic.standalone_question,
|
|
814
|
+
"conversation_id": conversation.conversation_id,
|
|
815
|
+
}
|
|
816
|
+
),
|
|
817
|
+
)
|
|
818
|
+
|
|
816
819
|
llm_config = get_llm_runtime_config_safe() if context_mode == "llm" else None
|
|
817
820
|
classification = create_conversation_context_classifier(llm_config).classify(
|
|
818
821
|
request,
|
|
@@ -831,6 +834,8 @@ def resolve_request_conversation(
|
|
|
831
834
|
request.question,
|
|
832
835
|
standalone_question,
|
|
833
836
|
context,
|
|
837
|
+
classification,
|
|
838
|
+
allow_deterministic_fallback=context_mode != "llm",
|
|
834
839
|
)
|
|
835
840
|
if guard_rewrite is None:
|
|
836
841
|
classification = classification.model_copy(
|
|
@@ -897,6 +902,7 @@ def deterministic_follow_up_classification(
|
|
|
897
902
|
"and change only the requested output fields."
|
|
898
903
|
),
|
|
899
904
|
model_response=projected_result,
|
|
905
|
+
source="deterministic",
|
|
900
906
|
)
|
|
901
907
|
|
|
902
908
|
if is_open_only_command(question):
|
|
@@ -911,6 +917,7 @@ def deterministic_follow_up_classification(
|
|
|
911
917
|
standalone_question=filter_rewrite["standalone_question"],
|
|
912
918
|
reason="Deterministic rewrite: preserve previous topic and add open-status filter.",
|
|
913
919
|
model_response=filter_rewrite,
|
|
920
|
+
source="deterministic",
|
|
914
921
|
)
|
|
915
922
|
|
|
916
923
|
previous_period = rewrite_previous_period_follow_up(previous, previous_question, question)
|
|
@@ -921,6 +928,7 @@ def deterministic_follow_up_classification(
|
|
|
921
928
|
standalone_question=previous_period["standalone_question"],
|
|
922
929
|
reason="Deterministic rewrite: replace current time period with previous period.",
|
|
923
930
|
model_response=previous_period,
|
|
931
|
+
source="deterministic",
|
|
924
932
|
)
|
|
925
933
|
|
|
926
934
|
return None
|
|
@@ -930,6 +938,9 @@ def guard_follow_up_rewrite(
|
|
|
930
938
|
original_question: str,
|
|
931
939
|
standalone_question: str,
|
|
932
940
|
context: dict[str, Any],
|
|
941
|
+
classification: ConversationContextClassification,
|
|
942
|
+
*,
|
|
943
|
+
allow_deterministic_fallback: bool,
|
|
933
944
|
) -> str | None:
|
|
934
945
|
original = normalize_question_text(original_question)
|
|
935
946
|
standalone = normalize_question_text(standalone_question)
|
|
@@ -944,6 +955,10 @@ def guard_follow_up_rewrite(
|
|
|
944
955
|
)
|
|
945
956
|
|
|
946
957
|
if rewrite_is_too_close_to_original(original, standalone):
|
|
958
|
+
if classification.model_response.get("current_question_is_standalone") is True:
|
|
959
|
+
return original_question.strip()
|
|
960
|
+
if not allow_deterministic_fallback:
|
|
961
|
+
return None
|
|
947
962
|
deterministic = deterministic_follow_up_classification(
|
|
948
963
|
QueryRequest(question=original_question),
|
|
949
964
|
context,
|
|
@@ -955,6 +970,8 @@ def guard_follow_up_rewrite(
|
|
|
955
970
|
if is_open_only_command(original) and not carries_previous_context(
|
|
956
971
|
standalone, previous_question
|
|
957
972
|
):
|
|
973
|
+
if not allow_deterministic_fallback:
|
|
974
|
+
return None
|
|
958
975
|
deterministic = deterministic_follow_up_classification(
|
|
959
976
|
QueryRequest(question=original_question),
|
|
960
977
|
context,
|
|
@@ -210,6 +210,9 @@ def build_conversation_metadata(
|
|
|
210
210
|
"standalone_question": classification.standalone_question,
|
|
211
211
|
"confidence": classification.confidence,
|
|
212
212
|
"context_reason": classification.reason,
|
|
213
|
+
"context_source": classification.source,
|
|
214
|
+
"context_model_response": classification.model_response,
|
|
215
|
+
"context_prompt": classification.prompt,
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
|
|
@@ -221,6 +224,7 @@ def new_topic_classification(
|
|
|
221
224
|
confidence=confidence,
|
|
222
225
|
standalone_question=question,
|
|
223
226
|
reason="Started a new conversation context.",
|
|
227
|
+
source="system",
|
|
224
228
|
)
|
|
225
229
|
|
|
226
230
|
|
|
@@ -327,6 +331,10 @@ def conversation_turn_metadata(
|
|
|
327
331
|
compact = compact_response_metadata(metadata)
|
|
328
332
|
compact["context_reason"] = context_classification.reason
|
|
329
333
|
compact["context_model_response"] = context_classification.model_response
|
|
334
|
+
if context_classification.prompt:
|
|
335
|
+
compact["context_prompt"] = context_classification.prompt
|
|
336
|
+
if context_classification.source:
|
|
337
|
+
compact["context_source"] = context_classification.source
|
|
330
338
|
compact["working_context"] = build_working_context(
|
|
331
339
|
original_question=original_question,
|
|
332
340
|
standalone_question=standalone_question,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gaard-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: GAARD backend web services providing admin interface
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: gaard-core==0.2.
|
|
8
|
-
Requires-Dist: gaard-connectors==0.2.
|
|
9
|
-
Requires-Dist: gaard-llm==0.2.
|
|
10
|
-
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.
|
|
7
|
+
Requires-Dist: gaard-core==0.2.7
|
|
8
|
+
Requires-Dist: gaard-connectors==0.2.7
|
|
9
|
+
Requires-Dist: gaard-llm==0.2.7
|
|
10
|
+
Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.7
|
|
11
11
|
Requires-Dist: fastapi>=0.111.0
|
|
12
12
|
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
13
13
|
Requires-Dist: pydantic>=2.7.0
|
|
@@ -486,6 +486,68 @@ def test_dashboards_are_scoped_to_authenticated_user(admin_client: TestClient) -
|
|
|
486
486
|
"h": 3,
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
+
cross_delete_metric_response = admin_client.delete(
|
|
490
|
+
"/api/v1/dashboards/metrics/admin_saved_metric",
|
|
491
|
+
headers=analyst_headers,
|
|
492
|
+
)
|
|
493
|
+
assert cross_delete_metric_response.status_code == 404
|
|
494
|
+
|
|
495
|
+
delete_metric_response = admin_client.delete(
|
|
496
|
+
"/api/v1/dashboards/metrics/admin_saved_metric",
|
|
497
|
+
headers=admin_headers,
|
|
498
|
+
)
|
|
499
|
+
assert delete_metric_response.status_code == 200
|
|
500
|
+
assert delete_metric_response.json() == {
|
|
501
|
+
"status": "deleted",
|
|
502
|
+
"widget_key": "admin_saved_metric",
|
|
503
|
+
"removed_dashboard_widgets": 1,
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
deleted_metric_list = admin_client.get(
|
|
507
|
+
"/api/v1/dashboards/metrics",
|
|
508
|
+
headers=admin_headers,
|
|
509
|
+
)
|
|
510
|
+
assert deleted_metric_list.status_code == 200
|
|
511
|
+
assert all(
|
|
512
|
+
item["widget_key"] != "admin_saved_metric"
|
|
513
|
+
for item in deleted_metric_list.json()["items"]
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
widgets_after_metric_delete = admin_client.get(
|
|
517
|
+
f"/api/v1/dashboards/{admin_dashboard['id']}/widgets",
|
|
518
|
+
headers=admin_headers,
|
|
519
|
+
)
|
|
520
|
+
assert widgets_after_metric_delete.status_code == 200
|
|
521
|
+
assert widgets_after_metric_delete.json()["items"] == []
|
|
522
|
+
|
|
523
|
+
with create_session() as session:
|
|
524
|
+
assert (
|
|
525
|
+
session.scalar(
|
|
526
|
+
select(UserSavedMetric).where(
|
|
527
|
+
UserSavedMetric.owner_user_id == "1",
|
|
528
|
+
UserSavedMetric.widget_key == "admin_saved_metric",
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
is None
|
|
532
|
+
)
|
|
533
|
+
assert (
|
|
534
|
+
session.scalar(
|
|
535
|
+
select(DashboardWidget).where(
|
|
536
|
+
DashboardWidget.owner_user_id == "1",
|
|
537
|
+
DashboardWidget.metric_widget_key == "admin_saved_metric",
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
is None
|
|
541
|
+
)
|
|
542
|
+
assert (
|
|
543
|
+
session.scalar(
|
|
544
|
+
select(OverviewWidget).where(
|
|
545
|
+
OverviewWidget.widget_key == "admin_saved_metric",
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
is None
|
|
549
|
+
)
|
|
550
|
+
|
|
489
551
|
admin_list = admin_client.get("/api/v1/dashboards", headers=admin_headers)
|
|
490
552
|
analyst_list = admin_client.get("/api/v1/dashboards", headers=analyst_headers)
|
|
491
553
|
|
|
@@ -754,7 +816,9 @@ def test_admin_first_login_requires_password_change(admin_client: TestClient) ->
|
|
|
754
816
|
)
|
|
755
817
|
|
|
756
818
|
assert prompts_response.status_code == 200
|
|
757
|
-
|
|
819
|
+
prompt_keys = {item["prompt_key"] for item in prompts_response.json()["items"]}
|
|
820
|
+
assert len(prompt_keys) >= 2
|
|
821
|
+
assert "conversation_context_classification" in prompt_keys
|
|
758
822
|
|
|
759
823
|
|
|
760
824
|
def test_prompt_update_creates_admin_audit_event(admin_client: TestClient) -> None:
|
|
@@ -441,6 +441,83 @@ def test_query_follow_up_guard_rejects_unrewritten_classifier_output(
|
|
|
441
441
|
assert payload["metadata"]["conversation"]["context_decision"] == "ambiguous"
|
|
442
442
|
|
|
443
443
|
|
|
444
|
+
def test_query_accepts_llm_follow_up_when_current_question_is_standalone(
|
|
445
|
+
conversation_client: TestClient,
|
|
446
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
447
|
+
) -> None:
|
|
448
|
+
headers = auth_headers(conversation_client)
|
|
449
|
+
first = conversation_client.post(
|
|
450
|
+
"/api/v1/query",
|
|
451
|
+
headers=headers,
|
|
452
|
+
json={
|
|
453
|
+
"question": "ilu pacjentów było przyjętych tydzień temu",
|
|
454
|
+
"user_id": "alice",
|
|
455
|
+
},
|
|
456
|
+
)
|
|
457
|
+
assert first.status_code == 200
|
|
458
|
+
conversation_id = first.json()["metadata"]["conversation"]["id"]
|
|
459
|
+
|
|
460
|
+
second = conversation_client.post(
|
|
461
|
+
"/api/v1/query",
|
|
462
|
+
headers=headers,
|
|
463
|
+
json={
|
|
464
|
+
"question": "a dwa tygodnie temu?",
|
|
465
|
+
"user_id": "alice",
|
|
466
|
+
"conversation_id": conversation_id,
|
|
467
|
+
},
|
|
468
|
+
)
|
|
469
|
+
assert second.status_code == 200
|
|
470
|
+
|
|
471
|
+
class StandaloneContinuationClassifier:
|
|
472
|
+
def classify(self, request, context):
|
|
473
|
+
return ConversationContextClassification(
|
|
474
|
+
decision=ConversationContextDecision.FOLLOW_UP,
|
|
475
|
+
confidence=0.92,
|
|
476
|
+
standalone_question=request.question,
|
|
477
|
+
reason="LLM classified this as a logical continuation with a standalone question.",
|
|
478
|
+
model_response={
|
|
479
|
+
"is_continuation": True,
|
|
480
|
+
"current_question_is_standalone": True,
|
|
481
|
+
},
|
|
482
|
+
prompt={
|
|
483
|
+
"system_prompt": "Decide whether turn t is a logical continuation.",
|
|
484
|
+
"user_prompt": "turn_t_minus_1 + turn_t",
|
|
485
|
+
"metadata": {"decision_task": "logical_continuation_yes_no"},
|
|
486
|
+
},
|
|
487
|
+
source="llm",
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
monkeypatch.setattr(
|
|
491
|
+
query_module,
|
|
492
|
+
"create_conversation_context_classifier",
|
|
493
|
+
lambda _llm_config=None: StandaloneContinuationClassifier(),
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
third = conversation_client.post(
|
|
497
|
+
"/api/v1/query",
|
|
498
|
+
headers=headers,
|
|
499
|
+
json={
|
|
500
|
+
"question": "ilu pacjentów przyjęto w tym tygodniu",
|
|
501
|
+
"user_id": "alice",
|
|
502
|
+
"conversation_id": conversation_id,
|
|
503
|
+
},
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
assert third.status_code == 200
|
|
507
|
+
payload = third.json()
|
|
508
|
+
conversation = payload["metadata"]["conversation"]
|
|
509
|
+
assert payload["sql"]
|
|
510
|
+
assert payload["metadata"].get("blocked") is not True
|
|
511
|
+
assert "Potrzebuję doprecyzowania" not in payload["answer"]
|
|
512
|
+
assert conversation["context_decision"] == "follow_up"
|
|
513
|
+
assert conversation["standalone_question"] == "ilu pacjentów przyjęto w tym tygodniu"
|
|
514
|
+
assert conversation["context_source"] == "llm"
|
|
515
|
+
assert conversation["context_model_response"]["is_continuation"] is True
|
|
516
|
+
assert conversation["context_prompt"]["metadata"]["decision_task"] == (
|
|
517
|
+
"logical_continuation_yes_no"
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
444
521
|
def test_query_turn_metadata_keeps_context_reason_and_working_context(
|
|
445
522
|
conversation_client: TestClient,
|
|
446
523
|
) -> None:
|
|
@@ -4,6 +4,7 @@ from gaard_core.query_pipeline.models import QueryRequest, QueryResult
|
|
|
4
4
|
|
|
5
5
|
from gaard_api.admin.models import PromptTemplate
|
|
6
6
|
from gaard_api.admin.prompt_runtime import (
|
|
7
|
+
MetadataConversationContextPromptCompiler,
|
|
7
8
|
MetadataIntentClassificationPromptCompiler,
|
|
8
9
|
MetadataResultClassificationPromptCompiler,
|
|
9
10
|
MetadataResultInterpretationPromptCompiler,
|
|
@@ -59,6 +60,44 @@ def test_metadata_intent_classification_prompt_compiler_serializes_question() ->
|
|
|
59
60
|
assert compiled.metadata["prompt_key"] == "intent_classification"
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
def test_metadata_conversation_context_prompt_compiler_serializes_recent_turns() -> None:
|
|
64
|
+
compiler = MetadataConversationContextPromptCompiler(
|
|
65
|
+
prompt_template=PromptTemplate(
|
|
66
|
+
prompt_key="conversation_context_classification",
|
|
67
|
+
name="Conversation context classification",
|
|
68
|
+
system_prompt="system",
|
|
69
|
+
user_prompt_template="{payload}\n{question}",
|
|
70
|
+
version=3,
|
|
71
|
+
active=True,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
compiled = compiler.compile(
|
|
76
|
+
request=QueryRequest(question="ilu pacjentów przyjęto w tym tygodniu"),
|
|
77
|
+
conversation_context={
|
|
78
|
+
"turns": [
|
|
79
|
+
{
|
|
80
|
+
"question": "ilu pacjentów było przyjętych tydzień temu",
|
|
81
|
+
"answer": "12",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"question": "a dwa tygodnie temu?",
|
|
85
|
+
"standalone_question": "ilu pacjentów było przyjętych dwa tygodnie temu",
|
|
86
|
+
"answer": "9",
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
assert '"turn_t_minus_2"' in compiled.user_prompt
|
|
93
|
+
assert '"turn_t_minus_1"' in compiled.user_prompt
|
|
94
|
+
assert '"turn_t"' in compiled.user_prompt
|
|
95
|
+
assert "ilu pacjentów przyjęto w tym tygodniu" in compiled.user_prompt
|
|
96
|
+
assert compiled.metadata["prompt_key"] == "conversation_context_classification"
|
|
97
|
+
assert compiled.metadata["prompt_version"] == 3
|
|
98
|
+
assert compiled.metadata["decision_task"] == "logical_continuation_yes_no"
|
|
99
|
+
|
|
100
|
+
|
|
62
101
|
def test_metadata_result_classification_prompt_compiler_serializes_answer() -> None:
|
|
63
102
|
compiler = MetadataResultClassificationPromptCompiler(
|
|
64
103
|
prompt_template=PromptTemplate(
|
|
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
|
{gaard_api-0.2.6 → gaard_api-0.2.7}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack-all.js
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|