amd-gaia 0.15.0__py3-none-any.whl → 0.15.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/METADATA +222 -223
- amd_gaia-0.15.2.dist-info/RECORD +182 -0
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/WHEEL +1 -1
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/entry_points.txt +1 -0
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/licenses/LICENSE.md +20 -20
- gaia/__init__.py +29 -29
- gaia/agents/__init__.py +19 -19
- gaia/agents/base/__init__.py +9 -9
- gaia/agents/base/agent.py +2132 -2177
- gaia/agents/base/api_agent.py +119 -120
- gaia/agents/base/console.py +1967 -1841
- gaia/agents/base/errors.py +237 -237
- gaia/agents/base/mcp_agent.py +86 -86
- gaia/agents/base/tools.py +88 -83
- gaia/agents/blender/__init__.py +7 -0
- gaia/agents/blender/agent.py +553 -556
- gaia/agents/blender/agent_simple.py +133 -135
- gaia/agents/blender/app.py +211 -211
- gaia/agents/blender/app_simple.py +41 -41
- gaia/agents/blender/core/__init__.py +16 -16
- gaia/agents/blender/core/materials.py +506 -506
- gaia/agents/blender/core/objects.py +316 -316
- gaia/agents/blender/core/rendering.py +225 -225
- gaia/agents/blender/core/scene.py +220 -220
- gaia/agents/blender/core/view.py +146 -146
- gaia/agents/chat/__init__.py +9 -9
- gaia/agents/chat/agent.py +809 -835
- gaia/agents/chat/app.py +1065 -1058
- gaia/agents/chat/session.py +508 -508
- gaia/agents/chat/tools/__init__.py +15 -15
- gaia/agents/chat/tools/file_tools.py +96 -96
- gaia/agents/chat/tools/rag_tools.py +1744 -1729
- gaia/agents/chat/tools/shell_tools.py +437 -436
- gaia/agents/code/__init__.py +7 -7
- gaia/agents/code/agent.py +549 -549
- gaia/agents/code/cli.py +377 -0
- gaia/agents/code/models.py +135 -135
- gaia/agents/code/orchestration/__init__.py +24 -24
- gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
- gaia/agents/code/orchestration/checklist_generator.py +713 -713
- gaia/agents/code/orchestration/factories/__init__.py +9 -9
- gaia/agents/code/orchestration/factories/base.py +63 -63
- gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
- gaia/agents/code/orchestration/factories/python_factory.py +106 -106
- gaia/agents/code/orchestration/orchestrator.py +841 -841
- gaia/agents/code/orchestration/project_analyzer.py +391 -391
- gaia/agents/code/orchestration/steps/__init__.py +67 -67
- gaia/agents/code/orchestration/steps/base.py +188 -188
- gaia/agents/code/orchestration/steps/error_handler.py +314 -314
- gaia/agents/code/orchestration/steps/nextjs.py +828 -828
- gaia/agents/code/orchestration/steps/python.py +307 -307
- gaia/agents/code/orchestration/template_catalog.py +469 -469
- gaia/agents/code/orchestration/workflows/__init__.py +14 -14
- gaia/agents/code/orchestration/workflows/base.py +80 -80
- gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
- gaia/agents/code/orchestration/workflows/python.py +94 -94
- gaia/agents/code/prompts/__init__.py +11 -11
- gaia/agents/code/prompts/base_prompt.py +77 -77
- gaia/agents/code/prompts/code_patterns.py +2034 -2036
- gaia/agents/code/prompts/nextjs_prompt.py +40 -40
- gaia/agents/code/prompts/python_prompt.py +109 -109
- gaia/agents/code/schema_inference.py +365 -365
- gaia/agents/code/system_prompt.py +41 -41
- gaia/agents/code/tools/__init__.py +42 -42
- gaia/agents/code/tools/cli_tools.py +1138 -1138
- gaia/agents/code/tools/code_formatting.py +319 -319
- gaia/agents/code/tools/code_tools.py +769 -769
- gaia/agents/code/tools/error_fixing.py +1347 -1347
- gaia/agents/code/tools/external_tools.py +180 -180
- gaia/agents/code/tools/file_io.py +845 -845
- gaia/agents/code/tools/prisma_tools.py +190 -190
- gaia/agents/code/tools/project_management.py +1016 -1016
- gaia/agents/code/tools/testing.py +321 -321
- gaia/agents/code/tools/typescript_tools.py +122 -122
- gaia/agents/code/tools/validation_parsing.py +461 -461
- gaia/agents/code/tools/validation_tools.py +806 -806
- gaia/agents/code/tools/web_dev_tools.py +1758 -1758
- gaia/agents/code/validators/__init__.py +16 -16
- gaia/agents/code/validators/antipattern_checker.py +241 -241
- gaia/agents/code/validators/ast_analyzer.py +197 -197
- gaia/agents/code/validators/requirements_validator.py +145 -145
- gaia/agents/code/validators/syntax_validator.py +171 -171
- gaia/agents/docker/__init__.py +7 -7
- gaia/agents/docker/agent.py +643 -642
- gaia/agents/emr/__init__.py +8 -8
- gaia/agents/emr/agent.py +1504 -1506
- gaia/agents/emr/cli.py +1322 -1322
- gaia/agents/emr/constants.py +475 -475
- gaia/agents/emr/dashboard/__init__.py +4 -4
- gaia/agents/emr/dashboard/server.py +1972 -1974
- gaia/agents/jira/__init__.py +11 -11
- gaia/agents/jira/agent.py +894 -894
- gaia/agents/jira/jql_templates.py +299 -299
- gaia/agents/routing/__init__.py +7 -7
- gaia/agents/routing/agent.py +567 -570
- gaia/agents/routing/system_prompt.py +75 -75
- gaia/agents/summarize/__init__.py +11 -0
- gaia/agents/summarize/agent.py +885 -0
- gaia/agents/summarize/prompts.py +129 -0
- gaia/api/__init__.py +23 -23
- gaia/api/agent_registry.py +238 -238
- gaia/api/app.py +305 -305
- gaia/api/openai_server.py +575 -575
- gaia/api/schemas.py +186 -186
- gaia/api/sse_handler.py +373 -373
- gaia/apps/__init__.py +4 -4
- gaia/apps/llm/__init__.py +6 -6
- gaia/apps/llm/app.py +184 -169
- gaia/apps/summarize/app.py +116 -633
- gaia/apps/summarize/html_viewer.py +133 -133
- gaia/apps/summarize/pdf_formatter.py +284 -284
- gaia/audio/__init__.py +2 -2
- gaia/audio/audio_client.py +439 -439
- gaia/audio/audio_recorder.py +269 -269
- gaia/audio/kokoro_tts.py +599 -599
- gaia/audio/whisper_asr.py +432 -432
- gaia/chat/__init__.py +16 -16
- gaia/chat/app.py +428 -430
- gaia/chat/prompts.py +522 -522
- gaia/chat/sdk.py +1228 -1225
- gaia/cli.py +5659 -5632
- gaia/database/__init__.py +10 -10
- gaia/database/agent.py +176 -176
- gaia/database/mixin.py +290 -290
- gaia/database/testing.py +64 -64
- gaia/eval/batch_experiment.py +2332 -2332
- gaia/eval/claude.py +542 -542
- gaia/eval/config.py +37 -37
- gaia/eval/email_generator.py +512 -512
- gaia/eval/eval.py +3179 -3179
- gaia/eval/groundtruth.py +1130 -1130
- gaia/eval/transcript_generator.py +582 -582
- gaia/eval/webapp/README.md +167 -167
- gaia/eval/webapp/package-lock.json +875 -875
- gaia/eval/webapp/package.json +20 -20
- gaia/eval/webapp/public/app.js +3402 -3402
- gaia/eval/webapp/public/index.html +87 -87
- gaia/eval/webapp/public/styles.css +3661 -3661
- gaia/eval/webapp/server.js +415 -415
- gaia/eval/webapp/test-setup.js +72 -72
- gaia/installer/__init__.py +23 -0
- gaia/installer/init_command.py +1275 -0
- gaia/installer/lemonade_installer.py +619 -0
- gaia/llm/__init__.py +10 -2
- gaia/llm/base_client.py +60 -0
- gaia/llm/exceptions.py +12 -0
- gaia/llm/factory.py +70 -0
- gaia/llm/lemonade_client.py +3421 -3221
- gaia/llm/lemonade_manager.py +294 -294
- gaia/llm/providers/__init__.py +9 -0
- gaia/llm/providers/claude.py +108 -0
- gaia/llm/providers/lemonade.py +118 -0
- gaia/llm/providers/openai_provider.py +79 -0
- gaia/llm/vlm_client.py +382 -382
- gaia/logger.py +189 -189
- gaia/mcp/agent_mcp_server.py +245 -245
- gaia/mcp/blender_mcp_client.py +138 -138
- gaia/mcp/blender_mcp_server.py +648 -648
- gaia/mcp/context7_cache.py +332 -332
- gaia/mcp/external_services.py +518 -518
- gaia/mcp/mcp_bridge.py +811 -550
- gaia/mcp/servers/__init__.py +6 -6
- gaia/mcp/servers/docker_mcp.py +83 -83
- gaia/perf_analysis.py +361 -0
- gaia/rag/__init__.py +10 -10
- gaia/rag/app.py +293 -293
- gaia/rag/demo.py +304 -304
- gaia/rag/pdf_utils.py +235 -235
- gaia/rag/sdk.py +2194 -2194
- gaia/security.py +183 -163
- gaia/talk/app.py +287 -289
- gaia/talk/sdk.py +538 -538
- gaia/testing/__init__.py +87 -87
- gaia/testing/assertions.py +330 -330
- gaia/testing/fixtures.py +333 -333
- gaia/testing/mocks.py +493 -493
- gaia/util.py +46 -46
- gaia/utils/__init__.py +33 -33
- gaia/utils/file_watcher.py +675 -675
- gaia/utils/parsing.py +223 -223
- gaia/version.py +100 -100
- amd_gaia-0.15.0.dist-info/RECORD +0 -168
- gaia/agents/code/app.py +0 -266
- gaia/llm/llm_client.py +0 -723
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/top_level.txt +0 -0
|
@@ -1,365 +1,365 @@
|
|
|
1
|
-
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
"""AI-powered schema inference for Code Agent.
|
|
4
|
-
|
|
5
|
-
This module provides dynamic schema inference using AI (Perplexity or local LLM)
|
|
6
|
-
to understand what fields an application should have based on natural language
|
|
7
|
-
descriptions. NO hardcoded app types or patterns - all inference is AI-driven.
|
|
8
|
-
|
|
9
|
-
Example:
|
|
10
|
-
User: "Build me a task tracker"
|
|
11
|
-
AI Response: {"entity": "Task", "fields": [
|
|
12
|
-
{"name": "title", "type": "string", "required": true},
|
|
13
|
-
{"name": "completed", "type": "boolean", "required": true},
|
|
14
|
-
{"name": "dueDate", "type": "datetime", "required": false}
|
|
15
|
-
]}
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
import json
|
|
19
|
-
import logging
|
|
20
|
-
import os
|
|
21
|
-
import re
|
|
22
|
-
from typing import Any, Dict, Optional
|
|
23
|
-
|
|
24
|
-
logger = logging.getLogger(__name__)
|
|
25
|
-
|
|
26
|
-
# Schema inference prompt - instructs AI to return minimal, appropriate fields
|
|
27
|
-
# NOTE: This prompt is optimized for both Perplexity and local LLMs
|
|
28
|
-
SCHEMA_INFERENCE_PROMPT = """You are a database schema designer. Analyze the app description and return the schema for the MAIN entity only.
|
|
29
|
-
|
|
30
|
-
CRITICAL RULES:
|
|
31
|
-
1. Return ONLY ONE entity - the main data entity (NOT User, NOT Auth)
|
|
32
|
-
2. Keep fields MINIMAL - only what's absolutely necessary
|
|
33
|
-
3. DO NOT include: id, createdAt, updatedAt, userId (auto-generated)
|
|
34
|
-
4. Think about INTUITIVE UX (e.g., Address Book apps NEED a "first name" string field)
|
|
35
|
-
|
|
36
|
-
EXACT OUTPUT FORMAT (single JSON object, no array):
|
|
37
|
-
{{"entity": "EntityName", "fields": [{{"name": "fieldName", "type": "type", "required": true}}]}}
|
|
38
|
-
|
|
39
|
-
Valid types: string, text, number, boolean, datetime, email, url
|
|
40
|
-
|
|
41
|
-
EXAMPLES:
|
|
42
|
-
- "todo app" -> {{"entity": "Todo", "fields": [{{"name": "title", "type": "string", "required": true}}, {{"name": "completed", "type": "boolean", "required": true}}]}}
|
|
43
|
-
- "contact manager" -> {{"entity": "Contact", "fields": [{{"name": "firstName", "type": "string", "required": true}}, {{"name": "lastName", "type": "string", "required": true}}, {{"name": "email", "type": "email", "required": false}}]}}
|
|
44
|
-
|
|
45
|
-
App description: "{query}"
|
|
46
|
-
|
|
47
|
-
Keep the schema dead simple, and focus on the most basic fields needed. For example, if the app is a contact manager, include "firstName" and "lastName" fields, but do NOT add address fields unless absolutely necessary.
|
|
48
|
-
|
|
49
|
-
Return ONLY the JSON object for the MAIN entity:"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def infer_schema(
|
|
53
|
-
user_query: str,
|
|
54
|
-
chat_sdk: Optional[Any] = None,
|
|
55
|
-
) -> Dict[str, Any]:
|
|
56
|
-
"""Infer schema fields from user's natural language query using AI.
|
|
57
|
-
|
|
58
|
-
Uses cascading fallback: Perplexity API -> Local LLM -> Generic fallback.
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
user_query: The user's app description (e.g., "build me a todo app")
|
|
62
|
-
chat_sdk: Optional ChatSDK instance for local LLM fallback
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
Dictionary with:
|
|
66
|
-
- entity: Suggested entity name (e.g., "Todo", "Task", "Contact")
|
|
67
|
-
- fields: List of field definitions with name, type, required
|
|
68
|
-
- source: Which method was used ("perplexity", "local_llm", "fallback")
|
|
69
|
-
"""
|
|
70
|
-
# Check if this looks like an app creation request
|
|
71
|
-
if not _is_app_creation_request(user_query):
|
|
72
|
-
logger.debug(f"Query doesn't appear to be app creation: {user_query[:50]}...")
|
|
73
|
-
return {"entity": None, "fields": [], "source": "skipped"}
|
|
74
|
-
|
|
75
|
-
# Try Perplexity first (if API key is set)
|
|
76
|
-
perplexity_key = os.getenv("PERPLEXITY_API_KEY")
|
|
77
|
-
if perplexity_key:
|
|
78
|
-
logger.info("Attempting schema inference via Perplexity")
|
|
79
|
-
result = _infer_via_perplexity(user_query)
|
|
80
|
-
if result.get("entity"):
|
|
81
|
-
result["source"] = "perplexity"
|
|
82
|
-
logger.info(
|
|
83
|
-
f"Perplexity inferred schema: {result['entity']} with {len(result['fields'])} fields"
|
|
84
|
-
)
|
|
85
|
-
return result
|
|
86
|
-
|
|
87
|
-
# Fall back to local LLM
|
|
88
|
-
if chat_sdk:
|
|
89
|
-
logger.debug("Attempting schema inference via local LLM")
|
|
90
|
-
result = _infer_via_local_llm(user_query, chat_sdk)
|
|
91
|
-
if result.get("entity"):
|
|
92
|
-
result["source"] = "local_llm"
|
|
93
|
-
logger.debug(
|
|
94
|
-
f"Local LLM inferred schema: {result['entity']} with {len(result['fields'])} fields"
|
|
95
|
-
)
|
|
96
|
-
return result
|
|
97
|
-
|
|
98
|
-
# Final fallback - no schema inference available
|
|
99
|
-
logger.warning("No schema inference available - returning empty schema")
|
|
100
|
-
return {"entity": None, "fields": [], "source": "fallback"}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _is_app_creation_request(query: str) -> bool:
|
|
104
|
-
"""Check if the query appears to be an app creation request.
|
|
105
|
-
|
|
106
|
-
Uses semantic patterns to detect app creation intent without hardcoding
|
|
107
|
-
specific app types.
|
|
108
|
-
"""
|
|
109
|
-
query_lower = query.lower()
|
|
110
|
-
|
|
111
|
-
# App creation indicators (verbs + objects)
|
|
112
|
-
creation_verbs = [
|
|
113
|
-
"build",
|
|
114
|
-
"create",
|
|
115
|
-
"make",
|
|
116
|
-
"develop",
|
|
117
|
-
"generate",
|
|
118
|
-
"design",
|
|
119
|
-
"implement",
|
|
120
|
-
]
|
|
121
|
-
app_objects = [
|
|
122
|
-
"app",
|
|
123
|
-
"application",
|
|
124
|
-
"crud",
|
|
125
|
-
"website",
|
|
126
|
-
"site",
|
|
127
|
-
"system",
|
|
128
|
-
"tracker",
|
|
129
|
-
"manager",
|
|
130
|
-
"dashboard",
|
|
131
|
-
]
|
|
132
|
-
|
|
133
|
-
# Check for creation verb + app object pattern
|
|
134
|
-
has_creation_verb = any(verb in query_lower for verb in creation_verbs)
|
|
135
|
-
has_app_object = any(obj in query_lower for obj in app_objects)
|
|
136
|
-
|
|
137
|
-
# Also check for "for managing X" or "to track X" patterns
|
|
138
|
-
management_patterns = [
|
|
139
|
-
"for managing",
|
|
140
|
-
"to manage",
|
|
141
|
-
"to track",
|
|
142
|
-
"for tracking",
|
|
143
|
-
"to organize",
|
|
144
|
-
"for organizing",
|
|
145
|
-
]
|
|
146
|
-
has_management_pattern = any(
|
|
147
|
-
pattern in query_lower for pattern in management_patterns
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
return (has_creation_verb and has_app_object) or has_management_pattern
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _infer_via_perplexity(query: str) -> Dict[str, Any]:
|
|
154
|
-
"""Infer schema using Perplexity API.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
query: User's app description
|
|
158
|
-
|
|
159
|
-
Returns:
|
|
160
|
-
Schema result or empty dict on failure
|
|
161
|
-
"""
|
|
162
|
-
try:
|
|
163
|
-
from gaia.mcp.external_services import get_perplexity_service
|
|
164
|
-
|
|
165
|
-
service = get_perplexity_service()
|
|
166
|
-
prompt = SCHEMA_INFERENCE_PROMPT.format(query=query)
|
|
167
|
-
result = service.search_web(prompt)
|
|
168
|
-
|
|
169
|
-
if result.get("success") and result.get("answer"):
|
|
170
|
-
return _parse_schema_response(result["answer"])
|
|
171
|
-
|
|
172
|
-
logger.warning(
|
|
173
|
-
f"Perplexity inference failed: {result.get('error', 'No answer')}"
|
|
174
|
-
)
|
|
175
|
-
return {"entity": None, "fields": []}
|
|
176
|
-
|
|
177
|
-
except Exception as e:
|
|
178
|
-
logger.warning(f"Perplexity inference error: {e}")
|
|
179
|
-
return {"entity": None, "fields": []}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def _infer_via_local_llm(query: str, chat_sdk: Any) -> Dict[str, Any]:
|
|
183
|
-
"""Infer schema using local LLM via ChatSDK.
|
|
184
|
-
|
|
185
|
-
Args:
|
|
186
|
-
query: User's app description
|
|
187
|
-
chat_sdk: ChatSDK instance for LLM calls
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
Schema result or empty dict on failure
|
|
191
|
-
"""
|
|
192
|
-
try:
|
|
193
|
-
prompt = SCHEMA_INFERENCE_PROMPT.format(query=query)
|
|
194
|
-
response = chat_sdk.send(prompt, max_tokens=500)
|
|
195
|
-
|
|
196
|
-
if response and response.text:
|
|
197
|
-
return _parse_schema_response(response.text)
|
|
198
|
-
|
|
199
|
-
logger.warning("Local LLM returned empty response")
|
|
200
|
-
return {"entity": None, "fields": []}
|
|
201
|
-
|
|
202
|
-
except Exception as e:
|
|
203
|
-
logger.warning(f"Local LLM inference error: {e}")
|
|
204
|
-
return {"entity": None, "fields": []}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def _parse_schema_response(response: str) -> Dict[str, Any]:
|
|
208
|
-
"""Parse schema JSON from AI response.
|
|
209
|
-
|
|
210
|
-
Handles various response formats including:
|
|
211
|
-
- Clean JSON object
|
|
212
|
-
- JSON array (takes first non-User entity)
|
|
213
|
-
- JSON in markdown code blocks
|
|
214
|
-
- JSON with surrounding text
|
|
215
|
-
|
|
216
|
-
Args:
|
|
217
|
-
response: Raw AI response text
|
|
218
|
-
|
|
219
|
-
Returns:
|
|
220
|
-
Parsed schema or empty dict on failure
|
|
221
|
-
"""
|
|
222
|
-
try:
|
|
223
|
-
# Try to extract JSON from the response
|
|
224
|
-
json_str = _extract_json(response)
|
|
225
|
-
if not json_str:
|
|
226
|
-
logger.warning(f"Could not extract JSON from response: {response[:100]}...")
|
|
227
|
-
return {"entity": None, "fields": []}
|
|
228
|
-
|
|
229
|
-
logger.debug(f"Extracted JSON: {json_str[:200]}...")
|
|
230
|
-
data = json.loads(json_str)
|
|
231
|
-
|
|
232
|
-
# Handle array response - take first non-User/Auth entity
|
|
233
|
-
if isinstance(data, list):
|
|
234
|
-
logger.debug(
|
|
235
|
-
f"Response is array with {len(data)} items, selecting main entity"
|
|
236
|
-
)
|
|
237
|
-
skip_names = {"user", "auth", "session", "account"}
|
|
238
|
-
for item in data:
|
|
239
|
-
if isinstance(item, dict):
|
|
240
|
-
name = item.get("entity", "").lower()
|
|
241
|
-
if name and name not in skip_names:
|
|
242
|
-
data = item
|
|
243
|
-
break
|
|
244
|
-
else:
|
|
245
|
-
# No suitable entity found, take first if available
|
|
246
|
-
data = data[0] if data else {}
|
|
247
|
-
|
|
248
|
-
# Validate it's a dict
|
|
249
|
-
if not isinstance(data, dict):
|
|
250
|
-
logger.warning(f"Expected dict but got {type(data).__name__}")
|
|
251
|
-
return {"entity": None, "fields": []}
|
|
252
|
-
|
|
253
|
-
# Validate required fields
|
|
254
|
-
entity = data.get("entity")
|
|
255
|
-
fields = data.get("fields", [])
|
|
256
|
-
|
|
257
|
-
if not entity or not isinstance(fields, list):
|
|
258
|
-
logger.warning(f"Invalid schema format: {data}")
|
|
259
|
-
return {"entity": None, "fields": []}
|
|
260
|
-
|
|
261
|
-
# Normalize fields, filter out auto-generated ones
|
|
262
|
-
normalized_fields = []
|
|
263
|
-
skip_fields = {"id", "createdat", "updatedat", "userid"}
|
|
264
|
-
for field in fields:
|
|
265
|
-
if isinstance(field, dict) and "name" in field:
|
|
266
|
-
if field["name"].lower() in skip_fields:
|
|
267
|
-
continue
|
|
268
|
-
normalized_fields.append(
|
|
269
|
-
{
|
|
270
|
-
"name": field["name"],
|
|
271
|
-
"type": field.get("type", "string"),
|
|
272
|
-
"required": field.get("required", False),
|
|
273
|
-
}
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
logger.debug(f"Parsed schema: {entity} with {len(normalized_fields)} fields")
|
|
277
|
-
return {"entity": entity, "fields": normalized_fields}
|
|
278
|
-
|
|
279
|
-
except json.JSONDecodeError as e:
|
|
280
|
-
logger.warning(f"JSON parse error: {e}")
|
|
281
|
-
return {"entity": None, "fields": []}
|
|
282
|
-
except Exception as e:
|
|
283
|
-
logger.warning(f"Schema parse error: {e}")
|
|
284
|
-
return {"entity": None, "fields": []}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def _extract_json(text: str) -> Optional[str]:
|
|
288
|
-
"""Extract JSON from text, handling various formats.
|
|
289
|
-
|
|
290
|
-
Args:
|
|
291
|
-
text: Raw text possibly containing JSON
|
|
292
|
-
|
|
293
|
-
Returns:
|
|
294
|
-
Extracted JSON string or None
|
|
295
|
-
"""
|
|
296
|
-
# Try to find JSON in code blocks first
|
|
297
|
-
code_block_patterns = [
|
|
298
|
-
r"```json\s*([\s\S]*?)\s*```",
|
|
299
|
-
r"```\s*([\s\S]*?)\s*```",
|
|
300
|
-
]
|
|
301
|
-
|
|
302
|
-
for pattern in code_block_patterns:
|
|
303
|
-
match = re.search(pattern, text)
|
|
304
|
-
if match:
|
|
305
|
-
return match.group(1).strip()
|
|
306
|
-
|
|
307
|
-
# Check if text starts with array or object to pick correct pattern
|
|
308
|
-
stripped = text.strip()
|
|
309
|
-
if stripped.startswith("["):
|
|
310
|
-
# JSON array - extract it
|
|
311
|
-
bracket_match = re.search(r"\[[\s\S]*\]", text)
|
|
312
|
-
if bracket_match:
|
|
313
|
-
return bracket_match.group(0)
|
|
314
|
-
|
|
315
|
-
# Try to find JSON object directly
|
|
316
|
-
brace_match = re.search(r"\{[\s\S]*\}", text)
|
|
317
|
-
if brace_match:
|
|
318
|
-
return brace_match.group(0)
|
|
319
|
-
|
|
320
|
-
# Return stripped text as last resort
|
|
321
|
-
return text.strip()
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def format_schema_context(schema_result: Dict[str, Any]) -> str:
|
|
325
|
-
"""Format inferred schema for injection into system prompt.
|
|
326
|
-
|
|
327
|
-
Args:
|
|
328
|
-
schema_result: Result from infer_schema()
|
|
329
|
-
|
|
330
|
-
Returns:
|
|
331
|
-
Formatted string for system prompt, or empty string if no schema
|
|
332
|
-
"""
|
|
333
|
-
entity = schema_result.get("entity")
|
|
334
|
-
fields = schema_result.get("fields", [])
|
|
335
|
-
source = schema_result.get("source", "unknown")
|
|
336
|
-
|
|
337
|
-
if not entity or not fields:
|
|
338
|
-
return ""
|
|
339
|
-
|
|
340
|
-
# Format fields for prompt
|
|
341
|
-
field_lines = []
|
|
342
|
-
for field in fields:
|
|
343
|
-
name = field["name"]
|
|
344
|
-
field_type = field["type"]
|
|
345
|
-
required = "required" if field.get("required") else "optional"
|
|
346
|
-
field_lines.append(f" - {name}: {field_type} ({required})")
|
|
347
|
-
|
|
348
|
-
fields_str = "\n".join(field_lines)
|
|
349
|
-
|
|
350
|
-
context = f"""
|
|
351
|
-
## AI-Inferred Schema (source: {source})
|
|
352
|
-
|
|
353
|
-
Based on the user's request, the following schema has been determined:
|
|
354
|
-
|
|
355
|
-
**Entity:** {entity}
|
|
356
|
-
**Fields:**
|
|
357
|
-
{fields_str}
|
|
358
|
-
|
|
359
|
-
IMPORTANT: Use these fields when creating the data model and components.
|
|
360
|
-
- Use `manage_data_model` with these field names and types
|
|
361
|
-
- Use the same fields consistently across all tools (API, components, forms)
|
|
362
|
-
- Boolean fields (like 'completed') should render as checkboxes in forms and lists
|
|
363
|
-
"""
|
|
364
|
-
|
|
365
|
-
return context
|
|
1
|
+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""AI-powered schema inference for Code Agent.
|
|
4
|
+
|
|
5
|
+
This module provides dynamic schema inference using AI (Perplexity or local LLM)
|
|
6
|
+
to understand what fields an application should have based on natural language
|
|
7
|
+
descriptions. NO hardcoded app types or patterns - all inference is AI-driven.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
User: "Build me a task tracker"
|
|
11
|
+
AI Response: {"entity": "Task", "fields": [
|
|
12
|
+
{"name": "title", "type": "string", "required": true},
|
|
13
|
+
{"name": "completed", "type": "boolean", "required": true},
|
|
14
|
+
{"name": "dueDate", "type": "datetime", "required": false}
|
|
15
|
+
]}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
from typing import Any, Dict, Optional
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Schema inference prompt - instructs AI to return minimal, appropriate fields
|
|
27
|
+
# NOTE: This prompt is optimized for both Perplexity and local LLMs
|
|
28
|
+
SCHEMA_INFERENCE_PROMPT = """You are a database schema designer. Analyze the app description and return the schema for the MAIN entity only.
|
|
29
|
+
|
|
30
|
+
CRITICAL RULES:
|
|
31
|
+
1. Return ONLY ONE entity - the main data entity (NOT User, NOT Auth)
|
|
32
|
+
2. Keep fields MINIMAL - only what's absolutely necessary
|
|
33
|
+
3. DO NOT include: id, createdAt, updatedAt, userId (auto-generated)
|
|
34
|
+
4. Think about INTUITIVE UX (e.g., Address Book apps NEED a "first name" string field)
|
|
35
|
+
|
|
36
|
+
EXACT OUTPUT FORMAT (single JSON object, no array):
|
|
37
|
+
{{"entity": "EntityName", "fields": [{{"name": "fieldName", "type": "type", "required": true}}]}}
|
|
38
|
+
|
|
39
|
+
Valid types: string, text, number, boolean, datetime, email, url
|
|
40
|
+
|
|
41
|
+
EXAMPLES:
|
|
42
|
+
- "todo app" -> {{"entity": "Todo", "fields": [{{"name": "title", "type": "string", "required": true}}, {{"name": "completed", "type": "boolean", "required": true}}]}}
|
|
43
|
+
- "contact manager" -> {{"entity": "Contact", "fields": [{{"name": "firstName", "type": "string", "required": true}}, {{"name": "lastName", "type": "string", "required": true}}, {{"name": "email", "type": "email", "required": false}}]}}
|
|
44
|
+
|
|
45
|
+
App description: "{query}"
|
|
46
|
+
|
|
47
|
+
Keep the schema dead simple, and focus on the most basic fields needed. For example, if the app is a contact manager, include "firstName" and "lastName" fields, but do NOT add address fields unless absolutely necessary.
|
|
48
|
+
|
|
49
|
+
Return ONLY the JSON object for the MAIN entity:"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def infer_schema(
|
|
53
|
+
user_query: str,
|
|
54
|
+
chat_sdk: Optional[Any] = None,
|
|
55
|
+
) -> Dict[str, Any]:
|
|
56
|
+
"""Infer schema fields from user's natural language query using AI.
|
|
57
|
+
|
|
58
|
+
Uses cascading fallback: Perplexity API -> Local LLM -> Generic fallback.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
user_query: The user's app description (e.g., "build me a todo app")
|
|
62
|
+
chat_sdk: Optional ChatSDK instance for local LLM fallback
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Dictionary with:
|
|
66
|
+
- entity: Suggested entity name (e.g., "Todo", "Task", "Contact")
|
|
67
|
+
- fields: List of field definitions with name, type, required
|
|
68
|
+
- source: Which method was used ("perplexity", "local_llm", "fallback")
|
|
69
|
+
"""
|
|
70
|
+
# Check if this looks like an app creation request
|
|
71
|
+
if not _is_app_creation_request(user_query):
|
|
72
|
+
logger.debug(f"Query doesn't appear to be app creation: {user_query[:50]}...")
|
|
73
|
+
return {"entity": None, "fields": [], "source": "skipped"}
|
|
74
|
+
|
|
75
|
+
# Try Perplexity first (if API key is set)
|
|
76
|
+
perplexity_key = os.getenv("PERPLEXITY_API_KEY")
|
|
77
|
+
if perplexity_key:
|
|
78
|
+
logger.info("Attempting schema inference via Perplexity")
|
|
79
|
+
result = _infer_via_perplexity(user_query)
|
|
80
|
+
if result.get("entity"):
|
|
81
|
+
result["source"] = "perplexity"
|
|
82
|
+
logger.info(
|
|
83
|
+
f"Perplexity inferred schema: {result['entity']} with {len(result['fields'])} fields"
|
|
84
|
+
)
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
# Fall back to local LLM
|
|
88
|
+
if chat_sdk:
|
|
89
|
+
logger.debug("Attempting schema inference via local LLM")
|
|
90
|
+
result = _infer_via_local_llm(user_query, chat_sdk)
|
|
91
|
+
if result.get("entity"):
|
|
92
|
+
result["source"] = "local_llm"
|
|
93
|
+
logger.debug(
|
|
94
|
+
f"Local LLM inferred schema: {result['entity']} with {len(result['fields'])} fields"
|
|
95
|
+
)
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
# Final fallback - no schema inference available
|
|
99
|
+
logger.warning("No schema inference available - returning empty schema")
|
|
100
|
+
return {"entity": None, "fields": [], "source": "fallback"}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _is_app_creation_request(query: str) -> bool:
|
|
104
|
+
"""Check if the query appears to be an app creation request.
|
|
105
|
+
|
|
106
|
+
Uses semantic patterns to detect app creation intent without hardcoding
|
|
107
|
+
specific app types.
|
|
108
|
+
"""
|
|
109
|
+
query_lower = query.lower()
|
|
110
|
+
|
|
111
|
+
# App creation indicators (verbs + objects)
|
|
112
|
+
creation_verbs = [
|
|
113
|
+
"build",
|
|
114
|
+
"create",
|
|
115
|
+
"make",
|
|
116
|
+
"develop",
|
|
117
|
+
"generate",
|
|
118
|
+
"design",
|
|
119
|
+
"implement",
|
|
120
|
+
]
|
|
121
|
+
app_objects = [
|
|
122
|
+
"app",
|
|
123
|
+
"application",
|
|
124
|
+
"crud",
|
|
125
|
+
"website",
|
|
126
|
+
"site",
|
|
127
|
+
"system",
|
|
128
|
+
"tracker",
|
|
129
|
+
"manager",
|
|
130
|
+
"dashboard",
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
# Check for creation verb + app object pattern
|
|
134
|
+
has_creation_verb = any(verb in query_lower for verb in creation_verbs)
|
|
135
|
+
has_app_object = any(obj in query_lower for obj in app_objects)
|
|
136
|
+
|
|
137
|
+
# Also check for "for managing X" or "to track X" patterns
|
|
138
|
+
management_patterns = [
|
|
139
|
+
"for managing",
|
|
140
|
+
"to manage",
|
|
141
|
+
"to track",
|
|
142
|
+
"for tracking",
|
|
143
|
+
"to organize",
|
|
144
|
+
"for organizing",
|
|
145
|
+
]
|
|
146
|
+
has_management_pattern = any(
|
|
147
|
+
pattern in query_lower for pattern in management_patterns
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return (has_creation_verb and has_app_object) or has_management_pattern
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _infer_via_perplexity(query: str) -> Dict[str, Any]:
|
|
154
|
+
"""Infer schema using Perplexity API.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
query: User's app description
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Schema result or empty dict on failure
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
from gaia.mcp.external_services import get_perplexity_service
|
|
164
|
+
|
|
165
|
+
service = get_perplexity_service()
|
|
166
|
+
prompt = SCHEMA_INFERENCE_PROMPT.format(query=query)
|
|
167
|
+
result = service.search_web(prompt)
|
|
168
|
+
|
|
169
|
+
if result.get("success") and result.get("answer"):
|
|
170
|
+
return _parse_schema_response(result["answer"])
|
|
171
|
+
|
|
172
|
+
logger.warning(
|
|
173
|
+
f"Perplexity inference failed: {result.get('error', 'No answer')}"
|
|
174
|
+
)
|
|
175
|
+
return {"entity": None, "fields": []}
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.warning(f"Perplexity inference error: {e}")
|
|
179
|
+
return {"entity": None, "fields": []}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _infer_via_local_llm(query: str, chat_sdk: Any) -> Dict[str, Any]:
|
|
183
|
+
"""Infer schema using local LLM via ChatSDK.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
query: User's app description
|
|
187
|
+
chat_sdk: ChatSDK instance for LLM calls
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Schema result or empty dict on failure
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
prompt = SCHEMA_INFERENCE_PROMPT.format(query=query)
|
|
194
|
+
response = chat_sdk.send(prompt, max_tokens=500)
|
|
195
|
+
|
|
196
|
+
if response and response.text:
|
|
197
|
+
return _parse_schema_response(response.text)
|
|
198
|
+
|
|
199
|
+
logger.warning("Local LLM returned empty response")
|
|
200
|
+
return {"entity": None, "fields": []}
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.warning(f"Local LLM inference error: {e}")
|
|
204
|
+
return {"entity": None, "fields": []}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _parse_schema_response(response: str) -> Dict[str, Any]:
|
|
208
|
+
"""Parse schema JSON from AI response.
|
|
209
|
+
|
|
210
|
+
Handles various response formats including:
|
|
211
|
+
- Clean JSON object
|
|
212
|
+
- JSON array (takes first non-User entity)
|
|
213
|
+
- JSON in markdown code blocks
|
|
214
|
+
- JSON with surrounding text
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
response: Raw AI response text
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Parsed schema or empty dict on failure
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
# Try to extract JSON from the response
|
|
224
|
+
json_str = _extract_json(response)
|
|
225
|
+
if not json_str:
|
|
226
|
+
logger.warning(f"Could not extract JSON from response: {response[:100]}...")
|
|
227
|
+
return {"entity": None, "fields": []}
|
|
228
|
+
|
|
229
|
+
logger.debug(f"Extracted JSON: {json_str[:200]}...")
|
|
230
|
+
data = json.loads(json_str)
|
|
231
|
+
|
|
232
|
+
# Handle array response - take first non-User/Auth entity
|
|
233
|
+
if isinstance(data, list):
|
|
234
|
+
logger.debug(
|
|
235
|
+
f"Response is array with {len(data)} items, selecting main entity"
|
|
236
|
+
)
|
|
237
|
+
skip_names = {"user", "auth", "session", "account"}
|
|
238
|
+
for item in data:
|
|
239
|
+
if isinstance(item, dict):
|
|
240
|
+
name = item.get("entity", "").lower()
|
|
241
|
+
if name and name not in skip_names:
|
|
242
|
+
data = item
|
|
243
|
+
break
|
|
244
|
+
else:
|
|
245
|
+
# No suitable entity found, take first if available
|
|
246
|
+
data = data[0] if data else {}
|
|
247
|
+
|
|
248
|
+
# Validate it's a dict
|
|
249
|
+
if not isinstance(data, dict):
|
|
250
|
+
logger.warning(f"Expected dict but got {type(data).__name__}")
|
|
251
|
+
return {"entity": None, "fields": []}
|
|
252
|
+
|
|
253
|
+
# Validate required fields
|
|
254
|
+
entity = data.get("entity")
|
|
255
|
+
fields = data.get("fields", [])
|
|
256
|
+
|
|
257
|
+
if not entity or not isinstance(fields, list):
|
|
258
|
+
logger.warning(f"Invalid schema format: {data}")
|
|
259
|
+
return {"entity": None, "fields": []}
|
|
260
|
+
|
|
261
|
+
# Normalize fields, filter out auto-generated ones
|
|
262
|
+
normalized_fields = []
|
|
263
|
+
skip_fields = {"id", "createdat", "updatedat", "userid"}
|
|
264
|
+
for field in fields:
|
|
265
|
+
if isinstance(field, dict) and "name" in field:
|
|
266
|
+
if field["name"].lower() in skip_fields:
|
|
267
|
+
continue
|
|
268
|
+
normalized_fields.append(
|
|
269
|
+
{
|
|
270
|
+
"name": field["name"],
|
|
271
|
+
"type": field.get("type", "string"),
|
|
272
|
+
"required": field.get("required", False),
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
logger.debug(f"Parsed schema: {entity} with {len(normalized_fields)} fields")
|
|
277
|
+
return {"entity": entity, "fields": normalized_fields}
|
|
278
|
+
|
|
279
|
+
except json.JSONDecodeError as e:
|
|
280
|
+
logger.warning(f"JSON parse error: {e}")
|
|
281
|
+
return {"entity": None, "fields": []}
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.warning(f"Schema parse error: {e}")
|
|
284
|
+
return {"entity": None, "fields": []}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _extract_json(text: str) -> Optional[str]:
|
|
288
|
+
"""Extract JSON from text, handling various formats.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
text: Raw text possibly containing JSON
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Extracted JSON string or None
|
|
295
|
+
"""
|
|
296
|
+
# Try to find JSON in code blocks first
|
|
297
|
+
code_block_patterns = [
|
|
298
|
+
r"```json\s*([\s\S]*?)\s*```",
|
|
299
|
+
r"```\s*([\s\S]*?)\s*```",
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
for pattern in code_block_patterns:
|
|
303
|
+
match = re.search(pattern, text)
|
|
304
|
+
if match:
|
|
305
|
+
return match.group(1).strip()
|
|
306
|
+
|
|
307
|
+
# Check if text starts with array or object to pick correct pattern
|
|
308
|
+
stripped = text.strip()
|
|
309
|
+
if stripped.startswith("["):
|
|
310
|
+
# JSON array - extract it
|
|
311
|
+
bracket_match = re.search(r"\[[\s\S]*\]", text)
|
|
312
|
+
if bracket_match:
|
|
313
|
+
return bracket_match.group(0)
|
|
314
|
+
|
|
315
|
+
# Try to find JSON object directly
|
|
316
|
+
brace_match = re.search(r"\{[\s\S]*\}", text)
|
|
317
|
+
if brace_match:
|
|
318
|
+
return brace_match.group(0)
|
|
319
|
+
|
|
320
|
+
# Return stripped text as last resort
|
|
321
|
+
return text.strip()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def format_schema_context(schema_result: Dict[str, Any]) -> str:
|
|
325
|
+
"""Format inferred schema for injection into system prompt.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
schema_result: Result from infer_schema()
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Formatted string for system prompt, or empty string if no schema
|
|
332
|
+
"""
|
|
333
|
+
entity = schema_result.get("entity")
|
|
334
|
+
fields = schema_result.get("fields", [])
|
|
335
|
+
source = schema_result.get("source", "unknown")
|
|
336
|
+
|
|
337
|
+
if not entity or not fields:
|
|
338
|
+
return ""
|
|
339
|
+
|
|
340
|
+
# Format fields for prompt
|
|
341
|
+
field_lines = []
|
|
342
|
+
for field in fields:
|
|
343
|
+
name = field["name"]
|
|
344
|
+
field_type = field["type"]
|
|
345
|
+
required = "required" if field.get("required") else "optional"
|
|
346
|
+
field_lines.append(f" - {name}: {field_type} ({required})")
|
|
347
|
+
|
|
348
|
+
fields_str = "\n".join(field_lines)
|
|
349
|
+
|
|
350
|
+
context = f"""
|
|
351
|
+
## AI-Inferred Schema (source: {source})
|
|
352
|
+
|
|
353
|
+
Based on the user's request, the following schema has been determined:
|
|
354
|
+
|
|
355
|
+
**Entity:** {entity}
|
|
356
|
+
**Fields:**
|
|
357
|
+
{fields_str}
|
|
358
|
+
|
|
359
|
+
IMPORTANT: Use these fields when creating the data model and components.
|
|
360
|
+
- Use `manage_data_model` with these field names and types
|
|
361
|
+
- Use the same fields consistently across all tools (API, components, forms)
|
|
362
|
+
- Boolean fields (like 'completed') should render as checkboxes in forms and lists
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
return context
|