autoforge-ai 0.1.16 → 0.1.18
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.
- package/autonomous_agent_demo.py +2 -2
- package/package.json +1 -1
- package/parallel_orchestrator.py +4 -4
- package/requirements-prod.txt +4 -0
- package/server/routers/agent.py +11 -4
- package/server/routers/expand_project.py +3 -3
- package/server/routers/settings.py +5 -0
- package/server/routers/spec_creation.py +3 -3
- package/server/schemas.py +55 -21
- package/server/services/chat_constants.py +36 -0
- package/server/services/expand_chat_session.py +14 -13
- package/server/services/process_manager.py +4 -0
- package/server/services/spec_chat_session.py +12 -14
- package/server/utils/document_extraction.py +221 -0
- package/ui/dist/assets/index-DXm5cuJA.js +96 -0
- package/ui/dist/assets/index-DlYws_VI.css +1 -0
- package/ui/dist/assets/vendor-flow-CSXy01ye.js +7 -0
- package/ui/dist/assets/{vendor-markdown-lmnOnLXp.js → vendor-markdown-BxiGvyag.js} +3 -3
- package/ui/dist/assets/vendor-query-CcgjkJlA.js +1 -0
- package/ui/dist/assets/vendor-radix-DIVIznMB.js +45 -0
- package/ui/dist/assets/vendor-react-l0sNRNKZ.js +1 -0
- package/ui/dist/assets/{vendor-utils-CdMnkzGY.js → vendor-utils-CJmVD20L.js} +1 -1
- package/ui/dist/index.html +7 -8
- package/ui/dist/assets/index-CX9TqxHJ.css +0 -1
- package/ui/dist/assets/index-DtBG9zqQ.js +0 -96
- package/ui/dist/assets/vendor-flow-CVNK-_lx.js +0 -7
- package/ui/dist/assets/vendor-query-BUABzP5o.js +0 -1
- package/ui/dist/assets/vendor-radix-DjWauVBs.js +0 -45
- package/ui/dist/assets/vendor-react-qkC6yhPU.js +0 -1
package/autonomous_agent_demo.py
CHANGED
|
@@ -176,14 +176,14 @@ Authentication:
|
|
|
176
176
|
"--testing-batch-size",
|
|
177
177
|
type=int,
|
|
178
178
|
default=3,
|
|
179
|
-
help="Number of features per testing batch (1-
|
|
179
|
+
help="Number of features per testing batch (1-15, default: 3)",
|
|
180
180
|
)
|
|
181
181
|
|
|
182
182
|
parser.add_argument(
|
|
183
183
|
"--batch-size",
|
|
184
184
|
type=int,
|
|
185
185
|
default=3,
|
|
186
|
-
help="Max features per coding agent batch (1-
|
|
186
|
+
help="Max features per coding agent batch (1-15, default: 3)",
|
|
187
187
|
)
|
|
188
188
|
|
|
189
189
|
return parser.parse_args()
|
package/package.json
CHANGED
package/parallel_orchestrator.py
CHANGED
|
@@ -131,7 +131,7 @@ def _dump_database_state(feature_dicts: list[dict], label: str = ""):
|
|
|
131
131
|
MAX_PARALLEL_AGENTS = 5
|
|
132
132
|
MAX_TOTAL_AGENTS = 10
|
|
133
133
|
DEFAULT_CONCURRENCY = 3
|
|
134
|
-
DEFAULT_TESTING_BATCH_SIZE = 3 # Number of features per testing batch (1-
|
|
134
|
+
DEFAULT_TESTING_BATCH_SIZE = 3 # Number of features per testing batch (1-15)
|
|
135
135
|
POLL_INTERVAL = 5 # seconds between checking for ready features
|
|
136
136
|
MAX_FEATURE_RETRIES = 3 # Maximum times to retry a failed feature
|
|
137
137
|
INITIALIZER_TIMEOUT = 1800 # 30 minutes timeout for initializer
|
|
@@ -168,7 +168,7 @@ class ParallelOrchestrator:
|
|
|
168
168
|
yolo_mode: Whether to run in YOLO mode (skip testing agents entirely)
|
|
169
169
|
testing_agent_ratio: Number of regression testing agents to maintain (0-3).
|
|
170
170
|
0 = disabled, 1-3 = maintain that many testing agents running independently.
|
|
171
|
-
testing_batch_size: Number of features to include per testing session (1-
|
|
171
|
+
testing_batch_size: Number of features to include per testing session (1-15).
|
|
172
172
|
Each testing agent receives this many features to regression test.
|
|
173
173
|
on_output: Callback for agent output (feature_id, line)
|
|
174
174
|
on_status: Callback for agent status changes (feature_id, status)
|
|
@@ -178,8 +178,8 @@ class ParallelOrchestrator:
|
|
|
178
178
|
self.model = model
|
|
179
179
|
self.yolo_mode = yolo_mode
|
|
180
180
|
self.testing_agent_ratio = min(max(testing_agent_ratio, 0), 3) # Clamp 0-3
|
|
181
|
-
self.testing_batch_size = min(max(testing_batch_size, 1),
|
|
182
|
-
self.batch_size = min(max(batch_size, 1),
|
|
181
|
+
self.testing_batch_size = min(max(testing_batch_size, 1), 15) # Clamp 1-15
|
|
182
|
+
self.batch_size = min(max(batch_size, 1), 15) # Clamp 1-15
|
|
183
183
|
self.on_output = on_output
|
|
184
184
|
self.on_status = on_status
|
|
185
185
|
|
package/requirements-prod.txt
CHANGED
package/server/routers/agent.py
CHANGED
|
@@ -17,11 +17,11 @@ from ..utils.project_helpers import get_project_path as _get_project_path
|
|
|
17
17
|
from ..utils.validation import validate_project_name
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def _get_settings_defaults() -> tuple[bool, str, int, bool, int]:
|
|
20
|
+
def _get_settings_defaults() -> tuple[bool, str, int, bool, int, int]:
|
|
21
21
|
"""Get defaults from global settings.
|
|
22
22
|
|
|
23
23
|
Returns:
|
|
24
|
-
Tuple of (yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size)
|
|
24
|
+
Tuple of (yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size, testing_batch_size)
|
|
25
25
|
"""
|
|
26
26
|
import sys
|
|
27
27
|
root = Path(__file__).parent.parent.parent
|
|
@@ -47,7 +47,12 @@ def _get_settings_defaults() -> tuple[bool, str, int, bool, int]:
|
|
|
47
47
|
except (ValueError, TypeError):
|
|
48
48
|
batch_size = 3
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
try:
|
|
51
|
+
testing_batch_size = int(settings.get("testing_batch_size", "3"))
|
|
52
|
+
except (ValueError, TypeError):
|
|
53
|
+
testing_batch_size = 3
|
|
54
|
+
|
|
55
|
+
return yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size, testing_batch_size
|
|
51
56
|
|
|
52
57
|
|
|
53
58
|
router = APIRouter(prefix="/api/projects/{project_name}/agent", tags=["agent"])
|
|
@@ -96,7 +101,7 @@ async def start_agent(
|
|
|
96
101
|
manager = get_project_manager(project_name)
|
|
97
102
|
|
|
98
103
|
# Get defaults from global settings if not provided in request
|
|
99
|
-
default_yolo, default_model, default_testing_ratio, playwright_headless, default_batch_size = _get_settings_defaults()
|
|
104
|
+
default_yolo, default_model, default_testing_ratio, playwright_headless, default_batch_size, default_testing_batch_size = _get_settings_defaults()
|
|
100
105
|
|
|
101
106
|
yolo_mode = request.yolo_mode if request.yolo_mode is not None else default_yolo
|
|
102
107
|
model = request.model if request.model else default_model
|
|
@@ -104,6 +109,7 @@ async def start_agent(
|
|
|
104
109
|
testing_agent_ratio = request.testing_agent_ratio if request.testing_agent_ratio is not None else default_testing_ratio
|
|
105
110
|
|
|
106
111
|
batch_size = default_batch_size
|
|
112
|
+
testing_batch_size = default_testing_batch_size
|
|
107
113
|
|
|
108
114
|
success, message = await manager.start(
|
|
109
115
|
yolo_mode=yolo_mode,
|
|
@@ -112,6 +118,7 @@ async def start_agent(
|
|
|
112
118
|
testing_agent_ratio=testing_agent_ratio,
|
|
113
119
|
playwright_headless=playwright_headless,
|
|
114
120
|
batch_size=batch_size,
|
|
121
|
+
testing_batch_size=testing_batch_size,
|
|
115
122
|
)
|
|
116
123
|
|
|
117
124
|
# Notify scheduler of manual start (to prevent auto-stop during scheduled window)
|
|
@@ -13,7 +13,7 @@ from typing import Optional
|
|
|
13
13
|
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
|
|
14
14
|
from pydantic import BaseModel, ValidationError
|
|
15
15
|
|
|
16
|
-
from ..schemas import
|
|
16
|
+
from ..schemas import FileAttachment
|
|
17
17
|
from ..services.expand_chat_session import (
|
|
18
18
|
ExpandChatSession,
|
|
19
19
|
create_expand_session,
|
|
@@ -181,12 +181,12 @@ async def expand_project_websocket(websocket: WebSocket, project_name: str):
|
|
|
181
181
|
user_content = message.get("content", "").strip()
|
|
182
182
|
|
|
183
183
|
# Parse attachments if present
|
|
184
|
-
attachments: list[
|
|
184
|
+
attachments: list[FileAttachment] = []
|
|
185
185
|
raw_attachments = message.get("attachments", [])
|
|
186
186
|
if raw_attachments:
|
|
187
187
|
try:
|
|
188
188
|
for raw_att in raw_attachments:
|
|
189
|
-
attachments.append(
|
|
189
|
+
attachments.append(FileAttachment(**raw_att))
|
|
190
190
|
except (ValidationError, Exception) as e:
|
|
191
191
|
logger.warning(f"Invalid attachment data: {e}")
|
|
192
192
|
await websocket.send_json({
|
|
@@ -113,6 +113,7 @@ async def get_settings():
|
|
|
113
113
|
testing_agent_ratio=_parse_int(all_settings.get("testing_agent_ratio"), 1),
|
|
114
114
|
playwright_headless=_parse_bool(all_settings.get("playwright_headless"), default=True),
|
|
115
115
|
batch_size=_parse_int(all_settings.get("batch_size"), 3),
|
|
116
|
+
testing_batch_size=_parse_int(all_settings.get("testing_batch_size"), 3),
|
|
116
117
|
api_provider=api_provider,
|
|
117
118
|
api_base_url=all_settings.get("api_base_url"),
|
|
118
119
|
api_has_auth_token=bool(all_settings.get("api_auth_token")),
|
|
@@ -138,6 +139,9 @@ async def update_settings(update: SettingsUpdate):
|
|
|
138
139
|
if update.batch_size is not None:
|
|
139
140
|
set_setting("batch_size", str(update.batch_size))
|
|
140
141
|
|
|
142
|
+
if update.testing_batch_size is not None:
|
|
143
|
+
set_setting("testing_batch_size", str(update.testing_batch_size))
|
|
144
|
+
|
|
141
145
|
# API provider settings
|
|
142
146
|
if update.api_provider is not None:
|
|
143
147
|
old_provider = get_setting("api_provider", "claude")
|
|
@@ -177,6 +181,7 @@ async def update_settings(update: SettingsUpdate):
|
|
|
177
181
|
testing_agent_ratio=_parse_int(all_settings.get("testing_agent_ratio"), 1),
|
|
178
182
|
playwright_headless=_parse_bool(all_settings.get("playwright_headless"), default=True),
|
|
179
183
|
batch_size=_parse_int(all_settings.get("batch_size"), 3),
|
|
184
|
+
testing_batch_size=_parse_int(all_settings.get("testing_batch_size"), 3),
|
|
180
185
|
api_provider=api_provider,
|
|
181
186
|
api_base_url=all_settings.get("api_base_url"),
|
|
182
187
|
api_has_auth_token=bool(all_settings.get("api_auth_token")),
|
|
@@ -12,7 +12,7 @@ from typing import Optional
|
|
|
12
12
|
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
|
|
13
13
|
from pydantic import BaseModel, ValidationError
|
|
14
14
|
|
|
15
|
-
from ..schemas import
|
|
15
|
+
from ..schemas import FileAttachment
|
|
16
16
|
from ..services.spec_chat_session import (
|
|
17
17
|
SpecChatSession,
|
|
18
18
|
create_session,
|
|
@@ -242,12 +242,12 @@ async def spec_chat_websocket(websocket: WebSocket, project_name: str):
|
|
|
242
242
|
user_content = message.get("content", "").strip()
|
|
243
243
|
|
|
244
244
|
# Parse attachments if present
|
|
245
|
-
attachments: list[
|
|
245
|
+
attachments: list[FileAttachment] = []
|
|
246
246
|
raw_attachments = message.get("attachments", [])
|
|
247
247
|
if raw_attachments:
|
|
248
248
|
try:
|
|
249
249
|
for raw_att in raw_attachments:
|
|
250
|
-
attachments.append(
|
|
250
|
+
attachments.append(FileAttachment(**raw_att))
|
|
251
251
|
except (ValidationError, Exception) as e:
|
|
252
252
|
logger.warning(f"Invalid attachment data: {e}")
|
|
253
253
|
await websocket.send_json({
|
package/server/schemas.py
CHANGED
|
@@ -11,7 +11,7 @@ from datetime import datetime
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Literal
|
|
13
13
|
|
|
14
|
-
from pydantic import BaseModel, Field, field_validator
|
|
14
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
15
15
|
|
|
16
16
|
# Import model constants from registry (single source of truth)
|
|
17
17
|
_root = Path(__file__).parent.parent
|
|
@@ -331,36 +331,61 @@ class WSAgentUpdateMessage(BaseModel):
|
|
|
331
331
|
|
|
332
332
|
|
|
333
333
|
# ============================================================================
|
|
334
|
-
#
|
|
334
|
+
# Chat Attachment Schemas
|
|
335
335
|
# ============================================================================
|
|
336
336
|
|
|
337
|
-
#
|
|
338
|
-
MAX_IMAGE_SIZE = 5 * 1024 * 1024
|
|
337
|
+
# Size limits
|
|
338
|
+
MAX_IMAGE_SIZE = 5 * 1024 * 1024 # 5 MB for images
|
|
339
|
+
MAX_DOCUMENT_SIZE = 20 * 1024 * 1024 # 20 MB for documents
|
|
339
340
|
|
|
341
|
+
_IMAGE_MIME_TYPES = {'image/jpeg', 'image/png'}
|
|
340
342
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
+
|
|
344
|
+
class FileAttachment(BaseModel):
|
|
345
|
+
"""File attachment from client for spec creation / expand project chat."""
|
|
343
346
|
filename: str = Field(..., min_length=1, max_length=255)
|
|
344
|
-
mimeType: Literal[
|
|
347
|
+
mimeType: Literal[
|
|
348
|
+
'image/jpeg', 'image/png',
|
|
349
|
+
'text/plain', 'text/markdown', 'text/csv',
|
|
350
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
351
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
352
|
+
'application/pdf',
|
|
353
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
354
|
+
]
|
|
345
355
|
base64Data: str
|
|
346
356
|
|
|
347
357
|
@field_validator('base64Data')
|
|
348
358
|
@classmethod
|
|
349
|
-
def
|
|
350
|
-
"""Validate that base64 data is
|
|
359
|
+
def validate_base64(cls, v: str) -> str:
|
|
360
|
+
"""Validate that base64 data is decodable."""
|
|
351
361
|
try:
|
|
352
|
-
|
|
353
|
-
if len(decoded) > MAX_IMAGE_SIZE:
|
|
354
|
-
raise ValueError(
|
|
355
|
-
f'Image size ({len(decoded) / (1024 * 1024):.1f} MB) exceeds '
|
|
356
|
-
f'maximum of {MAX_IMAGE_SIZE // (1024 * 1024)} MB'
|
|
357
|
-
)
|
|
362
|
+
base64.b64decode(v)
|
|
358
363
|
return v
|
|
359
364
|
except Exception as e:
|
|
360
|
-
if 'Image size' in str(e):
|
|
361
|
-
raise
|
|
362
365
|
raise ValueError(f'Invalid base64 data: {e}')
|
|
363
366
|
|
|
367
|
+
@model_validator(mode='after')
|
|
368
|
+
def validate_size(self) -> 'FileAttachment':
|
|
369
|
+
"""Validate file size based on MIME type."""
|
|
370
|
+
try:
|
|
371
|
+
decoded = base64.b64decode(self.base64Data)
|
|
372
|
+
except Exception:
|
|
373
|
+
return self # Already caught by field validator
|
|
374
|
+
|
|
375
|
+
if self.mimeType in _IMAGE_MIME_TYPES:
|
|
376
|
+
max_size = MAX_IMAGE_SIZE
|
|
377
|
+
label = "Image"
|
|
378
|
+
else:
|
|
379
|
+
max_size = MAX_DOCUMENT_SIZE
|
|
380
|
+
label = "Document"
|
|
381
|
+
|
|
382
|
+
if len(decoded) > max_size:
|
|
383
|
+
raise ValueError(
|
|
384
|
+
f'{label} size ({len(decoded) / (1024 * 1024):.1f} MB) exceeds '
|
|
385
|
+
f'maximum of {max_size // (1024 * 1024)} MB'
|
|
386
|
+
)
|
|
387
|
+
return self
|
|
388
|
+
|
|
364
389
|
|
|
365
390
|
# ============================================================================
|
|
366
391
|
# Filesystem Schemas
|
|
@@ -444,7 +469,8 @@ class SettingsResponse(BaseModel):
|
|
|
444
469
|
ollama_mode: bool = False # True when api_provider is "ollama"
|
|
445
470
|
testing_agent_ratio: int = 1 # Regression testing agents (0-3)
|
|
446
471
|
playwright_headless: bool = True
|
|
447
|
-
batch_size: int = 3 # Features per coding agent batch (1-
|
|
472
|
+
batch_size: int = 3 # Features per coding agent batch (1-15)
|
|
473
|
+
testing_batch_size: int = 3 # Features per testing agent batch (1-15)
|
|
448
474
|
api_provider: str = "claude"
|
|
449
475
|
api_base_url: str | None = None
|
|
450
476
|
api_has_auth_token: bool = False # Never expose actual token
|
|
@@ -463,7 +489,8 @@ class SettingsUpdate(BaseModel):
|
|
|
463
489
|
model: str | None = None
|
|
464
490
|
testing_agent_ratio: int | None = None # 0-3
|
|
465
491
|
playwright_headless: bool | None = None
|
|
466
|
-
batch_size: int | None = None # Features per agent batch (1-
|
|
492
|
+
batch_size: int | None = None # Features per agent batch (1-15)
|
|
493
|
+
testing_batch_size: int | None = None # Features per testing agent batch (1-15)
|
|
467
494
|
api_provider: str | None = None
|
|
468
495
|
api_base_url: str | None = Field(None, max_length=500)
|
|
469
496
|
api_auth_token: str | None = Field(None, max_length=500) # Write-only, never returned
|
|
@@ -500,8 +527,15 @@ class SettingsUpdate(BaseModel):
|
|
|
500
527
|
@field_validator('batch_size')
|
|
501
528
|
@classmethod
|
|
502
529
|
def validate_batch_size(cls, v: int | None) -> int | None:
|
|
503
|
-
if v is not None and (v < 1 or v >
|
|
504
|
-
raise ValueError("batch_size must be between 1 and
|
|
530
|
+
if v is not None and (v < 1 or v > 15):
|
|
531
|
+
raise ValueError("batch_size must be between 1 and 15")
|
|
532
|
+
return v
|
|
533
|
+
|
|
534
|
+
@field_validator('testing_batch_size')
|
|
535
|
+
@classmethod
|
|
536
|
+
def validate_testing_batch_size(cls, v: int | None) -> int | None:
|
|
537
|
+
if v is not None and (v < 1 or v > 15):
|
|
538
|
+
raise ValueError("testing_batch_size must be between 1 and 15")
|
|
505
539
|
return v
|
|
506
540
|
|
|
507
541
|
|
|
@@ -35,6 +35,13 @@ if _root_str not in sys.path:
|
|
|
35
35
|
from env_constants import API_ENV_VARS # noqa: E402, F401
|
|
36
36
|
from rate_limit_utils import is_rate_limit_error, parse_retry_after # noqa: E402, F401
|
|
37
37
|
|
|
38
|
+
from ..schemas import FileAttachment
|
|
39
|
+
from ..utils.document_extraction import (
|
|
40
|
+
extract_text_from_document,
|
|
41
|
+
is_document,
|
|
42
|
+
is_image,
|
|
43
|
+
)
|
|
44
|
+
|
|
38
45
|
logger = logging.getLogger(__name__)
|
|
39
46
|
|
|
40
47
|
|
|
@@ -88,6 +95,35 @@ async def safe_receive_response(client: Any, log: logging.Logger) -> AsyncGenera
|
|
|
88
95
|
raise
|
|
89
96
|
|
|
90
97
|
|
|
98
|
+
def build_attachment_content_blocks(attachments: list[FileAttachment]) -> list[dict]:
|
|
99
|
+
"""Convert FileAttachment objects to Claude API content blocks.
|
|
100
|
+
|
|
101
|
+
Images become image content blocks (passed directly to Claude's vision).
|
|
102
|
+
Documents are extracted to text and become text content blocks.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
DocumentExtractionError: If a document cannot be read.
|
|
106
|
+
"""
|
|
107
|
+
blocks: list[dict] = []
|
|
108
|
+
for att in attachments:
|
|
109
|
+
if is_image(att.mimeType):
|
|
110
|
+
blocks.append({
|
|
111
|
+
"type": "image",
|
|
112
|
+
"source": {
|
|
113
|
+
"type": "base64",
|
|
114
|
+
"media_type": att.mimeType,
|
|
115
|
+
"data": att.base64Data,
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
elif is_document(att.mimeType):
|
|
119
|
+
text = extract_text_from_document(att.base64Data, att.mimeType, att.filename)
|
|
120
|
+
blocks.append({
|
|
121
|
+
"type": "text",
|
|
122
|
+
"text": f"[Content of uploaded file: {att.filename}]\n\n{text}",
|
|
123
|
+
})
|
|
124
|
+
return blocks
|
|
125
|
+
|
|
126
|
+
|
|
91
127
|
async def make_multimodal_message(content_blocks: list[dict]) -> AsyncGenerator[dict, None]:
|
|
92
128
|
"""Yield a single multimodal user message in Claude Agent SDK format.
|
|
93
129
|
|
|
@@ -21,9 +21,11 @@ from typing import Any, AsyncGenerator, Optional
|
|
|
21
21
|
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
|
22
22
|
from dotenv import load_dotenv
|
|
23
23
|
|
|
24
|
-
from ..schemas import
|
|
24
|
+
from ..schemas import FileAttachment
|
|
25
|
+
from ..utils.document_extraction import DocumentExtractionError
|
|
25
26
|
from .chat_constants import (
|
|
26
27
|
ROOT_DIR,
|
|
28
|
+
build_attachment_content_blocks,
|
|
27
29
|
check_rate_limit_error,
|
|
28
30
|
make_multimodal_message,
|
|
29
31
|
safe_receive_response,
|
|
@@ -226,7 +228,7 @@ class ExpandChatSession:
|
|
|
226
228
|
async def send_message(
|
|
227
229
|
self,
|
|
228
230
|
user_message: str,
|
|
229
|
-
attachments: list[
|
|
231
|
+
attachments: list[FileAttachment] | None = None
|
|
230
232
|
) -> AsyncGenerator[dict, None]:
|
|
231
233
|
"""
|
|
232
234
|
Send user message and stream Claude's response.
|
|
@@ -273,7 +275,7 @@ class ExpandChatSession:
|
|
|
273
275
|
async def _query_claude(
|
|
274
276
|
self,
|
|
275
277
|
message: str,
|
|
276
|
-
attachments: list[
|
|
278
|
+
attachments: list[FileAttachment] | None = None
|
|
277
279
|
) -> AsyncGenerator[dict, None]:
|
|
278
280
|
"""
|
|
279
281
|
Internal method to query Claude and stream responses.
|
|
@@ -289,17 +291,16 @@ class ExpandChatSession:
|
|
|
289
291
|
content_blocks: list[dict[str, Any]] = []
|
|
290
292
|
if message:
|
|
291
293
|
content_blocks.append({"type": "text", "text": message})
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
})
|
|
294
|
+
|
|
295
|
+
# Add attachment blocks (images as image blocks, documents as extracted text)
|
|
296
|
+
try:
|
|
297
|
+
content_blocks.extend(build_attachment_content_blocks(attachments))
|
|
298
|
+
except DocumentExtractionError as e:
|
|
299
|
+
yield {"type": "error", "content": str(e)}
|
|
300
|
+
return
|
|
301
|
+
|
|
301
302
|
await self.client.query(make_multimodal_message(content_blocks))
|
|
302
|
-
logger.info(f"Sent multimodal message with {len(attachments)}
|
|
303
|
+
logger.info(f"Sent multimodal message with {len(attachments)} attachment(s)")
|
|
303
304
|
else:
|
|
304
305
|
await self.client.query(message)
|
|
305
306
|
|
|
@@ -374,6 +374,7 @@ class AgentProcessManager:
|
|
|
374
374
|
testing_agent_ratio: int = 1,
|
|
375
375
|
playwright_headless: bool = True,
|
|
376
376
|
batch_size: int = 3,
|
|
377
|
+
testing_batch_size: int = 3,
|
|
377
378
|
) -> tuple[bool, str]:
|
|
378
379
|
"""
|
|
379
380
|
Start the agent as a subprocess.
|
|
@@ -440,6 +441,9 @@ class AgentProcessManager:
|
|
|
440
441
|
# Add --batch-size flag for multi-feature batching
|
|
441
442
|
cmd.extend(["--batch-size", str(batch_size)])
|
|
442
443
|
|
|
444
|
+
# Add --testing-batch-size flag for testing agent batching
|
|
445
|
+
cmd.extend(["--testing-batch-size", str(testing_batch_size)])
|
|
446
|
+
|
|
443
447
|
# Apply headless setting to .playwright/cli.config.json so playwright-cli
|
|
444
448
|
# picks it up (the only mechanism it supports for headless control)
|
|
445
449
|
self._apply_playwright_headless(playwright_headless)
|
|
@@ -18,9 +18,11 @@ from typing import Any, AsyncGenerator, Optional
|
|
|
18
18
|
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
|
19
19
|
from dotenv import load_dotenv
|
|
20
20
|
|
|
21
|
-
from ..schemas import
|
|
21
|
+
from ..schemas import FileAttachment
|
|
22
|
+
from ..utils.document_extraction import DocumentExtractionError
|
|
22
23
|
from .chat_constants import (
|
|
23
24
|
ROOT_DIR,
|
|
25
|
+
build_attachment_content_blocks,
|
|
24
26
|
check_rate_limit_error,
|
|
25
27
|
make_multimodal_message,
|
|
26
28
|
safe_receive_response,
|
|
@@ -201,7 +203,7 @@ class SpecChatSession:
|
|
|
201
203
|
async def send_message(
|
|
202
204
|
self,
|
|
203
205
|
user_message: str,
|
|
204
|
-
attachments: list[
|
|
206
|
+
attachments: list[FileAttachment] | None = None
|
|
205
207
|
) -> AsyncGenerator[dict, None]:
|
|
206
208
|
"""
|
|
207
209
|
Send user message and stream Claude's response.
|
|
@@ -247,7 +249,7 @@ class SpecChatSession:
|
|
|
247
249
|
async def _query_claude(
|
|
248
250
|
self,
|
|
249
251
|
message: str,
|
|
250
|
-
attachments: list[
|
|
252
|
+
attachments: list[FileAttachment] | None = None
|
|
251
253
|
) -> AsyncGenerator[dict, None]:
|
|
252
254
|
"""
|
|
253
255
|
Internal method to query Claude and stream responses.
|
|
@@ -273,21 +275,17 @@ class SpecChatSession:
|
|
|
273
275
|
if message:
|
|
274
276
|
content_blocks.append({"type": "text", "text": message})
|
|
275
277
|
|
|
276
|
-
# Add image blocks
|
|
277
|
-
|
|
278
|
-
content_blocks.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"media_type": att.mimeType,
|
|
283
|
-
"data": att.base64Data,
|
|
284
|
-
}
|
|
285
|
-
})
|
|
278
|
+
# Add attachment blocks (images as image blocks, documents as extracted text)
|
|
279
|
+
try:
|
|
280
|
+
content_blocks.extend(build_attachment_content_blocks(attachments))
|
|
281
|
+
except DocumentExtractionError as e:
|
|
282
|
+
yield {"type": "error", "content": str(e)}
|
|
283
|
+
return
|
|
286
284
|
|
|
287
285
|
# Send multimodal content to Claude using async generator format
|
|
288
286
|
# The SDK's query() accepts AsyncIterable[dict] for custom message formats
|
|
289
287
|
await self.client.query(make_multimodal_message(content_blocks))
|
|
290
|
-
logger.info(f"Sent multimodal message with {len(attachments)}
|
|
288
|
+
logger.info(f"Sent multimodal message with {len(attachments)} attachment(s)")
|
|
291
289
|
else:
|
|
292
290
|
# Text-only message: use string format
|
|
293
291
|
await self.client.query(message)
|