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/api/openai_server.py
CHANGED
|
@@ -1,575 +1,575 @@
|
|
|
1
|
-
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
"""
|
|
4
|
-
OpenAI-compatible API server for GAIA
|
|
5
|
-
|
|
6
|
-
This module provides a FastAPI server that exposes GAIA agents via
|
|
7
|
-
OpenAI-compatible endpoints, allowing VSCode and other tools to use
|
|
8
|
-
GAIA agents as if they were OpenAI models.
|
|
9
|
-
|
|
10
|
-
Endpoints:
|
|
11
|
-
POST /v1/chat/completions - Create chat completion (streaming and non-streaming)
|
|
12
|
-
GET /v1/models - List available models (GAIA agents)
|
|
13
|
-
GET /health - Health check
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import asyncio
|
|
17
|
-
import json
|
|
18
|
-
import logging
|
|
19
|
-
import os
|
|
20
|
-
import time
|
|
21
|
-
import uuid
|
|
22
|
-
from typing import AsyncGenerator
|
|
23
|
-
|
|
24
|
-
from fastapi import FastAPI, HTTPException, Request
|
|
25
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
26
|
-
from fastapi.responses import StreamingResponse
|
|
27
|
-
|
|
28
|
-
from gaia.agents.base.api_agent import ApiAgent
|
|
29
|
-
|
|
30
|
-
from .agent_registry import registry
|
|
31
|
-
from .schemas import (
|
|
32
|
-
ChatCompletionChoice,
|
|
33
|
-
ChatCompletionRequest,
|
|
34
|
-
ChatCompletionResponse,
|
|
35
|
-
ChatCompletionResponseMessage,
|
|
36
|
-
ModelListResponse,
|
|
37
|
-
UsageInfo,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
# Configure logging
|
|
41
|
-
logger = logging.getLogger(__name__)
|
|
42
|
-
|
|
43
|
-
# Set logger level based on debug flag
|
|
44
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
45
|
-
logger.setLevel(logging.DEBUG)
|
|
46
|
-
logger.info("Debug logging enabled for API server")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def extract_workspace_root(messages):
|
|
50
|
-
"""
|
|
51
|
-
Extract workspace root path from GitHub Copilot messages.
|
|
52
|
-
|
|
53
|
-
GitHub Copilot includes workspace info in messages like:
|
|
54
|
-
<workspace_info>
|
|
55
|
-
I am working in a workspace with the following folders:
|
|
56
|
-
- /Users/username/path/to/workspace
|
|
57
|
-
</workspace_info>
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
messages: List of ChatMessage objects
|
|
61
|
-
|
|
62
|
-
Returns:
|
|
63
|
-
str: Workspace root path, or None if not found
|
|
64
|
-
"""
|
|
65
|
-
import re
|
|
66
|
-
|
|
67
|
-
for msg in messages:
|
|
68
|
-
if msg.role == "user" and msg.content:
|
|
69
|
-
# Look for workspace_info section
|
|
70
|
-
workspace_match = re.search(
|
|
71
|
-
r"<workspace_info>.*?following folders:\s*\n\s*-\s*([^\s\n]+)",
|
|
72
|
-
msg.content,
|
|
73
|
-
re.DOTALL,
|
|
74
|
-
)
|
|
75
|
-
if workspace_match:
|
|
76
|
-
return workspace_match.group(1).strip()
|
|
77
|
-
|
|
78
|
-
return None
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# Initialize FastAPI app
|
|
82
|
-
app = FastAPI(
|
|
83
|
-
title="GAIA OpenAI-Compatible API",
|
|
84
|
-
description="OpenAI-compatible API for GAIA agents",
|
|
85
|
-
version="1.0.0",
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
# CORS middleware - allow all origins for development
|
|
89
|
-
app.add_middleware(
|
|
90
|
-
CORSMiddleware,
|
|
91
|
-
allow_origins=["*"],
|
|
92
|
-
allow_credentials=True,
|
|
93
|
-
allow_methods=["*"],
|
|
94
|
-
allow_headers=["*"],
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# Raw request logging middleware (debug mode only)
|
|
99
|
-
@app.middleware("http")
|
|
100
|
-
async def log_raw_requests(request: Request, call_next):
|
|
101
|
-
"""
|
|
102
|
-
Middleware to log raw HTTP requests when debug mode is enabled.
|
|
103
|
-
For streaming endpoints, only log headers to avoid breaking SSE.
|
|
104
|
-
"""
|
|
105
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
106
|
-
logger.debug("=" * 80)
|
|
107
|
-
logger.debug("📥 RAW HTTP REQUEST")
|
|
108
|
-
logger.debug("=" * 80)
|
|
109
|
-
logger.debug(f"Path: {request.url.path}")
|
|
110
|
-
logger.debug(f"Method: {request.method}")
|
|
111
|
-
logger.debug("Headers:")
|
|
112
|
-
for name, value in request.headers.items():
|
|
113
|
-
logger.debug(f" {name}: {value}")
|
|
114
|
-
|
|
115
|
-
# DON'T read body for streaming endpoints - it breaks ASGI message flow
|
|
116
|
-
# Per FastAPI docs: "Never read the request body in middleware for streaming responses"
|
|
117
|
-
if request.url.path == "/v1/chat/completions" and request.method == "POST":
|
|
118
|
-
logger.debug(
|
|
119
|
-
"Body: [Skipped for streaming endpoint - prevents ASGI message flow disruption]"
|
|
120
|
-
)
|
|
121
|
-
else:
|
|
122
|
-
# Safe to read body for non-streaming endpoints
|
|
123
|
-
body_bytes = await request.body()
|
|
124
|
-
logger.debug(f"Body (raw bytes length): {len(body_bytes)}")
|
|
125
|
-
if body_bytes:
|
|
126
|
-
try:
|
|
127
|
-
body_str = body_bytes.decode("utf-8")
|
|
128
|
-
logger.debug("Body (decoded UTF-8):")
|
|
129
|
-
logger.debug(body_str)
|
|
130
|
-
# Try to pretty-print JSON
|
|
131
|
-
try:
|
|
132
|
-
body_json = json.loads(body_str)
|
|
133
|
-
logger.debug("Body (parsed JSON):")
|
|
134
|
-
logger.debug(json.dumps(body_json, indent=2))
|
|
135
|
-
except json.JSONDecodeError:
|
|
136
|
-
pass
|
|
137
|
-
except UnicodeDecodeError:
|
|
138
|
-
logger.debug("Body contains non-UTF-8 data")
|
|
139
|
-
|
|
140
|
-
logger.debug("=" * 80)
|
|
141
|
-
|
|
142
|
-
response = await call_next(request)
|
|
143
|
-
return response
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@app.post("/v1/chat/completions")
|
|
147
|
-
async def create_chat_completion(request: ChatCompletionRequest):
|
|
148
|
-
"""
|
|
149
|
-
Create chat completion (OpenAI-compatible endpoint).
|
|
150
|
-
|
|
151
|
-
Supports both streaming (SSE) and non-streaming responses.
|
|
152
|
-
|
|
153
|
-
Args:
|
|
154
|
-
request: Chat completion request with model, messages, and options
|
|
155
|
-
|
|
156
|
-
Returns:
|
|
157
|
-
For non-streaming: ChatCompletionResponse
|
|
158
|
-
For streaming: StreamingResponse with SSE chunks
|
|
159
|
-
|
|
160
|
-
Raises:
|
|
161
|
-
HTTPException 404: Model not found
|
|
162
|
-
HTTPException 400: No user message in request
|
|
163
|
-
|
|
164
|
-
Example:
|
|
165
|
-
Non-streaming:
|
|
166
|
-
```
|
|
167
|
-
POST /v1/chat/completions
|
|
168
|
-
{
|
|
169
|
-
"model": "gaia-code",
|
|
170
|
-
"messages": [{"role": "user", "content": "Write hello world"}],
|
|
171
|
-
"stream": false
|
|
172
|
-
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Streaming:
|
|
176
|
-
```
|
|
177
|
-
POST /v1/chat/completions
|
|
178
|
-
{
|
|
179
|
-
"model": "gaia-code",
|
|
180
|
-
"messages": [{"role": "user", "content": "Write hello world"}],
|
|
181
|
-
"stream": true
|
|
182
|
-
}
|
|
183
|
-
```
|
|
184
|
-
"""
|
|
185
|
-
# Debug logging: trace incoming request
|
|
186
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
187
|
-
logger.debug("=" * 80)
|
|
188
|
-
logger.debug("📥 INCOMING CHAT COMPLETION REQUEST")
|
|
189
|
-
logger.debug("=" * 80)
|
|
190
|
-
logger.debug(f"Model: {request.model}")
|
|
191
|
-
logger.debug(f"Stream: {request.stream}")
|
|
192
|
-
logger.debug(f"Message count: {len(request.messages)}")
|
|
193
|
-
logger.debug("-" * 80)
|
|
194
|
-
|
|
195
|
-
for i, msg in enumerate(request.messages):
|
|
196
|
-
logger.debug(f"Message {i}:")
|
|
197
|
-
logger.debug(f" Role: {msg.role}")
|
|
198
|
-
# Preview content (truncate if too long)
|
|
199
|
-
content_preview = (
|
|
200
|
-
msg.content[:500] if len(msg.content) > 500 else msg.content
|
|
201
|
-
)
|
|
202
|
-
if len(msg.content) > 500:
|
|
203
|
-
content_preview += (
|
|
204
|
-
f"\n ... (truncated, total length: {len(msg.content)} chars)"
|
|
205
|
-
)
|
|
206
|
-
logger.debug(f" Content:\n{content_preview}")
|
|
207
|
-
logger.debug("-" * 40)
|
|
208
|
-
|
|
209
|
-
# Log additional request parameters
|
|
210
|
-
logger.debug("Request parameters:")
|
|
211
|
-
logger.debug(f" temperature: {getattr(request, 'temperature', 'not set')}")
|
|
212
|
-
logger.debug(f" max_tokens: {getattr(request, 'max_tokens', 'not set')}")
|
|
213
|
-
logger.debug(f" top_p: {getattr(request, 'top_p', 'not set')}")
|
|
214
|
-
logger.debug("=" * 80)
|
|
215
|
-
|
|
216
|
-
# Validate model exists
|
|
217
|
-
if not registry.model_exists(request.model):
|
|
218
|
-
raise HTTPException(
|
|
219
|
-
status_code=404, detail=f"Model '{request.model}' not found"
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
# Extract workspace root from messages (for converting relative paths to absolute)
|
|
223
|
-
workspace_root = extract_workspace_root(request.messages)
|
|
224
|
-
if os.environ.get("GAIA_API_DEBUG") == "1" and workspace_root:
|
|
225
|
-
logger.debug(f"📁 Extracted workspace root: {workspace_root}")
|
|
226
|
-
|
|
227
|
-
# Extract user query from messages (get last user message)
|
|
228
|
-
user_message = next(
|
|
229
|
-
(m.content for m in reversed(request.messages) if m.role == "user"), None
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
if not user_message:
|
|
233
|
-
raise HTTPException(
|
|
234
|
-
status_code=400, detail="No user message found in messages array"
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
# Debug logging: show what we're passing to the agent
|
|
238
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
239
|
-
logger.debug("🔄 EXTRACTED FOR AGENT:")
|
|
240
|
-
logger.debug(f"Passing to agent: {user_message[:500]}...")
|
|
241
|
-
if len(user_message) > 500:
|
|
242
|
-
logger.debug(f"(Total length: {len(user_message)} chars)")
|
|
243
|
-
logger.debug("=" * 80)
|
|
244
|
-
|
|
245
|
-
# Get agent instance for this model
|
|
246
|
-
try:
|
|
247
|
-
agent = registry.get_agent(request.model)
|
|
248
|
-
except ValueError as e:
|
|
249
|
-
raise HTTPException(status_code=404, detail=str(e))
|
|
250
|
-
|
|
251
|
-
# Handle streaming vs non-streaming
|
|
252
|
-
if request.stream:
|
|
253
|
-
# Debug logging for streaming mode
|
|
254
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
255
|
-
logger.debug("🌊 Using STREAMING mode")
|
|
256
|
-
|
|
257
|
-
return StreamingResponse(
|
|
258
|
-
create_sse_stream(
|
|
259
|
-
agent, user_message, request.model, workspace_root=workspace_root
|
|
260
|
-
),
|
|
261
|
-
media_type="text/event-stream",
|
|
262
|
-
headers={
|
|
263
|
-
"Cache-Control": "no-cache",
|
|
264
|
-
"Connection": "keep-alive",
|
|
265
|
-
"X-Accel-Buffering": "no", # Disable proxy buffering
|
|
266
|
-
},
|
|
267
|
-
)
|
|
268
|
-
else:
|
|
269
|
-
# Debug logging for non-streaming mode
|
|
270
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
271
|
-
logger.debug("📦 Using NON-STREAMING mode")
|
|
272
|
-
|
|
273
|
-
# Process query synchronously with workspace root
|
|
274
|
-
result = agent.process_query(user_message, workspace_root=workspace_root)
|
|
275
|
-
|
|
276
|
-
# Debug logging: show what agent returned
|
|
277
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
278
|
-
logger.debug("=" * 80)
|
|
279
|
-
logger.debug("📤 AGENT RESPONSE (NON-STREAMING)")
|
|
280
|
-
logger.debug("=" * 80)
|
|
281
|
-
logger.debug(f"Result type: {type(result)}")
|
|
282
|
-
logger.debug(
|
|
283
|
-
f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
|
|
284
|
-
)
|
|
285
|
-
logger.debug(
|
|
286
|
-
f"Status: {result.get('status') if isinstance(result, dict) else 'N/A'}"
|
|
287
|
-
)
|
|
288
|
-
logger.debug(
|
|
289
|
-
f"Steps taken: {result.get('steps_taken') if isinstance(result, dict) else 'N/A'}"
|
|
290
|
-
)
|
|
291
|
-
result_preview = (
|
|
292
|
-
str(result.get("result", ""))[:200]
|
|
293
|
-
if isinstance(result, dict)
|
|
294
|
-
else str(result)[:200]
|
|
295
|
-
)
|
|
296
|
-
logger.debug(f"Result preview: {result_preview}...")
|
|
297
|
-
logger.debug("=" * 80)
|
|
298
|
-
|
|
299
|
-
# Extract content from result
|
|
300
|
-
content = result.get("result", str(result))
|
|
301
|
-
|
|
302
|
-
# Estimate tokens
|
|
303
|
-
if isinstance(agent, ApiAgent):
|
|
304
|
-
prompt_tokens = agent.estimate_tokens(user_message)
|
|
305
|
-
completion_tokens = agent.estimate_tokens(content)
|
|
306
|
-
else:
|
|
307
|
-
prompt_tokens = len(user_message) // 4
|
|
308
|
-
completion_tokens = len(content) // 4
|
|
309
|
-
|
|
310
|
-
return ChatCompletionResponse(
|
|
311
|
-
id=f"chatcmpl-{uuid.uuid4().hex[:24]}",
|
|
312
|
-
object="chat.completion",
|
|
313
|
-
created=int(time.time()),
|
|
314
|
-
model=request.model,
|
|
315
|
-
choices=[
|
|
316
|
-
ChatCompletionChoice(
|
|
317
|
-
index=0,
|
|
318
|
-
message=ChatCompletionResponseMessage(
|
|
319
|
-
role="assistant",
|
|
320
|
-
content=content,
|
|
321
|
-
),
|
|
322
|
-
finish_reason="stop",
|
|
323
|
-
)
|
|
324
|
-
],
|
|
325
|
-
usage=UsageInfo(
|
|
326
|
-
prompt_tokens=prompt_tokens,
|
|
327
|
-
completion_tokens=completion_tokens,
|
|
328
|
-
total_tokens=prompt_tokens + completion_tokens,
|
|
329
|
-
),
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
async def create_sse_stream(
|
|
334
|
-
agent, query: str, model: str, workspace_root: str = None
|
|
335
|
-
) -> AsyncGenerator[str, None]:
|
|
336
|
-
"""
|
|
337
|
-
Create Server-Sent Events stream for chat completion.
|
|
338
|
-
|
|
339
|
-
This function processes the agent query in a thread pool (to avoid blocking)
|
|
340
|
-
and streams agent progress events in real-time via the SSEOutputHandler.
|
|
341
|
-
|
|
342
|
-
Args:
|
|
343
|
-
agent: Agent instance (with SSEOutputHandler)
|
|
344
|
-
query: User query string
|
|
345
|
-
model: Model ID
|
|
346
|
-
workspace_root: Optional workspace root path for absolute file paths
|
|
347
|
-
|
|
348
|
-
Yields:
|
|
349
|
-
SSE-formatted chunks with "data: " prefix
|
|
350
|
-
|
|
351
|
-
Example output:
|
|
352
|
-
data: {"id":"chatcmpl-123","object":"chat.completion.chunk",...}
|
|
353
|
-
data: {"id":"chatcmpl-123","object":"chat.completion.chunk",...}
|
|
354
|
-
data: [DONE]
|
|
355
|
-
"""
|
|
356
|
-
# Debug logging - FIRST LINE to confirm generator starts
|
|
357
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
358
|
-
logger.debug("🎬 Generator started! Client is consuming the stream.")
|
|
359
|
-
|
|
360
|
-
completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
|
|
361
|
-
created = int(time.time())
|
|
362
|
-
|
|
363
|
-
# First chunk with role
|
|
364
|
-
first_chunk = {
|
|
365
|
-
"id": completion_id,
|
|
366
|
-
"object": "chat.completion.chunk",
|
|
367
|
-
"created": created,
|
|
368
|
-
"model": model,
|
|
369
|
-
"choices": [
|
|
370
|
-
{
|
|
371
|
-
"index": 0,
|
|
372
|
-
"delta": {"role": "assistant", "content": ""},
|
|
373
|
-
"finish_reason": None,
|
|
374
|
-
}
|
|
375
|
-
],
|
|
376
|
-
}
|
|
377
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
378
|
-
logger.debug(f"📤 Sending first chunk: {json.dumps(first_chunk)}")
|
|
379
|
-
yield f"data: {json.dumps(first_chunk)}\n\n"
|
|
380
|
-
|
|
381
|
-
# Debug logging
|
|
382
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
383
|
-
logger.debug("🔄 Starting agent query processing in thread pool...")
|
|
384
|
-
|
|
385
|
-
# Process query in thread pool to avoid blocking event loop
|
|
386
|
-
loop = asyncio.get_event_loop()
|
|
387
|
-
|
|
388
|
-
# Get the SSEOutputHandler from the agent (try output_handler first, fall back to console)
|
|
389
|
-
output_handler = getattr(agent, "output_handler", None) or getattr(
|
|
390
|
-
agent, "console", None
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
try:
|
|
394
|
-
# Start processing in background
|
|
395
|
-
task = loop.run_in_executor(
|
|
396
|
-
None, lambda: agent.process_query(query, workspace_root=workspace_root)
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
# Stream events as they are generated
|
|
400
|
-
while not task.done():
|
|
401
|
-
# Check for new events from the output handler
|
|
402
|
-
if hasattr(output_handler, "has_events") and output_handler.has_events():
|
|
403
|
-
events = output_handler.get_events()
|
|
404
|
-
|
|
405
|
-
for event in events:
|
|
406
|
-
event_type = event.get("type", "message")
|
|
407
|
-
|
|
408
|
-
# Check if this event should be streamed to client
|
|
409
|
-
if not output_handler.should_stream_as_content(event_type):
|
|
410
|
-
# Still log it in debug mode
|
|
411
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
412
|
-
logger.debug(f"📝 Skipping event: {event_type}")
|
|
413
|
-
continue
|
|
414
|
-
|
|
415
|
-
# Format event as clean content
|
|
416
|
-
content_text = output_handler.format_event_as_content(event)
|
|
417
|
-
|
|
418
|
-
# Skip empty content (filtered events)
|
|
419
|
-
if not content_text:
|
|
420
|
-
continue
|
|
421
|
-
|
|
422
|
-
content_chunk = {
|
|
423
|
-
"id": completion_id,
|
|
424
|
-
"object": "chat.completion.chunk",
|
|
425
|
-
"created": created,
|
|
426
|
-
"model": model,
|
|
427
|
-
"choices": [
|
|
428
|
-
{
|
|
429
|
-
"index": 0,
|
|
430
|
-
"delta": {"content": content_text},
|
|
431
|
-
"finish_reason": None,
|
|
432
|
-
}
|
|
433
|
-
],
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
437
|
-
logger.debug(
|
|
438
|
-
f"📤 Streaming event: {event_type} -> {content_text[:100]}"
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
yield f"data: {json.dumps(content_chunk)}\n\n"
|
|
442
|
-
|
|
443
|
-
# Small delay to avoid busy waiting
|
|
444
|
-
await asyncio.sleep(0.1)
|
|
445
|
-
|
|
446
|
-
# Get the final result
|
|
447
|
-
result = await task
|
|
448
|
-
|
|
449
|
-
# Get any remaining events
|
|
450
|
-
if hasattr(output_handler, "has_events") and output_handler.has_events():
|
|
451
|
-
events = output_handler.get_events()
|
|
452
|
-
for event in events:
|
|
453
|
-
event_type = event.get("type", "message")
|
|
454
|
-
|
|
455
|
-
# Check if this event should be streamed
|
|
456
|
-
if not output_handler.should_stream_as_content(event_type):
|
|
457
|
-
continue
|
|
458
|
-
|
|
459
|
-
# Format event as clean content
|
|
460
|
-
content_text = output_handler.format_event_as_content(event)
|
|
461
|
-
|
|
462
|
-
# Skip empty content
|
|
463
|
-
if not content_text:
|
|
464
|
-
continue
|
|
465
|
-
|
|
466
|
-
content_chunk = {
|
|
467
|
-
"id": completion_id,
|
|
468
|
-
"object": "chat.completion.chunk",
|
|
469
|
-
"created": created,
|
|
470
|
-
"model": model,
|
|
471
|
-
"choices": [
|
|
472
|
-
{
|
|
473
|
-
"index": 0,
|
|
474
|
-
"delta": {"content": content_text},
|
|
475
|
-
"finish_reason": None,
|
|
476
|
-
}
|
|
477
|
-
],
|
|
478
|
-
}
|
|
479
|
-
yield f"data: {json.dumps(content_chunk)}\n\n"
|
|
480
|
-
|
|
481
|
-
# Debug logging: show what agent returned
|
|
482
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
483
|
-
logger.debug("=" * 80)
|
|
484
|
-
logger.debug("📤 AGENT RESPONSE (STREAMING)")
|
|
485
|
-
logger.debug("=" * 80)
|
|
486
|
-
logger.debug(f"Result type: {type(result)}")
|
|
487
|
-
logger.debug(
|
|
488
|
-
f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
|
|
489
|
-
)
|
|
490
|
-
logger.debug(
|
|
491
|
-
f"Status: {result.get('status') if isinstance(result, dict) else 'N/A'}"
|
|
492
|
-
)
|
|
493
|
-
logger.debug(
|
|
494
|
-
f"Steps taken: {result.get('steps_taken') if isinstance(result, dict) else 'N/A'}"
|
|
495
|
-
)
|
|
496
|
-
result_preview = (
|
|
497
|
-
str(result.get("result", ""))[:200]
|
|
498
|
-
if isinstance(result, dict)
|
|
499
|
-
else str(result)[:200]
|
|
500
|
-
)
|
|
501
|
-
logger.debug(f"Result preview: {result_preview}...")
|
|
502
|
-
logger.debug("=" * 80)
|
|
503
|
-
|
|
504
|
-
except Exception as e:
|
|
505
|
-
# Log and re-raise errors
|
|
506
|
-
logger.error(f"❌ Agent query processing failed: {e}", exc_info=True)
|
|
507
|
-
raise
|
|
508
|
-
|
|
509
|
-
# Final chunk with finish_reason
|
|
510
|
-
final_chunk = {
|
|
511
|
-
"id": completion_id,
|
|
512
|
-
"object": "chat.completion.chunk",
|
|
513
|
-
"created": created,
|
|
514
|
-
"model": model,
|
|
515
|
-
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
|
|
516
|
-
}
|
|
517
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
518
|
-
logger.debug("📤 Sending final chunk with finish_reason=stop")
|
|
519
|
-
yield f"data: {json.dumps(final_chunk)}\n\n"
|
|
520
|
-
|
|
521
|
-
# Done marker
|
|
522
|
-
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
523
|
-
logger.debug("✅ SSE stream complete. Sending [DONE] marker.")
|
|
524
|
-
yield "data: [DONE]\n\n"
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
@app.get("/v1/models")
|
|
528
|
-
async def list_models() -> ModelListResponse:
|
|
529
|
-
"""
|
|
530
|
-
List available models (OpenAI-compatible endpoint).
|
|
531
|
-
|
|
532
|
-
Note: These are GAIA agents exposed as "models", not LLM models.
|
|
533
|
-
Lemonade manages the actual LLM models underneath.
|
|
534
|
-
|
|
535
|
-
Returns:
|
|
536
|
-
ModelListResponse with list of available agent "models"
|
|
537
|
-
|
|
538
|
-
Example:
|
|
539
|
-
```
|
|
540
|
-
GET /v1/models
|
|
541
|
-
{
|
|
542
|
-
"object": "list",
|
|
543
|
-
"data": [
|
|
544
|
-
{
|
|
545
|
-
"id": "gaia-code",
|
|
546
|
-
"object": "model",
|
|
547
|
-
"created": 1234567890,
|
|
548
|
-
"owned_by": "amd-gaia"
|
|
549
|
-
},
|
|
550
|
-
...
|
|
551
|
-
]
|
|
552
|
-
}
|
|
553
|
-
```
|
|
554
|
-
"""
|
|
555
|
-
return ModelListResponse(object="list", data=registry.list_models())
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
@app.get("/health")
|
|
559
|
-
async def health_check():
|
|
560
|
-
"""
|
|
561
|
-
Health check endpoint.
|
|
562
|
-
|
|
563
|
-
Returns:
|
|
564
|
-
Status and service name
|
|
565
|
-
|
|
566
|
-
Example:
|
|
567
|
-
```
|
|
568
|
-
GET /health
|
|
569
|
-
{
|
|
570
|
-
"status": "ok",
|
|
571
|
-
"service": "gaia-api"
|
|
572
|
-
}
|
|
573
|
-
```
|
|
574
|
-
"""
|
|
575
|
-
return {"status": "ok", "service": "gaia-api"}
|
|
1
|
+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""
|
|
4
|
+
OpenAI-compatible API server for GAIA
|
|
5
|
+
|
|
6
|
+
This module provides a FastAPI server that exposes GAIA agents via
|
|
7
|
+
OpenAI-compatible endpoints, allowing VSCode and other tools to use
|
|
8
|
+
GAIA agents as if they were OpenAI models.
|
|
9
|
+
|
|
10
|
+
Endpoints:
|
|
11
|
+
POST /v1/chat/completions - Create chat completion (streaming and non-streaming)
|
|
12
|
+
GET /v1/models - List available models (GAIA agents)
|
|
13
|
+
GET /health - Health check
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
import uuid
|
|
22
|
+
from typing import AsyncGenerator
|
|
23
|
+
|
|
24
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
25
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
26
|
+
from fastapi.responses import StreamingResponse
|
|
27
|
+
|
|
28
|
+
from gaia.agents.base.api_agent import ApiAgent
|
|
29
|
+
|
|
30
|
+
from .agent_registry import registry
|
|
31
|
+
from .schemas import (
|
|
32
|
+
ChatCompletionChoice,
|
|
33
|
+
ChatCompletionRequest,
|
|
34
|
+
ChatCompletionResponse,
|
|
35
|
+
ChatCompletionResponseMessage,
|
|
36
|
+
ModelListResponse,
|
|
37
|
+
UsageInfo,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Configure logging
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# Set logger level based on debug flag
|
|
44
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
45
|
+
logger.setLevel(logging.DEBUG)
|
|
46
|
+
logger.info("Debug logging enabled for API server")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extract_workspace_root(messages):
|
|
50
|
+
"""
|
|
51
|
+
Extract workspace root path from GitHub Copilot messages.
|
|
52
|
+
|
|
53
|
+
GitHub Copilot includes workspace info in messages like:
|
|
54
|
+
<workspace_info>
|
|
55
|
+
I am working in a workspace with the following folders:
|
|
56
|
+
- /Users/username/path/to/workspace
|
|
57
|
+
</workspace_info>
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
messages: List of ChatMessage objects
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: Workspace root path, or None if not found
|
|
64
|
+
"""
|
|
65
|
+
import re
|
|
66
|
+
|
|
67
|
+
for msg in messages:
|
|
68
|
+
if msg.role == "user" and msg.content:
|
|
69
|
+
# Look for workspace_info section
|
|
70
|
+
workspace_match = re.search(
|
|
71
|
+
r"<workspace_info>.*?following folders:\s*\n\s*-\s*([^\s\n]+)",
|
|
72
|
+
msg.content,
|
|
73
|
+
re.DOTALL,
|
|
74
|
+
)
|
|
75
|
+
if workspace_match:
|
|
76
|
+
return workspace_match.group(1).strip()
|
|
77
|
+
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Initialize FastAPI app
|
|
82
|
+
app = FastAPI(
|
|
83
|
+
title="GAIA OpenAI-Compatible API",
|
|
84
|
+
description="OpenAI-compatible API for GAIA agents",
|
|
85
|
+
version="1.0.0",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# CORS middleware - allow all origins for development
|
|
89
|
+
app.add_middleware(
|
|
90
|
+
CORSMiddleware,
|
|
91
|
+
allow_origins=["*"],
|
|
92
|
+
allow_credentials=True,
|
|
93
|
+
allow_methods=["*"],
|
|
94
|
+
allow_headers=["*"],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Raw request logging middleware (debug mode only)
|
|
99
|
+
@app.middleware("http")
|
|
100
|
+
async def log_raw_requests(request: Request, call_next):
|
|
101
|
+
"""
|
|
102
|
+
Middleware to log raw HTTP requests when debug mode is enabled.
|
|
103
|
+
For streaming endpoints, only log headers to avoid breaking SSE.
|
|
104
|
+
"""
|
|
105
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
106
|
+
logger.debug("=" * 80)
|
|
107
|
+
logger.debug("📥 RAW HTTP REQUEST")
|
|
108
|
+
logger.debug("=" * 80)
|
|
109
|
+
logger.debug(f"Path: {request.url.path}")
|
|
110
|
+
logger.debug(f"Method: {request.method}")
|
|
111
|
+
logger.debug("Headers:")
|
|
112
|
+
for name, value in request.headers.items():
|
|
113
|
+
logger.debug(f" {name}: {value}")
|
|
114
|
+
|
|
115
|
+
# DON'T read body for streaming endpoints - it breaks ASGI message flow
|
|
116
|
+
# Per FastAPI docs: "Never read the request body in middleware for streaming responses"
|
|
117
|
+
if request.url.path == "/v1/chat/completions" and request.method == "POST":
|
|
118
|
+
logger.debug(
|
|
119
|
+
"Body: [Skipped for streaming endpoint - prevents ASGI message flow disruption]"
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
# Safe to read body for non-streaming endpoints
|
|
123
|
+
body_bytes = await request.body()
|
|
124
|
+
logger.debug(f"Body (raw bytes length): {len(body_bytes)}")
|
|
125
|
+
if body_bytes:
|
|
126
|
+
try:
|
|
127
|
+
body_str = body_bytes.decode("utf-8")
|
|
128
|
+
logger.debug("Body (decoded UTF-8):")
|
|
129
|
+
logger.debug(body_str)
|
|
130
|
+
# Try to pretty-print JSON
|
|
131
|
+
try:
|
|
132
|
+
body_json = json.loads(body_str)
|
|
133
|
+
logger.debug("Body (parsed JSON):")
|
|
134
|
+
logger.debug(json.dumps(body_json, indent=2))
|
|
135
|
+
except json.JSONDecodeError:
|
|
136
|
+
pass
|
|
137
|
+
except UnicodeDecodeError:
|
|
138
|
+
logger.debug("Body contains non-UTF-8 data")
|
|
139
|
+
|
|
140
|
+
logger.debug("=" * 80)
|
|
141
|
+
|
|
142
|
+
response = await call_next(request)
|
|
143
|
+
return response
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.post("/v1/chat/completions")
|
|
147
|
+
async def create_chat_completion(request: ChatCompletionRequest):
|
|
148
|
+
"""
|
|
149
|
+
Create chat completion (OpenAI-compatible endpoint).
|
|
150
|
+
|
|
151
|
+
Supports both streaming (SSE) and non-streaming responses.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
request: Chat completion request with model, messages, and options
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
For non-streaming: ChatCompletionResponse
|
|
158
|
+
For streaming: StreamingResponse with SSE chunks
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
HTTPException 404: Model not found
|
|
162
|
+
HTTPException 400: No user message in request
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
Non-streaming:
|
|
166
|
+
```
|
|
167
|
+
POST /v1/chat/completions
|
|
168
|
+
{
|
|
169
|
+
"model": "gaia-code",
|
|
170
|
+
"messages": [{"role": "user", "content": "Write hello world"}],
|
|
171
|
+
"stream": false
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Streaming:
|
|
176
|
+
```
|
|
177
|
+
POST /v1/chat/completions
|
|
178
|
+
{
|
|
179
|
+
"model": "gaia-code",
|
|
180
|
+
"messages": [{"role": "user", "content": "Write hello world"}],
|
|
181
|
+
"stream": true
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
"""
|
|
185
|
+
# Debug logging: trace incoming request
|
|
186
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
187
|
+
logger.debug("=" * 80)
|
|
188
|
+
logger.debug("📥 INCOMING CHAT COMPLETION REQUEST")
|
|
189
|
+
logger.debug("=" * 80)
|
|
190
|
+
logger.debug(f"Model: {request.model}")
|
|
191
|
+
logger.debug(f"Stream: {request.stream}")
|
|
192
|
+
logger.debug(f"Message count: {len(request.messages)}")
|
|
193
|
+
logger.debug("-" * 80)
|
|
194
|
+
|
|
195
|
+
for i, msg in enumerate(request.messages):
|
|
196
|
+
logger.debug(f"Message {i}:")
|
|
197
|
+
logger.debug(f" Role: {msg.role}")
|
|
198
|
+
# Preview content (truncate if too long)
|
|
199
|
+
content_preview = (
|
|
200
|
+
msg.content[:500] if len(msg.content) > 500 else msg.content
|
|
201
|
+
)
|
|
202
|
+
if len(msg.content) > 500:
|
|
203
|
+
content_preview += (
|
|
204
|
+
f"\n ... (truncated, total length: {len(msg.content)} chars)"
|
|
205
|
+
)
|
|
206
|
+
logger.debug(f" Content:\n{content_preview}")
|
|
207
|
+
logger.debug("-" * 40)
|
|
208
|
+
|
|
209
|
+
# Log additional request parameters
|
|
210
|
+
logger.debug("Request parameters:")
|
|
211
|
+
logger.debug(f" temperature: {getattr(request, 'temperature', 'not set')}")
|
|
212
|
+
logger.debug(f" max_tokens: {getattr(request, 'max_tokens', 'not set')}")
|
|
213
|
+
logger.debug(f" top_p: {getattr(request, 'top_p', 'not set')}")
|
|
214
|
+
logger.debug("=" * 80)
|
|
215
|
+
|
|
216
|
+
# Validate model exists
|
|
217
|
+
if not registry.model_exists(request.model):
|
|
218
|
+
raise HTTPException(
|
|
219
|
+
status_code=404, detail=f"Model '{request.model}' not found"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Extract workspace root from messages (for converting relative paths to absolute)
|
|
223
|
+
workspace_root = extract_workspace_root(request.messages)
|
|
224
|
+
if os.environ.get("GAIA_API_DEBUG") == "1" and workspace_root:
|
|
225
|
+
logger.debug(f"📁 Extracted workspace root: {workspace_root}")
|
|
226
|
+
|
|
227
|
+
# Extract user query from messages (get last user message)
|
|
228
|
+
user_message = next(
|
|
229
|
+
(m.content for m in reversed(request.messages) if m.role == "user"), None
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if not user_message:
|
|
233
|
+
raise HTTPException(
|
|
234
|
+
status_code=400, detail="No user message found in messages array"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Debug logging: show what we're passing to the agent
|
|
238
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
239
|
+
logger.debug("🔄 EXTRACTED FOR AGENT:")
|
|
240
|
+
logger.debug(f"Passing to agent: {user_message[:500]}...")
|
|
241
|
+
if len(user_message) > 500:
|
|
242
|
+
logger.debug(f"(Total length: {len(user_message)} chars)")
|
|
243
|
+
logger.debug("=" * 80)
|
|
244
|
+
|
|
245
|
+
# Get agent instance for this model
|
|
246
|
+
try:
|
|
247
|
+
agent = registry.get_agent(request.model)
|
|
248
|
+
except ValueError as e:
|
|
249
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
250
|
+
|
|
251
|
+
# Handle streaming vs non-streaming
|
|
252
|
+
if request.stream:
|
|
253
|
+
# Debug logging for streaming mode
|
|
254
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
255
|
+
logger.debug("🌊 Using STREAMING mode")
|
|
256
|
+
|
|
257
|
+
return StreamingResponse(
|
|
258
|
+
create_sse_stream(
|
|
259
|
+
agent, user_message, request.model, workspace_root=workspace_root
|
|
260
|
+
),
|
|
261
|
+
media_type="text/event-stream",
|
|
262
|
+
headers={
|
|
263
|
+
"Cache-Control": "no-cache",
|
|
264
|
+
"Connection": "keep-alive",
|
|
265
|
+
"X-Accel-Buffering": "no", # Disable proxy buffering
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
# Debug logging for non-streaming mode
|
|
270
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
271
|
+
logger.debug("📦 Using NON-STREAMING mode")
|
|
272
|
+
|
|
273
|
+
# Process query synchronously with workspace root
|
|
274
|
+
result = agent.process_query(user_message, workspace_root=workspace_root)
|
|
275
|
+
|
|
276
|
+
# Debug logging: show what agent returned
|
|
277
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
278
|
+
logger.debug("=" * 80)
|
|
279
|
+
logger.debug("📤 AGENT RESPONSE (NON-STREAMING)")
|
|
280
|
+
logger.debug("=" * 80)
|
|
281
|
+
logger.debug(f"Result type: {type(result)}")
|
|
282
|
+
logger.debug(
|
|
283
|
+
f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
|
|
284
|
+
)
|
|
285
|
+
logger.debug(
|
|
286
|
+
f"Status: {result.get('status') if isinstance(result, dict) else 'N/A'}"
|
|
287
|
+
)
|
|
288
|
+
logger.debug(
|
|
289
|
+
f"Steps taken: {result.get('steps_taken') if isinstance(result, dict) else 'N/A'}"
|
|
290
|
+
)
|
|
291
|
+
result_preview = (
|
|
292
|
+
str(result.get("result", ""))[:200]
|
|
293
|
+
if isinstance(result, dict)
|
|
294
|
+
else str(result)[:200]
|
|
295
|
+
)
|
|
296
|
+
logger.debug(f"Result preview: {result_preview}...")
|
|
297
|
+
logger.debug("=" * 80)
|
|
298
|
+
|
|
299
|
+
# Extract content from result
|
|
300
|
+
content = result.get("result", str(result))
|
|
301
|
+
|
|
302
|
+
# Estimate tokens
|
|
303
|
+
if isinstance(agent, ApiAgent):
|
|
304
|
+
prompt_tokens = agent.estimate_tokens(user_message)
|
|
305
|
+
completion_tokens = agent.estimate_tokens(content)
|
|
306
|
+
else:
|
|
307
|
+
prompt_tokens = len(user_message) // 4
|
|
308
|
+
completion_tokens = len(content) // 4
|
|
309
|
+
|
|
310
|
+
return ChatCompletionResponse(
|
|
311
|
+
id=f"chatcmpl-{uuid.uuid4().hex[:24]}",
|
|
312
|
+
object="chat.completion",
|
|
313
|
+
created=int(time.time()),
|
|
314
|
+
model=request.model,
|
|
315
|
+
choices=[
|
|
316
|
+
ChatCompletionChoice(
|
|
317
|
+
index=0,
|
|
318
|
+
message=ChatCompletionResponseMessage(
|
|
319
|
+
role="assistant",
|
|
320
|
+
content=content,
|
|
321
|
+
),
|
|
322
|
+
finish_reason="stop",
|
|
323
|
+
)
|
|
324
|
+
],
|
|
325
|
+
usage=UsageInfo(
|
|
326
|
+
prompt_tokens=prompt_tokens,
|
|
327
|
+
completion_tokens=completion_tokens,
|
|
328
|
+
total_tokens=prompt_tokens + completion_tokens,
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
async def create_sse_stream(
|
|
334
|
+
agent, query: str, model: str, workspace_root: str = None
|
|
335
|
+
) -> AsyncGenerator[str, None]:
|
|
336
|
+
"""
|
|
337
|
+
Create Server-Sent Events stream for chat completion.
|
|
338
|
+
|
|
339
|
+
This function processes the agent query in a thread pool (to avoid blocking)
|
|
340
|
+
and streams agent progress events in real-time via the SSEOutputHandler.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
agent: Agent instance (with SSEOutputHandler)
|
|
344
|
+
query: User query string
|
|
345
|
+
model: Model ID
|
|
346
|
+
workspace_root: Optional workspace root path for absolute file paths
|
|
347
|
+
|
|
348
|
+
Yields:
|
|
349
|
+
SSE-formatted chunks with "data: " prefix
|
|
350
|
+
|
|
351
|
+
Example output:
|
|
352
|
+
data: {"id":"chatcmpl-123","object":"chat.completion.chunk",...}
|
|
353
|
+
data: {"id":"chatcmpl-123","object":"chat.completion.chunk",...}
|
|
354
|
+
data: [DONE]
|
|
355
|
+
"""
|
|
356
|
+
# Debug logging - FIRST LINE to confirm generator starts
|
|
357
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
358
|
+
logger.debug("🎬 Generator started! Client is consuming the stream.")
|
|
359
|
+
|
|
360
|
+
completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
|
|
361
|
+
created = int(time.time())
|
|
362
|
+
|
|
363
|
+
# First chunk with role
|
|
364
|
+
first_chunk = {
|
|
365
|
+
"id": completion_id,
|
|
366
|
+
"object": "chat.completion.chunk",
|
|
367
|
+
"created": created,
|
|
368
|
+
"model": model,
|
|
369
|
+
"choices": [
|
|
370
|
+
{
|
|
371
|
+
"index": 0,
|
|
372
|
+
"delta": {"role": "assistant", "content": ""},
|
|
373
|
+
"finish_reason": None,
|
|
374
|
+
}
|
|
375
|
+
],
|
|
376
|
+
}
|
|
377
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
378
|
+
logger.debug(f"📤 Sending first chunk: {json.dumps(first_chunk)}")
|
|
379
|
+
yield f"data: {json.dumps(first_chunk)}\n\n"
|
|
380
|
+
|
|
381
|
+
# Debug logging
|
|
382
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
383
|
+
logger.debug("🔄 Starting agent query processing in thread pool...")
|
|
384
|
+
|
|
385
|
+
# Process query in thread pool to avoid blocking event loop
|
|
386
|
+
loop = asyncio.get_event_loop()
|
|
387
|
+
|
|
388
|
+
# Get the SSEOutputHandler from the agent (try output_handler first, fall back to console)
|
|
389
|
+
output_handler = getattr(agent, "output_handler", None) or getattr(
|
|
390
|
+
agent, "console", None
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
# Start processing in background
|
|
395
|
+
task = loop.run_in_executor(
|
|
396
|
+
None, lambda: agent.process_query(query, workspace_root=workspace_root)
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Stream events as they are generated
|
|
400
|
+
while not task.done():
|
|
401
|
+
# Check for new events from the output handler
|
|
402
|
+
if hasattr(output_handler, "has_events") and output_handler.has_events():
|
|
403
|
+
events = output_handler.get_events()
|
|
404
|
+
|
|
405
|
+
for event in events:
|
|
406
|
+
event_type = event.get("type", "message")
|
|
407
|
+
|
|
408
|
+
# Check if this event should be streamed to client
|
|
409
|
+
if not output_handler.should_stream_as_content(event_type):
|
|
410
|
+
# Still log it in debug mode
|
|
411
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
412
|
+
logger.debug(f"📝 Skipping event: {event_type}")
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
# Format event as clean content
|
|
416
|
+
content_text = output_handler.format_event_as_content(event)
|
|
417
|
+
|
|
418
|
+
# Skip empty content (filtered events)
|
|
419
|
+
if not content_text:
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
content_chunk = {
|
|
423
|
+
"id": completion_id,
|
|
424
|
+
"object": "chat.completion.chunk",
|
|
425
|
+
"created": created,
|
|
426
|
+
"model": model,
|
|
427
|
+
"choices": [
|
|
428
|
+
{
|
|
429
|
+
"index": 0,
|
|
430
|
+
"delta": {"content": content_text},
|
|
431
|
+
"finish_reason": None,
|
|
432
|
+
}
|
|
433
|
+
],
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
437
|
+
logger.debug(
|
|
438
|
+
f"📤 Streaming event: {event_type} -> {content_text[:100]}"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
yield f"data: {json.dumps(content_chunk)}\n\n"
|
|
442
|
+
|
|
443
|
+
# Small delay to avoid busy waiting
|
|
444
|
+
await asyncio.sleep(0.1)
|
|
445
|
+
|
|
446
|
+
# Get the final result
|
|
447
|
+
result = await task
|
|
448
|
+
|
|
449
|
+
# Get any remaining events
|
|
450
|
+
if hasattr(output_handler, "has_events") and output_handler.has_events():
|
|
451
|
+
events = output_handler.get_events()
|
|
452
|
+
for event in events:
|
|
453
|
+
event_type = event.get("type", "message")
|
|
454
|
+
|
|
455
|
+
# Check if this event should be streamed
|
|
456
|
+
if not output_handler.should_stream_as_content(event_type):
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
# Format event as clean content
|
|
460
|
+
content_text = output_handler.format_event_as_content(event)
|
|
461
|
+
|
|
462
|
+
# Skip empty content
|
|
463
|
+
if not content_text:
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
content_chunk = {
|
|
467
|
+
"id": completion_id,
|
|
468
|
+
"object": "chat.completion.chunk",
|
|
469
|
+
"created": created,
|
|
470
|
+
"model": model,
|
|
471
|
+
"choices": [
|
|
472
|
+
{
|
|
473
|
+
"index": 0,
|
|
474
|
+
"delta": {"content": content_text},
|
|
475
|
+
"finish_reason": None,
|
|
476
|
+
}
|
|
477
|
+
],
|
|
478
|
+
}
|
|
479
|
+
yield f"data: {json.dumps(content_chunk)}\n\n"
|
|
480
|
+
|
|
481
|
+
# Debug logging: show what agent returned
|
|
482
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
483
|
+
logger.debug("=" * 80)
|
|
484
|
+
logger.debug("📤 AGENT RESPONSE (STREAMING)")
|
|
485
|
+
logger.debug("=" * 80)
|
|
486
|
+
logger.debug(f"Result type: {type(result)}")
|
|
487
|
+
logger.debug(
|
|
488
|
+
f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
|
|
489
|
+
)
|
|
490
|
+
logger.debug(
|
|
491
|
+
f"Status: {result.get('status') if isinstance(result, dict) else 'N/A'}"
|
|
492
|
+
)
|
|
493
|
+
logger.debug(
|
|
494
|
+
f"Steps taken: {result.get('steps_taken') if isinstance(result, dict) else 'N/A'}"
|
|
495
|
+
)
|
|
496
|
+
result_preview = (
|
|
497
|
+
str(result.get("result", ""))[:200]
|
|
498
|
+
if isinstance(result, dict)
|
|
499
|
+
else str(result)[:200]
|
|
500
|
+
)
|
|
501
|
+
logger.debug(f"Result preview: {result_preview}...")
|
|
502
|
+
logger.debug("=" * 80)
|
|
503
|
+
|
|
504
|
+
except Exception as e:
|
|
505
|
+
# Log and re-raise errors
|
|
506
|
+
logger.error(f"❌ Agent query processing failed: {e}", exc_info=True)
|
|
507
|
+
raise
|
|
508
|
+
|
|
509
|
+
# Final chunk with finish_reason
|
|
510
|
+
final_chunk = {
|
|
511
|
+
"id": completion_id,
|
|
512
|
+
"object": "chat.completion.chunk",
|
|
513
|
+
"created": created,
|
|
514
|
+
"model": model,
|
|
515
|
+
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
|
|
516
|
+
}
|
|
517
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
518
|
+
logger.debug("📤 Sending final chunk with finish_reason=stop")
|
|
519
|
+
yield f"data: {json.dumps(final_chunk)}\n\n"
|
|
520
|
+
|
|
521
|
+
# Done marker
|
|
522
|
+
if os.environ.get("GAIA_API_DEBUG") == "1":
|
|
523
|
+
logger.debug("✅ SSE stream complete. Sending [DONE] marker.")
|
|
524
|
+
yield "data: [DONE]\n\n"
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
@app.get("/v1/models")
|
|
528
|
+
async def list_models() -> ModelListResponse:
|
|
529
|
+
"""
|
|
530
|
+
List available models (OpenAI-compatible endpoint).
|
|
531
|
+
|
|
532
|
+
Note: These are GAIA agents exposed as "models", not LLM models.
|
|
533
|
+
Lemonade manages the actual LLM models underneath.
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
ModelListResponse with list of available agent "models"
|
|
537
|
+
|
|
538
|
+
Example:
|
|
539
|
+
```
|
|
540
|
+
GET /v1/models
|
|
541
|
+
{
|
|
542
|
+
"object": "list",
|
|
543
|
+
"data": [
|
|
544
|
+
{
|
|
545
|
+
"id": "gaia-code",
|
|
546
|
+
"object": "model",
|
|
547
|
+
"created": 1234567890,
|
|
548
|
+
"owned_by": "amd-gaia"
|
|
549
|
+
},
|
|
550
|
+
...
|
|
551
|
+
]
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
"""
|
|
555
|
+
return ModelListResponse(object="list", data=registry.list_models())
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@app.get("/health")
|
|
559
|
+
async def health_check():
|
|
560
|
+
"""
|
|
561
|
+
Health check endpoint.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Status and service name
|
|
565
|
+
|
|
566
|
+
Example:
|
|
567
|
+
```
|
|
568
|
+
GET /health
|
|
569
|
+
{
|
|
570
|
+
"status": "ok",
|
|
571
|
+
"service": "gaia-api"
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
"""
|
|
575
|
+
return {"status": "ok", "service": "gaia-api"}
|