gaard-core 0.1.0__tar.gz → 0.2.0__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.
Files changed (68) hide show
  1. {gaard_core-0.1.0 → gaard_core-0.2.0}/PKG-INFO +2 -1
  2. {gaard_core-0.1.0 → gaard_core-0.2.0}/pyproject.toml +2 -1
  3. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/query_pipeline/models.py +1 -6
  4. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/query_pipeline/pipeline.py +20 -0
  5. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core.egg-info/PKG-INFO +2 -1
  6. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core.egg-info/SOURCES.txt +0 -12
  7. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core.egg-info/requires.txt +1 -0
  8. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_query_pipeline.py +24 -0
  9. gaard_core-0.1.0/src/gaard_core/investigation/__init__.py +0 -25
  10. gaard_core-0.1.0/src/gaard_core/investigation/llm_readiness_agent.py +0 -220
  11. gaard_core-0.1.0/src/gaard_core/investigation/loop.py +0 -83
  12. gaard_core-0.1.0/src/gaard_core/investigation/mock_readiness_agent.py +0 -20
  13. gaard_core-0.1.0/src/gaard_core/investigation/models.py +0 -62
  14. gaard_core-0.1.0/src/gaard_core/prompt_compiler/investigation_readiness_prompt.py +0 -84
  15. gaard_core-0.1.0/src/gaard_core/result_interpreter/__init__.py +0 -0
  16. gaard_core-0.1.0/src/gaard_core/schema/__init__.py +0 -0
  17. gaard_core-0.1.0/src/gaard_core/security/__init__.py +0 -0
  18. gaard_core-0.1.0/src/gaard_core/semantic_layer/__init__.py +0 -0
  19. gaard_core-0.1.0/src/gaard_core/sql_validator/__init__.py +0 -0
  20. gaard_core-0.1.0/tests/test_investigation_readiness.py +0 -150
  21. {gaard_core-0.1.0 → gaard_core-0.2.0}/README.md +0 -0
  22. {gaard_core-0.1.0 → gaard_core-0.2.0}/setup.cfg +0 -0
  23. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/__init__.py +0 -0
  24. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/errors.py +0 -0
  25. {gaard_core-0.1.0/src/gaard_core/audit → gaard_core-0.2.0/src/gaard_core/execution}/__init__.py +0 -0
  26. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/execution/mock_executor.py +0 -0
  27. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/json_utils.py +0 -0
  28. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/llm_output.py +0 -0
  29. {gaard_core-0.1.0/src/gaard_core/evaluation → gaard_core-0.2.0/src/gaard_core/prompt_compiler}/__init__.py +0 -0
  30. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/prompt_compiler/intent_classification_prompt.py +0 -0
  31. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/prompt_compiler/models.py +0 -0
  32. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/prompt_compiler/result_classification_prompt.py +0 -0
  33. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/prompt_compiler/result_interpretation_prompt.py +0 -0
  34. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/prompt_compiler/schema_formatter.py +0 -0
  35. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/prompt_compiler/sql_generation_prompt.py +0 -0
  36. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/query_intent/__init__.py +0 -0
  37. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/query_intent/llm_classifier.py +0 -0
  38. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/query_intent/mock_classifier.py +0 -0
  39. {gaard_core-0.1.0/src/gaard_core/execution → gaard_core-0.2.0/src/gaard_core/query_pipeline}/__init__.py +0 -0
  40. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/query_pipeline/llm_sql_generator.py +0 -0
  41. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/query_pipeline/mock_sql_generator.py +0 -0
  42. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/result_classifier/__init__.py +0 -0
  43. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/result_classifier/llm_classifier.py +0 -0
  44. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/result_classifier/mock_classifier.py +0 -0
  45. {gaard_core-0.1.0/src/gaard_core/policy_engine → gaard_core-0.2.0/src/gaard_core/result_interpreter}/__init__.py +0 -0
  46. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/result_interpreter/llm_interpreter.py +0 -0
  47. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/result_interpreter/mock_interpreter.py +0 -0
  48. {gaard_core-0.1.0/src/gaard_core/prompt_compiler → gaard_core-0.2.0/src/gaard_core/schema}/__init__.py +0 -0
  49. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/schema/cache.py +0 -0
  50. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/schema/context.py +0 -0
  51. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/schema/models.py +0 -0
  52. {gaard_core-0.1.0/src/gaard_core/query_pipeline → gaard_core-0.2.0/src/gaard_core/sql_validator}/__init__.py +0 -0
  53. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core/sql_validator/select_only.py +0 -0
  54. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core.egg-info/dependency_links.txt +0 -0
  55. {gaard_core-0.1.0 → gaard_core-0.2.0}/src/gaard_core.egg-info/top_level.txt +0 -0
  56. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_json_utils.py +0 -0
  57. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_llm_output.py +0 -0
  58. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_llm_query_intent_classifier.py +0 -0
  59. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_llm_result_classifier.py +0 -0
  60. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_llm_result_interpreter.py +0 -0
  61. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_llm_sql_generator.py +0 -0
  62. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_result_classification_prompt_compiler.py +0 -0
  63. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_result_interpretation_prompt_compiler.py +0 -0
  64. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_schema_context_cache.py +0 -0
  65. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_schema_context_service.py +0 -0
  66. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_schema_prompt_formatter.py +0 -0
  67. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_sql_generation_prompt_compiler.py +0 -0
  68. {gaard_core-0.1.0 → gaard_core-0.2.0}/tests/test_sql_validator.py +0 -0
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gaard-core
3
- Version: 0.1.0
3
+ Version: 0.2.0
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.0
7
8
  Requires-Dist: pydantic>=2.7.0
8
9
  Requires-Dist: sqlglot>=25.0.0
9
10
  Provides-Extra: dev
@@ -4,11 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gaard-core"
7
- version = "0.1.0"
7
+ version = "0.2.0"
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.0,<0.3.0",
12
13
  "pydantic>=2.7.0",
13
14
  "sqlglot>=25.0.0",
14
15
  ]
@@ -19,11 +19,6 @@ class QueryIntentDecision(StrEnum):
19
19
  AMBIGUOUS = "ambiguous"
20
20
 
21
21
 
22
- class QueryMode(StrEnum):
23
- SQL = "sql"
24
- INVESTIGATION = "investigation"
25
-
26
-
27
22
  class QueryIntentClassification(BaseModel):
28
23
  decision: QueryIntentDecision = QueryIntentDecision.AMBIGUOUS
29
24
  confidence: float = 0.0
@@ -35,7 +30,7 @@ class QueryRequest(BaseModel):
35
30
  question: str = Field(min_length=1)
36
31
  datasource_id: str = "default"
37
32
  user_id: str = "local-admin"
38
- mode: QueryMode = QueryMode.SQL
33
+ interpret: bool = True
39
34
 
40
35
 
41
36
  class GeneratedSql(BaseModel):
@@ -73,6 +73,26 @@ class QueryPipeline:
73
73
  self.sql_validator.validate(generated_sql.sql)
74
74
 
75
75
  result = self.executor.execute(generated_sql.sql)
76
+ if not request.interpret:
77
+ duration_ms = round((time.perf_counter() - started_at) * 1000, 2)
78
+ return QueryResponse(
79
+ question=request.question,
80
+ answer="",
81
+ sql=generated_sql.sql,
82
+ rows=result.rows,
83
+ metadata={
84
+ "duration_ms": duration_ms,
85
+ "datasource_id": request.datasource_id,
86
+ "user_id": request.user_id,
87
+ "confidence": generated_sql.confidence,
88
+ "assumptions": generated_sql.assumptions,
89
+ "sql_generation_mode": self.sql_generation_mode,
90
+ "result_interpretation_mode": "none",
91
+ "output_classification_mode": "none",
92
+ "output_classification": OutputClassification.UNKNOWN.value,
93
+ "raw_sql_output": True,
94
+ },
95
+ )
76
96
 
77
97
  try:
78
98
  answer = self.interpreter.interpret(
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gaard-core
3
- Version: 0.1.0
3
+ Version: 0.2.0
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.0
7
8
  Requires-Dist: pydantic>=2.7.0
8
9
  Requires-Dist: sqlglot>=25.0.0
9
10
  Provides-Extra: dev
@@ -9,19 +9,10 @@ src/gaard_core.egg-info/SOURCES.txt
9
9
  src/gaard_core.egg-info/dependency_links.txt
10
10
  src/gaard_core.egg-info/requires.txt
11
11
  src/gaard_core.egg-info/top_level.txt
12
- src/gaard_core/audit/__init__.py
13
- src/gaard_core/evaluation/__init__.py
14
12
  src/gaard_core/execution/__init__.py
15
13
  src/gaard_core/execution/mock_executor.py
16
- src/gaard_core/investigation/__init__.py
17
- src/gaard_core/investigation/llm_readiness_agent.py
18
- src/gaard_core/investigation/loop.py
19
- src/gaard_core/investigation/mock_readiness_agent.py
20
- src/gaard_core/investigation/models.py
21
- src/gaard_core/policy_engine/__init__.py
22
14
  src/gaard_core/prompt_compiler/__init__.py
23
15
  src/gaard_core/prompt_compiler/intent_classification_prompt.py
24
- src/gaard_core/prompt_compiler/investigation_readiness_prompt.py
25
16
  src/gaard_core/prompt_compiler/models.py
26
17
  src/gaard_core/prompt_compiler/result_classification_prompt.py
27
18
  src/gaard_core/prompt_compiler/result_interpretation_prompt.py
@@ -45,11 +36,8 @@ src/gaard_core/schema/__init__.py
45
36
  src/gaard_core/schema/cache.py
46
37
  src/gaard_core/schema/context.py
47
38
  src/gaard_core/schema/models.py
48
- src/gaard_core/security/__init__.py
49
- src/gaard_core/semantic_layer/__init__.py
50
39
  src/gaard_core/sql_validator/__init__.py
51
40
  src/gaard_core/sql_validator/select_only.py
52
- tests/test_investigation_readiness.py
53
41
  tests/test_json_utils.py
54
42
  tests/test_llm_output.py
55
43
  tests/test_llm_query_intent_classifier.py
@@ -1,3 +1,4 @@
1
+ gaard-plugin-api<0.3.0,>=0.2.0
1
2
  pydantic>=2.7.0
2
3
  sqlglot>=25.0.0
3
4
 
@@ -45,6 +45,11 @@ class FailingInterpreter:
45
45
  raise LlmProviderError("LLM provider request failed.")
46
46
 
47
47
 
48
+ class FailingClassifier:
49
+ def classify(self, request: QueryRequest, answer: str):
50
+ raise AssertionError("classifier should not be called")
51
+
52
+
48
53
  def test_query_pipeline_wraps_llm_provider_error_during_sql_generation() -> None:
49
54
  pipeline = QueryPipeline(sql_generator=FailingSqlGenerator())
50
55
 
@@ -69,3 +74,22 @@ def test_query_pipeline_wraps_llm_provider_error_during_result_interpretation()
69
74
  assert exc_info.value.code == "LLM_PROVIDER_ERROR"
70
75
  assert exc_info.value.phase == "result_interpretation"
71
76
  assert exc_info.value.sql == "SELECT 1 AS value"
77
+
78
+
79
+ def test_query_pipeline_can_return_raw_sql_output_without_interpretation() -> None:
80
+ pipeline = QueryPipeline(
81
+ sql_generator=StaticSqlGenerator(),
82
+ executor=StaticExecutor(),
83
+ interpreter=FailingInterpreter(),
84
+ classifier=FailingClassifier(),
85
+ )
86
+
87
+ response = pipeline.handle(QueryRequest(question="Inspect raw value", interpret=False))
88
+
89
+ assert response.answer == ""
90
+ assert response.sql == "SELECT 1 AS value"
91
+ assert response.rows == [{"value": 1}]
92
+ assert response.metadata["result_interpretation_mode"] == "none"
93
+ assert response.metadata["output_classification_mode"] == "none"
94
+ assert response.metadata["output_classification"] == "unknown"
95
+ assert response.metadata["raw_sql_output"] is True
@@ -1,25 +0,0 @@
1
- from gaard_core.investigation.loop import InvestigationLoop
2
- from gaard_core.investigation.llm_readiness_agent import LlmInvestigationReadinessAgent
3
- from gaard_core.investigation.mock_readiness_agent import MockInvestigationReadinessAgent
4
- from gaard_core.investigation.models import (
5
- InvestigationContext,
6
- InvestigationIteration,
7
- InvestigationLoopConfig,
8
- InvestigationLoopResult,
9
- InvestigationReadinessDecision,
10
- InvestigationRoute,
11
- RequiredAnalysisTask,
12
- )
13
-
14
- __all__ = [
15
- "InvestigationContext",
16
- "InvestigationIteration",
17
- "InvestigationLoop",
18
- "InvestigationLoopConfig",
19
- "InvestigationLoopResult",
20
- "InvestigationReadinessDecision",
21
- "InvestigationRoute",
22
- "LlmInvestigationReadinessAgent",
23
- "MockInvestigationReadinessAgent",
24
- "RequiredAnalysisTask",
25
- ]
@@ -1,220 +0,0 @@
1
- import json
2
- from typing import Any, Protocol
3
-
4
- from gaard_core.investigation.models import (
5
- InvestigationContext,
6
- InvestigationReadinessDecision,
7
- InvestigationRoute,
8
- RequiredAnalysisTask,
9
- )
10
- from gaard_core.llm_output import remove_thinking_blocks
11
- from gaard_core.prompt_compiler.investigation_readiness_prompt import (
12
- InvestigationReadinessPromptCompiler,
13
- )
14
- from gaard_core.prompt_compiler.models import CompiledPrompt
15
- from gaard_llm.openai_compatible.client import OpenAICompatibleClient
16
- from gaard_llm.providers.models import ChatCompletionRequest, ChatMessage
17
-
18
-
19
- class InvestigationReadinessPromptCompilerProtocol(Protocol):
20
- def compile(self, context: InvestigationContext) -> CompiledPrompt:
21
- pass
22
-
23
-
24
- class LlmInvestigationReadinessAgent:
25
- name = "llm_investigation_readiness"
26
-
27
- def __init__(
28
- self,
29
- client: OpenAICompatibleClient,
30
- model: str,
31
- extra_body: dict[str, Any] | None = None,
32
- prompt_compiler: InvestigationReadinessPromptCompilerProtocol | None = None,
33
- ) -> None:
34
- self.client = client
35
- self.model = model
36
- self.extra_body = extra_body or {}
37
- self.prompt_compiler = prompt_compiler or InvestigationReadinessPromptCompiler()
38
-
39
- def assess(self, context: InvestigationContext) -> InvestigationReadinessDecision:
40
- compiled_prompt = self.prompt_compiler.compile(context=context)
41
-
42
- response = self.client.create_chat_completion(
43
- ChatCompletionRequest(
44
- model=self.model,
45
- temperature=0.0,
46
- extra_body=self.extra_body,
47
- messages=[
48
- ChatMessage(
49
- role="system",
50
- content=compiled_prompt.system_prompt,
51
- ),
52
- ChatMessage(
53
- role="user",
54
- content=compiled_prompt.user_prompt,
55
- ),
56
- ],
57
- )
58
- )
59
-
60
- return parse_investigation_readiness_decision(response.content)
61
-
62
-
63
- def parse_investigation_readiness_decision(value: str) -> InvestigationReadinessDecision:
64
- cleaned = remove_thinking_blocks(value).strip()
65
-
66
- try:
67
- payload = json.loads(cleaned)
68
- except json.JSONDecodeError:
69
- return InvestigationReadinessDecision(
70
- ready_for_sql=False,
71
- route=InvestigationRoute.ANALYSIS,
72
- confidence=0.0,
73
- reason="Investigation readiness agent returned invalid JSON.",
74
- missing_information=["valid readiness JSON"],
75
- required_analysis=["Retry readiness assessment with a valid JSON response."],
76
- model_response={"raw": cleaned},
77
- )
78
-
79
- if not isinstance(payload, dict):
80
- return InvestigationReadinessDecision(
81
- ready_for_sql=False,
82
- route=InvestigationRoute.ANALYSIS,
83
- confidence=0.0,
84
- reason="Investigation readiness agent returned a non-object JSON value.",
85
- missing_information=["valid readiness JSON object"],
86
- required_analysis=["Retry readiness assessment with a JSON object response."],
87
- model_response={"raw": payload},
88
- )
89
-
90
- ready_for_sql = parse_bool(payload.get("ready_for_sql"))
91
- route = parse_route(payload.get("route"), ready_for_sql)
92
-
93
- if route == InvestigationRoute.SQL and not ready_for_sql:
94
- route = InvestigationRoute.ANALYSIS
95
-
96
- if route == InvestigationRoute.ANALYSIS:
97
- ready_for_sql = False
98
-
99
- missing_information = parse_string_list(payload.get("missing_information"))
100
- required_analysis = parse_string_list(payload.get("required_analysis"))
101
-
102
- return InvestigationReadinessDecision(
103
- ready_for_sql=ready_for_sql,
104
- route=route,
105
- confidence=parse_confidence(payload.get("confidence")),
106
- reason=str(payload.get("reason") or ""),
107
- missing_information=missing_information,
108
- required_analysis=required_analysis,
109
- required_analysis_tasks=parse_required_analysis_tasks(
110
- payload.get("required_analysis_tasks"),
111
- missing_information,
112
- required_analysis,
113
- ),
114
- assumptions=parse_string_list(payload.get("assumptions")),
115
- model_response=payload,
116
- )
117
-
118
-
119
- def parse_route(value: object, ready_for_sql: bool) -> InvestigationRoute:
120
- if isinstance(value, str):
121
- normalized = value.strip().lower().replace("-", "_").replace(" ", "_")
122
- if normalized in {"sql", "ready", "ready_for_sql"}:
123
- return InvestigationRoute.SQL
124
- if normalized in {"analysis", "analyze", "requires_analysis"}:
125
- return InvestigationRoute.ANALYSIS
126
-
127
- return InvestigationRoute.SQL if ready_for_sql else InvestigationRoute.ANALYSIS
128
-
129
-
130
- def parse_bool(value: object) -> bool:
131
- if isinstance(value, bool):
132
- return value
133
-
134
- if isinstance(value, str):
135
- return value.strip().lower() in {"true", "yes", "tak", "1"}
136
-
137
- return False
138
-
139
-
140
- def parse_confidence(value: object) -> float:
141
- try:
142
- confidence = float(value)
143
- except (TypeError, ValueError):
144
- return 0.0
145
-
146
- return max(0.0, min(1.0, confidence))
147
-
148
-
149
- def parse_string_list(value: object) -> list[str]:
150
- if not isinstance(value, list):
151
- return []
152
-
153
- items: list[str] = []
154
- for item in value:
155
- if item is None:
156
- continue
157
- text = str(item).strip()
158
- if text:
159
- items.append(text)
160
-
161
- return items
162
-
163
-
164
- def parse_required_analysis_tasks(
165
- value: object,
166
- missing_information: list[str],
167
- required_analysis: list[str],
168
- ) -> list[RequiredAnalysisTask]:
169
- if isinstance(value, list):
170
- tasks = [
171
- parse_required_analysis_task(item)
172
- for item in value
173
- if isinstance(item, dict)
174
- ]
175
- tasks = [task for task in tasks if task.required_analysis]
176
- if tasks:
177
- return tasks
178
-
179
- return required_analysis_tasks_from_lists(missing_information, required_analysis)
180
-
181
-
182
- def parse_required_analysis_task(value: dict[str, object]) -> RequiredAnalysisTask:
183
- return RequiredAnalysisTask(
184
- missing_information=str(value.get("missing_information") or "").strip(),
185
- required_analysis=str(value.get("required_analysis") or "").strip(),
186
- category=normalize_analysis_category(value.get("category")),
187
- expected_output=str(value.get("expected_output") or "").strip(),
188
- )
189
-
190
-
191
- def required_analysis_tasks_from_lists(
192
- missing_information: list[str],
193
- required_analysis: list[str],
194
- ) -> list[RequiredAnalysisTask]:
195
- tasks: list[RequiredAnalysisTask] = []
196
- for index, analysis_question in enumerate(required_analysis):
197
- tasks.append(
198
- RequiredAnalysisTask(
199
- missing_information=missing_information[index]
200
- if index < len(missing_information)
201
- else "",
202
- required_analysis=analysis_question,
203
- )
204
- )
205
-
206
- return tasks
207
-
208
-
209
- def normalize_analysis_category(value: object) -> str:
210
- normalized = str(value or "unknown").strip().lower().replace("-", "_").replace(" ", "_")
211
- allowed_categories = {
212
- "dictionary_value",
213
- "relationship_logic",
214
- "filter_logic",
215
- "aggregation_logic",
216
- "entity_mapping",
217
- "unknown",
218
- }
219
-
220
- return normalized if normalized in allowed_categories else "unknown"
@@ -1,83 +0,0 @@
1
- from typing import Protocol
2
-
3
- from gaard_core.investigation.models import (
4
- InvestigationContext,
5
- InvestigationIteration,
6
- InvestigationLoopConfig,
7
- InvestigationLoopResult,
8
- InvestigationReadinessDecision,
9
- InvestigationRoute,
10
- )
11
-
12
-
13
- class InvestigationReadinessAgent(Protocol):
14
- name: str
15
-
16
- def assess(self, context: InvestigationContext) -> InvestigationReadinessDecision:
17
- pass
18
-
19
-
20
- class InvestigationLoop:
21
- def __init__(
22
- self,
23
- readiness_agent: InvestigationReadinessAgent,
24
- config: InvestigationLoopConfig | None = None,
25
- ) -> None:
26
- self.readiness_agent = readiness_agent
27
- self.config = config or InvestigationLoopConfig()
28
-
29
- def run(self, context: InvestigationContext) -> InvestigationLoopResult:
30
- iterations: list[InvestigationIteration] = []
31
-
32
- for iteration_number in range(1, self.config.max_iterations + 1):
33
- decision = self.readiness_agent.assess(context)
34
- normalized_decision = self._normalize_decision(decision)
35
- iterations.append(
36
- InvestigationIteration(
37
- iteration=iteration_number,
38
- agent=self.readiness_agent.name,
39
- decision=normalized_decision,
40
- )
41
- )
42
-
43
- if normalized_decision.route == InvestigationRoute.SQL:
44
- return InvestigationLoopResult(
45
- route=InvestigationRoute.SQL,
46
- ready_for_sql=True,
47
- max_iterations=self.config.max_iterations,
48
- confidence_threshold=self.config.readiness_confidence_threshold,
49
- iterations=iterations,
50
- )
51
-
52
- return InvestigationLoopResult(
53
- route=InvestigationRoute.ANALYSIS,
54
- ready_for_sql=False,
55
- max_iterations=self.config.max_iterations,
56
- confidence_threshold=self.config.readiness_confidence_threshold,
57
- iterations=iterations,
58
- )
59
-
60
- return InvestigationLoopResult(
61
- route=InvestigationRoute.ANALYSIS,
62
- ready_for_sql=False,
63
- max_iterations=self.config.max_iterations,
64
- confidence_threshold=self.config.readiness_confidence_threshold,
65
- iterations=iterations,
66
- )
67
-
68
- def _normalize_decision(
69
- self,
70
- decision: InvestigationReadinessDecision,
71
- ) -> InvestigationReadinessDecision:
72
- ready = (
73
- decision.ready_for_sql
74
- and decision.confidence >= self.config.readiness_confidence_threshold
75
- )
76
- route = InvestigationRoute.SQL if ready else InvestigationRoute.ANALYSIS
77
-
78
- return decision.model_copy(
79
- update={
80
- "ready_for_sql": ready,
81
- "route": route,
82
- }
83
- )
@@ -1,20 +0,0 @@
1
- from gaard_core.investigation.models import (
2
- InvestigationContext,
3
- InvestigationReadinessDecision,
4
- InvestigationRoute,
5
- )
6
-
7
-
8
- class MockInvestigationReadinessAgent:
9
- name = "mock_investigation_readiness"
10
-
11
- def __init__(self, decision: InvestigationReadinessDecision | None = None) -> None:
12
- self.decision = decision or InvestigationReadinessDecision(
13
- ready_for_sql=True,
14
- route=InvestigationRoute.SQL,
15
- confidence=1.0,
16
- reason="Mock readiness agent allows the normal SQL pipeline.",
17
- )
18
-
19
- def assess(self, context: InvestigationContext) -> InvestigationReadinessDecision:
20
- return self.decision
@@ -1,62 +0,0 @@
1
- from enum import StrEnum
2
- from typing import Any
3
-
4
- from pydantic import BaseModel, Field
5
-
6
-
7
- class InvestigationRoute(StrEnum):
8
- SQL = "sql"
9
- ANALYSIS = "analysis"
10
-
11
-
12
- class InvestigationContext(BaseModel):
13
- question: str = Field(min_length=1)
14
- datasource_id: str = "default"
15
- user_id: str = "local-admin"
16
- formatted_schema: str = ""
17
- business_logic: str = ""
18
-
19
-
20
- class RequiredAnalysisTask(BaseModel):
21
- missing_information: str = ""
22
- required_analysis: str = ""
23
- category: str = "unknown"
24
- expected_output: str = ""
25
-
26
-
27
- class InvestigationReadinessDecision(BaseModel):
28
- ready_for_sql: bool = False
29
- route: InvestigationRoute = InvestigationRoute.ANALYSIS
30
- confidence: float = 0.0
31
- reason: str = ""
32
- missing_information: list[str] = Field(default_factory=list)
33
- required_analysis: list[str] = Field(default_factory=list)
34
- required_analysis_tasks: list[RequiredAnalysisTask] = Field(default_factory=list)
35
- assumptions: list[str] = Field(default_factory=list)
36
- model_response: dict[str, Any] = Field(default_factory=dict)
37
-
38
-
39
- class InvestigationIteration(BaseModel):
40
- iteration: int
41
- agent: str
42
- decision: InvestigationReadinessDecision
43
-
44
-
45
- class InvestigationLoopConfig(BaseModel):
46
- max_iterations: int = Field(default=1, ge=1)
47
- readiness_confidence_threshold: float = Field(default=0.85, ge=0.0, le=1.0)
48
-
49
-
50
- class InvestigationLoopResult(BaseModel):
51
- route: InvestigationRoute
52
- ready_for_sql: bool
53
- max_iterations: int
54
- confidence_threshold: float
55
- iterations: list[InvestigationIteration] = Field(default_factory=list)
56
-
57
- @property
58
- def final_decision(self) -> InvestigationReadinessDecision | None:
59
- if not self.iterations:
60
- return None
61
-
62
- return self.iterations[-1].decision
@@ -1,84 +0,0 @@
1
- from gaard_core.investigation.models import InvestigationContext, InvestigationRoute
2
- from gaard_core.json_utils import json_dumps
3
- from gaard_core.prompt_compiler.models import CompiledPrompt
4
-
5
-
6
- class InvestigationReadinessPromptCompiler:
7
- def compile(self, context: InvestigationContext) -> CompiledPrompt:
8
- payload = {
9
- "question": context.question,
10
- "datasource_id": context.datasource_id,
11
- "user_id": context.user_id,
12
- "schema": context.formatted_schema,
13
- "business_logic": context.business_logic,
14
- }
15
-
16
- return CompiledPrompt(
17
- system_prompt=self._build_system_prompt(),
18
- user_prompt=self._build_user_prompt(payload),
19
- metadata={
20
- "allowed_routes": [item.value for item in InvestigationRoute],
21
- },
22
- )
23
-
24
- def _build_system_prompt(self) -> str:
25
- return """You are GAARD Investigation Readiness.
26
-
27
- Your task is to decide whether GAARD already knows enough to create a correct SQL query for the user's question.
28
-
29
- Assume nothing. Verify continuously.
30
-
31
- Use only:
32
- - the user's question,
33
- - the active datasource schema,
34
- - the approved or previously saved business logic supplied in the payload.
35
-
36
- You do not generate SQL.
37
- You do not answer the user.
38
- You decide only whether normal SQL generation may start safely.
39
-
40
- Return ready_for_sql=true only when all information needed for correct SQL is explicit in the question, schema, and business logic:
41
- - requested business entity or metric,
42
- - relevant tables, views and columns,
43
- - required filters and dictionary/status values,
44
- - required joins or relationships,
45
- - requested output shape such as count, list, detail, or aggregation.
46
-
47
- Return ready_for_sql=false when any material element is missing, ambiguous, inferred only from the model, or would require checking data values before SQL can be trusted. In that case route must be analysis.
48
-
49
- Output rules:
50
- - Return only a JSON object.
51
- - Do not include markdown.
52
- - Do not include reasoning outside the JSON.
53
- - Do not include <think> blocks.
54
- - Use exactly this JSON shape:
55
- {"ready_for_sql":false,"route":"analysis","confidence":0.0,"reason":"short reason","missing_information":[],"required_analysis":[],"required_analysis_tasks":[],"assumptions":[]}
56
-
57
- Required analysis task shape:
58
- {"missing_information":"what is missing","required_analysis":"specific read-only data question for SQL analysis","category":"dictionary_value","expected_output":"what kind of result would resolve this"}
59
-
60
- Allowed categories:
61
- - dictionary_value
62
- - relationship_logic
63
- - filter_logic
64
- - aggregation_logic
65
- - entity_mapping
66
- - unknown
67
- """
68
-
69
- def _build_user_prompt(self, payload: dict[str, str]) -> str:
70
- return f"""Assess whether normal SQL generation can start.
71
-
72
- Input JSON:
73
- {json_dumps(payload, ensure_ascii=False, indent=2)}
74
-
75
- Return one JSON object with:
76
- - ready_for_sql: boolean
77
- - route: sql or analysis
78
- - confidence: number from 0 to 1
79
- - reason: short explanation
80
- - missing_information: list of missing or ambiguous items
81
- - required_analysis: list of checks that Analysis mode should perform when ready_for_sql=false
82
- - required_analysis_tasks: list of structured SQL-analysis tasks with missing_information, required_analysis, category, expected_output
83
- - assumptions: list of any assumptions that would affect SQL correctness
84
- """
File without changes
File without changes
File without changes
@@ -1,150 +0,0 @@
1
- from gaard_core.investigation.llm_readiness_agent import (
2
- parse_investigation_readiness_decision,
3
- )
4
- from gaard_core.investigation.loop import InvestigationLoop
5
- from gaard_core.investigation.mock_readiness_agent import MockInvestigationReadinessAgent
6
- from gaard_core.investigation.models import (
7
- InvestigationContext,
8
- InvestigationLoopConfig,
9
- InvestigationReadinessDecision,
10
- InvestigationRoute,
11
- RequiredAnalysisTask,
12
- )
13
- from gaard_core.prompt_compiler.investigation_readiness_prompt import (
14
- InvestigationReadinessPromptCompiler,
15
- )
16
-
17
-
18
- def test_parse_readiness_decision_routes_ready_json_to_sql() -> None:
19
- decision = parse_investigation_readiness_decision(
20
- """
21
- {
22
- "ready_for_sql": true,
23
- "route": "sql",
24
- "confidence": 0.94,
25
- "reason": "Schema and business logic identify the needed columns.",
26
- "missing_information": [],
27
- "required_analysis": [],
28
- "assumptions": []
29
- }
30
- """
31
- )
32
-
33
- assert decision.ready_for_sql is True
34
- assert decision.route == InvestigationRoute.SQL
35
- assert decision.confidence == 0.94
36
-
37
-
38
- def test_parse_readiness_decision_invalid_json_requires_analysis() -> None:
39
- decision = parse_investigation_readiness_decision("ready")
40
-
41
- assert decision.ready_for_sql is False
42
- assert decision.route == InvestigationRoute.ANALYSIS
43
- assert "valid readiness JSON" in decision.missing_information
44
-
45
-
46
- def test_parse_readiness_decision_requires_consistent_ready_signal() -> None:
47
- decision = parse_investigation_readiness_decision(
48
- '{"ready_for_sql": false, "route": "sql", "confidence": 0.99}'
49
- )
50
-
51
- assert decision.ready_for_sql is False
52
- assert decision.route == InvestigationRoute.ANALYSIS
53
-
54
-
55
- def test_parse_readiness_decision_reads_structured_required_analysis_tasks() -> None:
56
- decision = parse_investigation_readiness_decision(
57
- """
58
- {
59
- "ready_for_sql": false,
60
- "route": "analysis",
61
- "confidence": 0.91,
62
- "missing_information": ["specialty dictionary value"],
63
- "required_analysis": ["List distinct doctors.specialization values."],
64
- "required_analysis_tasks": [
65
- {
66
- "missing_information": "specialty dictionary value",
67
- "required_analysis": "List distinct doctors.specialization values.",
68
- "category": "dictionary-value",
69
- "expected_output": "specialization values"
70
- }
71
- ],
72
- "assumptions": []
73
- }
74
- """
75
- )
76
-
77
- assert decision.required_analysis_tasks == [
78
- RequiredAnalysisTask(
79
- missing_information="specialty dictionary value",
80
- required_analysis="List distinct doctors.specialization values.",
81
- category="dictionary_value",
82
- expected_output="specialization values",
83
- )
84
- ]
85
-
86
-
87
- def test_parse_readiness_decision_builds_tasks_from_legacy_lists() -> None:
88
- decision = parse_investigation_readiness_decision(
89
- """
90
- {
91
- "ready_for_sql": false,
92
- "route": "analysis",
93
- "confidence": 0.91,
94
- "missing_information": ["specialty dictionary value"],
95
- "required_analysis": ["List distinct doctors.specialization values."],
96
- "assumptions": []
97
- }
98
- """
99
- )
100
-
101
- assert decision.required_analysis_tasks == [
102
- RequiredAnalysisTask(
103
- missing_information="specialty dictionary value",
104
- required_analysis="List distinct doctors.specialization values.",
105
- )
106
- ]
107
-
108
-
109
- def test_investigation_loop_requires_confidence_threshold_for_sql() -> None:
110
- agent = MockInvestigationReadinessAgent(
111
- InvestigationReadinessDecision(
112
- ready_for_sql=True,
113
- route=InvestigationRoute.SQL,
114
- confidence=0.5,
115
- reason="Low confidence.",
116
- )
117
- )
118
-
119
- result = InvestigationLoop(
120
- readiness_agent=agent,
121
- config=InvestigationLoopConfig(
122
- max_iterations=1,
123
- readiness_confidence_threshold=0.85,
124
- ),
125
- ).run(
126
- InvestigationContext(
127
- question="ile jest wizyt",
128
- formatted_schema="Table: appointments",
129
- )
130
- )
131
-
132
- assert result.ready_for_sql is False
133
- assert result.route == InvestigationRoute.ANALYSIS
134
- assert result.iterations[0].decision.ready_for_sql is False
135
-
136
-
137
- def test_readiness_prompt_includes_schema_and_business_logic() -> None:
138
- compiled = InvestigationReadinessPromptCompiler().compile(
139
- InvestigationContext(
140
- question="ile jest aktywnych pacjentów",
141
- datasource_id="medical",
142
- user_id="alice",
143
- formatted_schema="Table: patients",
144
- business_logic="Active patient means patients.status = 'active'.",
145
- )
146
- )
147
-
148
- assert "Assume nothing. Verify continuously." in compiled.system_prompt
149
- assert "Table: patients" in compiled.user_prompt
150
- assert "patients.status = 'active'" in compiled.user_prompt
File without changes
File without changes