amd-gaia 0.14.3__py3-none-any.whl → 0.15.1__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.14.3.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
- amd_gaia-0.15.1.dist-info/RECORD +178 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.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 +2177 -2177
- gaia/agents/base/api_agent.py +120 -120
- gaia/agents/base/console.py +1841 -1841
- gaia/agents/base/errors.py +237 -237
- gaia/agents/base/mcp_agent.py +86 -86
- gaia/agents/base/tools.py +83 -83
- gaia/agents/blender/agent.py +556 -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 +835 -835
- gaia/agents/chat/app.py +1058 -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 +1729 -1729
- gaia/agents/chat/tools/shell_tools.py +436 -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 +2036 -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 +642 -642
- gaia/agents/emr/__init__.py +8 -8
- gaia/agents/emr/agent.py +1506 -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 +1974 -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 +173 -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 +430 -430
- gaia/chat/prompts.py +522 -522
- gaia/chat/sdk.py +1228 -1225
- gaia/cli.py +5481 -5621
- 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/llm/__init__.py +9 -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 +3236 -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 +120 -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 +163 -163
- gaia/talk/app.py +289 -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.14.3.dist-info/RECORD +0 -168
- gaia/agents/code/app.py +0 -266
- gaia/llm/llm_client.py +0 -729
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
gaia/utils/parsing.py
CHANGED
|
@@ -1,223 +1,223 @@
|
|
|
1
|
-
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
Parsing utilities for GAIA agents.
|
|
6
|
-
|
|
7
|
-
Provides utilities for extracting structured data from LLM outputs,
|
|
8
|
-
document conversion, and field change detection.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import logging
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def extract_json_from_text(text: str) -> Optional[Dict[str, Any]]:
|
|
20
|
-
"""
|
|
21
|
-
Extract a JSON object from text that may contain surrounding content.
|
|
22
|
-
|
|
23
|
-
LLMs often return JSON embedded in explanatory text. This function
|
|
24
|
-
handles nested JSON objects correctly using balanced brace counting,
|
|
25
|
-
unlike simple regex approaches.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
text: Text potentially containing a JSON object.
|
|
29
|
-
|
|
30
|
-
Returns:
|
|
31
|
-
Parsed JSON as a dict, or None if no valid JSON found.
|
|
32
|
-
|
|
33
|
-
Example:
|
|
34
|
-
from gaia.utils import extract_json_from_text
|
|
35
|
-
|
|
36
|
-
# LLM output with surrounding text
|
|
37
|
-
llm_output = '''Here is the extracted data:
|
|
38
|
-
{"name": "John", "address": {"city": "Boston", "zip": "02101"}}
|
|
39
|
-
I hope this helps!'''
|
|
40
|
-
|
|
41
|
-
data = extract_json_from_text(llm_output)
|
|
42
|
-
# Returns: {"name": "John", "address": {"city": "Boston", "zip": "02101"}}
|
|
43
|
-
"""
|
|
44
|
-
if not text:
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
# Try parsing entire response as JSON first
|
|
48
|
-
try:
|
|
49
|
-
return json.loads(text)
|
|
50
|
-
except json.JSONDecodeError:
|
|
51
|
-
pass
|
|
52
|
-
|
|
53
|
-
# Look for JSON object in response (handles nested braces properly)
|
|
54
|
-
try:
|
|
55
|
-
# Find first {
|
|
56
|
-
start = text.find("{")
|
|
57
|
-
if start == -1:
|
|
58
|
-
logger.debug("No JSON object found in text")
|
|
59
|
-
return None
|
|
60
|
-
|
|
61
|
-
# Count braces to find matching close
|
|
62
|
-
brace_count = 0
|
|
63
|
-
for i, char in enumerate(text[start:], start):
|
|
64
|
-
if char == "{":
|
|
65
|
-
brace_count += 1
|
|
66
|
-
elif char == "}":
|
|
67
|
-
brace_count -= 1
|
|
68
|
-
if brace_count == 0:
|
|
69
|
-
json_str = text[start : i + 1]
|
|
70
|
-
return json.loads(json_str)
|
|
71
|
-
|
|
72
|
-
logger.debug(f"Failed to find matching brace in text: {text[:200]}...")
|
|
73
|
-
return None
|
|
74
|
-
|
|
75
|
-
except json.JSONDecodeError as e:
|
|
76
|
-
logger.debug(f"JSON decode error: {e}")
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def pdf_page_to_image(
|
|
81
|
-
path: Union[str, Path],
|
|
82
|
-
page: int = 0,
|
|
83
|
-
scale: float = 2.0,
|
|
84
|
-
) -> Optional[bytes]:
|
|
85
|
-
"""
|
|
86
|
-
Convert a PDF page to PNG image bytes.
|
|
87
|
-
|
|
88
|
-
Uses PyMuPDF (fitz) for high-quality rendering. The scale parameter
|
|
89
|
-
controls resolution - higher values produce larger, sharper images
|
|
90
|
-
suitable for OCR or VLM processing.
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
path: Path to the PDF file.
|
|
94
|
-
page: Page number to convert (0-indexed, default: first page).
|
|
95
|
-
scale: Resolution multiplier (default: 2.0 for 2x resolution).
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
PNG image as bytes, or None if conversion failed.
|
|
99
|
-
|
|
100
|
-
Raises:
|
|
101
|
-
ImportError hint if PyMuPDF is not installed.
|
|
102
|
-
|
|
103
|
-
Example:
|
|
104
|
-
from gaia.utils import pdf_page_to_image
|
|
105
|
-
|
|
106
|
-
# Convert first page at 2x resolution
|
|
107
|
-
image_bytes = pdf_page_to_image("form.pdf")
|
|
108
|
-
|
|
109
|
-
# Convert third page at 3x resolution for better OCR
|
|
110
|
-
image_bytes = pdf_page_to_image("document.pdf", page=2, scale=3.0)
|
|
111
|
-
"""
|
|
112
|
-
doc = None
|
|
113
|
-
try:
|
|
114
|
-
import fitz # PyMuPDF
|
|
115
|
-
|
|
116
|
-
doc = fitz.open(str(path))
|
|
117
|
-
if page >= len(doc):
|
|
118
|
-
logger.warning(f"Page {page} not in PDF (has {len(doc)} pages)")
|
|
119
|
-
return None
|
|
120
|
-
|
|
121
|
-
pdf_page = doc[page]
|
|
122
|
-
# Render at specified scale for better quality
|
|
123
|
-
mat = fitz.Matrix(scale, scale)
|
|
124
|
-
pix = pdf_page.get_pixmap(matrix=mat)
|
|
125
|
-
return pix.tobytes("png")
|
|
126
|
-
|
|
127
|
-
except ImportError:
|
|
128
|
-
logger.error("PyMuPDF required for PDF processing: pip install pymupdf")
|
|
129
|
-
return None
|
|
130
|
-
except Exception as e:
|
|
131
|
-
logger.error(f"Failed to convert PDF to image: {e}")
|
|
132
|
-
return None
|
|
133
|
-
finally:
|
|
134
|
-
if doc:
|
|
135
|
-
doc.close()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def detect_field_changes(
|
|
139
|
-
old_data: Dict[str, Any],
|
|
140
|
-
new_data: Dict[str, Any],
|
|
141
|
-
fields: Optional[List[str]] = None,
|
|
142
|
-
) -> List[Dict[str, Any]]:
|
|
143
|
-
"""
|
|
144
|
-
Detect changes between two dictionaries.
|
|
145
|
-
|
|
146
|
-
Compares field values and returns a list of changes. Useful for
|
|
147
|
-
tracking record updates in database-backed agents.
|
|
148
|
-
|
|
149
|
-
Args:
|
|
150
|
-
old_data: The original/existing data.
|
|
151
|
-
new_data: The new/updated data.
|
|
152
|
-
fields: Specific fields to compare. If None, compares all keys
|
|
153
|
-
present in either dict.
|
|
154
|
-
|
|
155
|
-
Returns:
|
|
156
|
-
List of change dicts with keys: field, old, new.
|
|
157
|
-
|
|
158
|
-
Example:
|
|
159
|
-
from gaia.utils import detect_field_changes
|
|
160
|
-
|
|
161
|
-
old = {"phone": "555-1234", "email": "old@test.com", "name": "John"}
|
|
162
|
-
new = {"phone": "555-9999", "email": "old@test.com", "name": "John"}
|
|
163
|
-
|
|
164
|
-
changes = detect_field_changes(old, new, ["phone", "email"])
|
|
165
|
-
# Returns: [{"field": "phone", "old": "555-1234", "new": "555-9999"}]
|
|
166
|
-
"""
|
|
167
|
-
changes = []
|
|
168
|
-
|
|
169
|
-
# If no fields specified, compare all keys from both dicts
|
|
170
|
-
if fields is None:
|
|
171
|
-
fields = list(set(old_data.keys()) | set(new_data.keys()))
|
|
172
|
-
|
|
173
|
-
for field in fields:
|
|
174
|
-
old_val = old_data.get(field)
|
|
175
|
-
new_val = new_data.get(field)
|
|
176
|
-
|
|
177
|
-
# Skip if both are empty/None
|
|
178
|
-
if not old_val and not new_val:
|
|
179
|
-
continue
|
|
180
|
-
|
|
181
|
-
# Detect change (normalize to string for comparison)
|
|
182
|
-
if str(old_val or "").strip() != str(new_val or "").strip():
|
|
183
|
-
changes.append(
|
|
184
|
-
{
|
|
185
|
-
"field": field,
|
|
186
|
-
"old": old_val,
|
|
187
|
-
"new": new_val,
|
|
188
|
-
}
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
return changes
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def validate_required_fields(
|
|
195
|
-
data: Dict[str, Any],
|
|
196
|
-
required_fields: List[str],
|
|
197
|
-
) -> Tuple[bool, List[str]]:
|
|
198
|
-
"""
|
|
199
|
-
Validate that required fields are present and non-empty.
|
|
200
|
-
|
|
201
|
-
Args:
|
|
202
|
-
data: The data dict to validate.
|
|
203
|
-
required_fields: List of field names that must be present.
|
|
204
|
-
|
|
205
|
-
Returns:
|
|
206
|
-
Tuple of (is_valid, missing_fields).
|
|
207
|
-
is_valid is True if all required fields are present.
|
|
208
|
-
missing_fields lists any fields that are missing or empty.
|
|
209
|
-
|
|
210
|
-
Example:
|
|
211
|
-
from gaia.utils import validate_required_fields
|
|
212
|
-
|
|
213
|
-
data = {"name": "John", "email": ""}
|
|
214
|
-
is_valid, missing = validate_required_fields(data, ["name", "email", "phone"])
|
|
215
|
-
# Returns: (False, ["email", "phone"])
|
|
216
|
-
"""
|
|
217
|
-
missing = []
|
|
218
|
-
for field in required_fields:
|
|
219
|
-
value = data.get(field)
|
|
220
|
-
if value is None or (isinstance(value, str) and not value.strip()):
|
|
221
|
-
missing.append(field)
|
|
222
|
-
|
|
223
|
-
return (len(missing) == 0, missing)
|
|
1
|
+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Parsing utilities for GAIA agents.
|
|
6
|
+
|
|
7
|
+
Provides utilities for extracting structured data from LLM outputs,
|
|
8
|
+
document conversion, and field change detection.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extract_json_from_text(text: str) -> Optional[Dict[str, Any]]:
|
|
20
|
+
"""
|
|
21
|
+
Extract a JSON object from text that may contain surrounding content.
|
|
22
|
+
|
|
23
|
+
LLMs often return JSON embedded in explanatory text. This function
|
|
24
|
+
handles nested JSON objects correctly using balanced brace counting,
|
|
25
|
+
unlike simple regex approaches.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
text: Text potentially containing a JSON object.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Parsed JSON as a dict, or None if no valid JSON found.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
from gaia.utils import extract_json_from_text
|
|
35
|
+
|
|
36
|
+
# LLM output with surrounding text
|
|
37
|
+
llm_output = '''Here is the extracted data:
|
|
38
|
+
{"name": "John", "address": {"city": "Boston", "zip": "02101"}}
|
|
39
|
+
I hope this helps!'''
|
|
40
|
+
|
|
41
|
+
data = extract_json_from_text(llm_output)
|
|
42
|
+
# Returns: {"name": "John", "address": {"city": "Boston", "zip": "02101"}}
|
|
43
|
+
"""
|
|
44
|
+
if not text:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
# Try parsing entire response as JSON first
|
|
48
|
+
try:
|
|
49
|
+
return json.loads(text)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
# Look for JSON object in response (handles nested braces properly)
|
|
54
|
+
try:
|
|
55
|
+
# Find first {
|
|
56
|
+
start = text.find("{")
|
|
57
|
+
if start == -1:
|
|
58
|
+
logger.debug("No JSON object found in text")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# Count braces to find matching close
|
|
62
|
+
brace_count = 0
|
|
63
|
+
for i, char in enumerate(text[start:], start):
|
|
64
|
+
if char == "{":
|
|
65
|
+
brace_count += 1
|
|
66
|
+
elif char == "}":
|
|
67
|
+
brace_count -= 1
|
|
68
|
+
if brace_count == 0:
|
|
69
|
+
json_str = text[start : i + 1]
|
|
70
|
+
return json.loads(json_str)
|
|
71
|
+
|
|
72
|
+
logger.debug(f"Failed to find matching brace in text: {text[:200]}...")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
except json.JSONDecodeError as e:
|
|
76
|
+
logger.debug(f"JSON decode error: {e}")
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def pdf_page_to_image(
|
|
81
|
+
path: Union[str, Path],
|
|
82
|
+
page: int = 0,
|
|
83
|
+
scale: float = 2.0,
|
|
84
|
+
) -> Optional[bytes]:
|
|
85
|
+
"""
|
|
86
|
+
Convert a PDF page to PNG image bytes.
|
|
87
|
+
|
|
88
|
+
Uses PyMuPDF (fitz) for high-quality rendering. The scale parameter
|
|
89
|
+
controls resolution - higher values produce larger, sharper images
|
|
90
|
+
suitable for OCR or VLM processing.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
path: Path to the PDF file.
|
|
94
|
+
page: Page number to convert (0-indexed, default: first page).
|
|
95
|
+
scale: Resolution multiplier (default: 2.0 for 2x resolution).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
PNG image as bytes, or None if conversion failed.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ImportError hint if PyMuPDF is not installed.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
from gaia.utils import pdf_page_to_image
|
|
105
|
+
|
|
106
|
+
# Convert first page at 2x resolution
|
|
107
|
+
image_bytes = pdf_page_to_image("form.pdf")
|
|
108
|
+
|
|
109
|
+
# Convert third page at 3x resolution for better OCR
|
|
110
|
+
image_bytes = pdf_page_to_image("document.pdf", page=2, scale=3.0)
|
|
111
|
+
"""
|
|
112
|
+
doc = None
|
|
113
|
+
try:
|
|
114
|
+
import fitz # PyMuPDF
|
|
115
|
+
|
|
116
|
+
doc = fitz.open(str(path))
|
|
117
|
+
if page >= len(doc):
|
|
118
|
+
logger.warning(f"Page {page} not in PDF (has {len(doc)} pages)")
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
pdf_page = doc[page]
|
|
122
|
+
# Render at specified scale for better quality
|
|
123
|
+
mat = fitz.Matrix(scale, scale)
|
|
124
|
+
pix = pdf_page.get_pixmap(matrix=mat)
|
|
125
|
+
return pix.tobytes("png")
|
|
126
|
+
|
|
127
|
+
except ImportError:
|
|
128
|
+
logger.error("PyMuPDF required for PDF processing: pip install pymupdf")
|
|
129
|
+
return None
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Failed to convert PDF to image: {e}")
|
|
132
|
+
return None
|
|
133
|
+
finally:
|
|
134
|
+
if doc:
|
|
135
|
+
doc.close()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def detect_field_changes(
|
|
139
|
+
old_data: Dict[str, Any],
|
|
140
|
+
new_data: Dict[str, Any],
|
|
141
|
+
fields: Optional[List[str]] = None,
|
|
142
|
+
) -> List[Dict[str, Any]]:
|
|
143
|
+
"""
|
|
144
|
+
Detect changes between two dictionaries.
|
|
145
|
+
|
|
146
|
+
Compares field values and returns a list of changes. Useful for
|
|
147
|
+
tracking record updates in database-backed agents.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
old_data: The original/existing data.
|
|
151
|
+
new_data: The new/updated data.
|
|
152
|
+
fields: Specific fields to compare. If None, compares all keys
|
|
153
|
+
present in either dict.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of change dicts with keys: field, old, new.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
from gaia.utils import detect_field_changes
|
|
160
|
+
|
|
161
|
+
old = {"phone": "555-1234", "email": "old@test.com", "name": "John"}
|
|
162
|
+
new = {"phone": "555-9999", "email": "old@test.com", "name": "John"}
|
|
163
|
+
|
|
164
|
+
changes = detect_field_changes(old, new, ["phone", "email"])
|
|
165
|
+
# Returns: [{"field": "phone", "old": "555-1234", "new": "555-9999"}]
|
|
166
|
+
"""
|
|
167
|
+
changes = []
|
|
168
|
+
|
|
169
|
+
# If no fields specified, compare all keys from both dicts
|
|
170
|
+
if fields is None:
|
|
171
|
+
fields = list(set(old_data.keys()) | set(new_data.keys()))
|
|
172
|
+
|
|
173
|
+
for field in fields:
|
|
174
|
+
old_val = old_data.get(field)
|
|
175
|
+
new_val = new_data.get(field)
|
|
176
|
+
|
|
177
|
+
# Skip if both are empty/None
|
|
178
|
+
if not old_val and not new_val:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Detect change (normalize to string for comparison)
|
|
182
|
+
if str(old_val or "").strip() != str(new_val or "").strip():
|
|
183
|
+
changes.append(
|
|
184
|
+
{
|
|
185
|
+
"field": field,
|
|
186
|
+
"old": old_val,
|
|
187
|
+
"new": new_val,
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return changes
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def validate_required_fields(
|
|
195
|
+
data: Dict[str, Any],
|
|
196
|
+
required_fields: List[str],
|
|
197
|
+
) -> Tuple[bool, List[str]]:
|
|
198
|
+
"""
|
|
199
|
+
Validate that required fields are present and non-empty.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
data: The data dict to validate.
|
|
203
|
+
required_fields: List of field names that must be present.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Tuple of (is_valid, missing_fields).
|
|
207
|
+
is_valid is True if all required fields are present.
|
|
208
|
+
missing_fields lists any fields that are missing or empty.
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
from gaia.utils import validate_required_fields
|
|
212
|
+
|
|
213
|
+
data = {"name": "John", "email": ""}
|
|
214
|
+
is_valid, missing = validate_required_fields(data, ["name", "email", "phone"])
|
|
215
|
+
# Returns: (False, ["email", "phone"])
|
|
216
|
+
"""
|
|
217
|
+
missing = []
|
|
218
|
+
for field in required_fields:
|
|
219
|
+
value = data.get(field)
|
|
220
|
+
if value is None or (isinstance(value, str) and not value.strip()):
|
|
221
|
+
missing.append(field)
|
|
222
|
+
|
|
223
|
+
return (len(missing) == 0, missing)
|
gaia/version.py
CHANGED
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
import logging
|
|
5
|
-
import os
|
|
6
|
-
import subprocess
|
|
7
|
-
from importlib.metadata import version as get_package_version_metadata
|
|
8
|
-
|
|
9
|
-
__version__ = "0.
|
|
10
|
-
|
|
11
|
-
# Lemonade version used across CI and installer
|
|
12
|
-
LEMONADE_VERSION = "9.1.0"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def get_package_version() -> str:
|
|
16
|
-
"""Get the installed package version from importlib.metadata.
|
|
17
|
-
|
|
18
|
-
Returns:
|
|
19
|
-
str: The package version string
|
|
20
|
-
"""
|
|
21
|
-
try:
|
|
22
|
-
return get_package_version_metadata("amd-gaia")
|
|
23
|
-
except Exception as e:
|
|
24
|
-
logging.warning(f"Failed to get package version: {e}")
|
|
25
|
-
return ""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_git_hash(hash_length: int = 8) -> str:
|
|
29
|
-
"""Get the current git hash.
|
|
30
|
-
Only used during build/installer process.
|
|
31
|
-
|
|
32
|
-
Args:
|
|
33
|
-
hash_length: Length of the hash to return
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
str: The git hash or 'unknown' if git is not available
|
|
37
|
-
"""
|
|
38
|
-
try:
|
|
39
|
-
git_hash = subprocess.check_output(
|
|
40
|
-
["git", "rev-parse", "--short", "HEAD"],
|
|
41
|
-
stderr=subprocess.DEVNULL,
|
|
42
|
-
text=True,
|
|
43
|
-
).strip()
|
|
44
|
-
return git_hash[:hash_length]
|
|
45
|
-
except Exception:
|
|
46
|
-
logging.warning("Failed to get Git hash")
|
|
47
|
-
return ""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def get_version_with_hash() -> str:
|
|
51
|
-
"""Get the full version string including git hash.
|
|
52
|
-
Only used during build/installer process.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
str: Version string in format 'v{version}+{hash}'
|
|
56
|
-
"""
|
|
57
|
-
git_hash = get_git_hash()
|
|
58
|
-
if git_hash:
|
|
59
|
-
return f"v{__version__}+{git_hash}"
|
|
60
|
-
else:
|
|
61
|
-
return f"v{__version__}"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def write_version_files() -> None:
|
|
65
|
-
"""Write version information to version.txt and installer/version.nsh files.
|
|
66
|
-
Only used during build/installer process."""
|
|
67
|
-
try:
|
|
68
|
-
# Write version with hash to version.txt
|
|
69
|
-
with open("version.txt", "w", encoding="utf-8") as f:
|
|
70
|
-
f.write(get_version_with_hash())
|
|
71
|
-
|
|
72
|
-
# Write version with hash to installer/version.nsh
|
|
73
|
-
installer_dir = os.path.join("installer")
|
|
74
|
-
os.makedirs(installer_dir, exist_ok=True)
|
|
75
|
-
with open(
|
|
76
|
-
os.path.join(installer_dir, "version.nsh"), "w", encoding="utf-8"
|
|
77
|
-
) as f:
|
|
78
|
-
f.write(f'!define GAIA_VERSION "{get_version_with_hash()}"\n')
|
|
79
|
-
f.write(f'!define LEMONADE_VERSION "{LEMONADE_VERSION}"\n')
|
|
80
|
-
|
|
81
|
-
print(
|
|
82
|
-
"Version files created successfully: version.txt and installer/version.nsh"
|
|
83
|
-
)
|
|
84
|
-
except Exception as e:
|
|
85
|
-
print(f"Failed to write version files: {str(e)}")
|
|
86
|
-
raise
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# For regular package usage - just version number
|
|
90
|
-
version = get_package_version()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def main():
|
|
94
|
-
"""Generate version files when the script is run directly.
|
|
95
|
-
Only used during build/installer process."""
|
|
96
|
-
write_version_files()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if __name__ == "__main__":
|
|
100
|
-
main()
|
|
1
|
+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from importlib.metadata import version as get_package_version_metadata
|
|
8
|
+
|
|
9
|
+
__version__ = "0.15.1"
|
|
10
|
+
|
|
11
|
+
# Lemonade version used across CI and installer
|
|
12
|
+
LEMONADE_VERSION = "9.1.0"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_package_version() -> str:
|
|
16
|
+
"""Get the installed package version from importlib.metadata.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str: The package version string
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
return get_package_version_metadata("amd-gaia")
|
|
23
|
+
except Exception as e:
|
|
24
|
+
logging.warning(f"Failed to get package version: {e}")
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_git_hash(hash_length: int = 8) -> str:
|
|
29
|
+
"""Get the current git hash.
|
|
30
|
+
Only used during build/installer process.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
hash_length: Length of the hash to return
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
str: The git hash or 'unknown' if git is not available
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
git_hash = subprocess.check_output(
|
|
40
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
41
|
+
stderr=subprocess.DEVNULL,
|
|
42
|
+
text=True,
|
|
43
|
+
).strip()
|
|
44
|
+
return git_hash[:hash_length]
|
|
45
|
+
except Exception:
|
|
46
|
+
logging.warning("Failed to get Git hash")
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_version_with_hash() -> str:
|
|
51
|
+
"""Get the full version string including git hash.
|
|
52
|
+
Only used during build/installer process.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
str: Version string in format 'v{version}+{hash}'
|
|
56
|
+
"""
|
|
57
|
+
git_hash = get_git_hash()
|
|
58
|
+
if git_hash:
|
|
59
|
+
return f"v{__version__}+{git_hash}"
|
|
60
|
+
else:
|
|
61
|
+
return f"v{__version__}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def write_version_files() -> None:
|
|
65
|
+
"""Write version information to version.txt and installer/version.nsh files.
|
|
66
|
+
Only used during build/installer process."""
|
|
67
|
+
try:
|
|
68
|
+
# Write version with hash to version.txt
|
|
69
|
+
with open("version.txt", "w", encoding="utf-8") as f:
|
|
70
|
+
f.write(get_version_with_hash())
|
|
71
|
+
|
|
72
|
+
# Write version with hash to installer/version.nsh
|
|
73
|
+
installer_dir = os.path.join("installer")
|
|
74
|
+
os.makedirs(installer_dir, exist_ok=True)
|
|
75
|
+
with open(
|
|
76
|
+
os.path.join(installer_dir, "version.nsh"), "w", encoding="utf-8"
|
|
77
|
+
) as f:
|
|
78
|
+
f.write(f'!define GAIA_VERSION "{get_version_with_hash()}"\n')
|
|
79
|
+
f.write(f'!define LEMONADE_VERSION "{LEMONADE_VERSION}"\n')
|
|
80
|
+
|
|
81
|
+
print(
|
|
82
|
+
"Version files created successfully: version.txt and installer/version.nsh"
|
|
83
|
+
)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Failed to write version files: {str(e)}")
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# For regular package usage - just version number
|
|
90
|
+
version = get_package_version()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main():
|
|
94
|
+
"""Generate version files when the script is run directly.
|
|
95
|
+
Only used during build/installer process."""
|
|
96
|
+
write_version_files()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
main()
|