refactor(agent): restructure visibility masks, task queues, and memory service

Visibility mask refactoring:
- Replace dead UI_REALTIME bit with CONTEXT_ASSEMBLY (bit 1)
- Remove visibility_consumer_bit from SystemAgentLLMConfig and system_agents.yaml
- Simplify _resolve_user_message_visibility_mask: chat->UI_HISTORY|CONTEXT_ASSEMBLY, automation->0
- Simplify _resolve_stage_visibility_mask: memory->UI_HISTORY, router/worker->UI_HISTORY|CONTEXT_ASSEMBLY
- Remove stage_visibility_bit_map from store.py

Task queue renaming:
- Replace default_broker/bulk_broker/critical_broker with worker_agent_broker/worker_automation_broker
- Queue names: 'default'/'bulk'/'critical' -> 'agent'/'automation'
- Rename run_command_task -> run_command_task_agent/run_command_task_automation
- AgentService derives queue from runtime_mode: chat->agent, automation->automation

Architecture cleanup:
- Move context_service.py from runtime/ to agentscope/services/
- Add MemoryService in v1/memory/ following repository/service pattern
- Move consumer_registry.py and pipeline_spec.py from schemas/agent to agentscope/schemas/
- Delete dead code: registry_builder.py, VisibilityBitRef
- Delete superseded plan docs
This commit is contained in:
zl-q
2026-03-22 20:35:55 +08:00
parent 20b9e70e84
commit 80ad5141a6
37 changed files with 628 additions and 2428 deletions
+39 -38
View File
@@ -6,7 +6,7 @@ import re
import tempfile
from collections.abc import AsyncIterator
from datetime import date
from typing import Annotated, Union
from typing import Annotated
from ag_ui.core import RunAgentInput
from core.agentscope.events import to_sse_event
@@ -28,7 +28,7 @@ from fastapi import (
UploadFile,
status,
)
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.responses import StreamingResponse
from services.base.redis import get_or_init_redis_client
from v1.agent.dependencies import get_agent_service
from v1.agent.schemas import (
@@ -39,7 +39,8 @@ from v1.agent.schemas import (
HistorySnapshotResponse,
TaskAcceptedResponse,
)
from v1.agent.service import AgentService, asr_service
from v1.agent.asr import asr_service
from v1.agent.service import AgentService
from v1.users.dependencies import get_current_user
router = APIRouter(prefix="/agent", tags=["agent"])
@@ -73,15 +74,13 @@ async def _acquire_sse_slot(*, user_id: str) -> bool:
count = await redis.incr(key)
if count == 1:
await redis.expire(key, _SSE_SLOT_TTL_SECONDS)
elif count > _MAX_SSE_CONNECTIONS_PER_USER:
await redis.decr(key)
return False
else:
ttl = await redis.ttl(key)
if int(ttl) < 0:
if ttl < 0:
await redis.expire(key, _SSE_SLOT_TTL_SECONDS)
if int(count) > _MAX_SSE_CONNECTIONS_PER_USER:
after_decr = await redis.decr(key)
if int(after_decr) <= 0:
await redis.delete(key)
return False
return True
except Exception as exc: # noqa: BLE001
logger.warning(
@@ -97,13 +96,18 @@ async def _release_sse_slot(*, user_id: str) -> None:
redis = await get_or_init_redis_client()
key = f"agent:sse-active:{user_id}"
count = await redis.decr(key)
if int(count) <= 0:
if count <= 0:
await redis.delete(key)
return None
ttl = await redis.ttl(key)
if int(ttl) < 0:
await redis.expire(key, _SSE_SLOT_TTL_SECONDS)
except Exception: # noqa: BLE001
else:
ttl = await redis.ttl(key)
if ttl < 0:
await redis.expire(key, _SSE_SLOT_TTL_SECONDS)
except Exception as exc: # noqa: BLE001
logger.warning(
"SSE slot release failed",
user_id=user_id,
reason=str(exc),
)
return None
@@ -176,6 +180,11 @@ async def stream_events(
last_event_id=cursor,
current_user=current_user,
)
except TimeoutError:
idle_polls += 1
yield ": keep-alive\n\n"
await asyncio.sleep(0.2)
continue
except Exception as exc: # noqa: BLE001
logger.warning(
"SSE stream read failed",
@@ -183,11 +192,6 @@ async def stream_events(
user_id=str(current_user.id),
reason=str(exc),
)
if "Timeout reading from" in str(exc):
idle_polls += 1
yield ": keep-alive\n\n"
await asyncio.sleep(0.2)
continue
break
if not rows:
@@ -291,12 +295,12 @@ async def create_attachment_signed_url(
async def transcribe(
audio: UploadFile,
request: Request,
_current_user: Annotated[CurrentUser, Depends(get_current_user)],
) -> Union[AsrTranscribeResponse, JSONResponse]:
current_user: Annotated[CurrentUser, Depends(get_current_user)],
) -> AsrTranscribeResponse:
temp_path: str | None = None
try:
if audio.content_type not in _ALLOWED_AUDIO_CONTENT_TYPES:
raise ValueError("Unsupported audio format")
raise HTTPException(status_code=400, detail="Unsupported audio format")
content_length = request.headers.get("content-length")
if content_length is not None:
@@ -309,7 +313,7 @@ async def transcribe(
and declared_length
> _MAX_TRANSCRIBE_AUDIO_BYTES + _MULTIPART_OVERHEAD_BYTES
):
raise ValueError("Audio file too large")
raise HTTPException(status_code=400, detail="Audio file too large")
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
temp_path = tmp_file.name
@@ -322,16 +326,16 @@ async def transcribe(
break
total_bytes += len(chunk)
if total_bytes > _MAX_TRANSCRIBE_AUDIO_BYTES:
raise ValueError("Audio file too large")
raise HTTPException(status_code=400, detail="Audio file too large")
if len(header) < _WAV_HEADER_MIN_BYTES:
required = _WAV_HEADER_MIN_BYTES - len(header)
header.extend(chunk[:required])
tmp_file.write(chunk)
if total_bytes == 0:
raise ValueError("Empty audio file")
raise HTTPException(status_code=400, detail="Empty audio file")
if not _looks_like_wav_header(bytes(header)):
raise ValueError("Unsupported audio format")
raise HTTPException(status_code=400, detail="Unsupported audio format")
transcript = await asr_service.transcribe_file(
temp_path, audio.filename or "unknown"
@@ -339,17 +343,14 @@ async def transcribe(
return AsrTranscribeResponse(transcript=transcript)
except ValueError as exc:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": str(exc)},
)
except HTTPException:
raise
except RuntimeError:
return JSONResponse(
status_code=status.HTTP_502_BAD_GATEWAY,
content={"detail": "ASR service unavailable"},
)
raise HTTPException(status_code=502, detail="ASR service unavailable")
finally:
await audio.close()
if temp_path and os.path.exists(temp_path):
os.unlink(temp_path)
if temp_path:
try:
os.unlink(temp_path)
except OSError:
pass