agent-brain-rag 1.2.0__py3-none-any.whl → 3.0.0__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.
- {agent_brain_rag-1.2.0.dist-info → agent_brain_rag-3.0.0.dist-info}/METADATA +55 -18
- agent_brain_rag-3.0.0.dist-info/RECORD +56 -0
- {agent_brain_rag-1.2.0.dist-info → agent_brain_rag-3.0.0.dist-info}/WHEEL +1 -1
- {agent_brain_rag-1.2.0.dist-info → agent_brain_rag-3.0.0.dist-info}/entry_points.txt +0 -1
- agent_brain_server/__init__.py +1 -1
- agent_brain_server/api/main.py +146 -45
- agent_brain_server/api/routers/__init__.py +2 -0
- agent_brain_server/api/routers/health.py +85 -21
- agent_brain_server/api/routers/index.py +108 -36
- agent_brain_server/api/routers/jobs.py +111 -0
- agent_brain_server/config/provider_config.py +352 -0
- agent_brain_server/config/settings.py +22 -5
- agent_brain_server/indexing/__init__.py +21 -0
- agent_brain_server/indexing/bm25_index.py +15 -2
- agent_brain_server/indexing/document_loader.py +45 -4
- agent_brain_server/indexing/embedding.py +86 -135
- agent_brain_server/indexing/graph_extractors.py +582 -0
- agent_brain_server/indexing/graph_index.py +536 -0
- agent_brain_server/job_queue/__init__.py +11 -0
- agent_brain_server/job_queue/job_service.py +317 -0
- agent_brain_server/job_queue/job_store.py +427 -0
- agent_brain_server/job_queue/job_worker.py +434 -0
- agent_brain_server/locking.py +101 -8
- agent_brain_server/models/__init__.py +28 -0
- agent_brain_server/models/graph.py +253 -0
- agent_brain_server/models/health.py +30 -3
- agent_brain_server/models/job.py +289 -0
- agent_brain_server/models/query.py +16 -3
- agent_brain_server/project_root.py +1 -1
- agent_brain_server/providers/__init__.py +64 -0
- agent_brain_server/providers/base.py +251 -0
- agent_brain_server/providers/embedding/__init__.py +23 -0
- agent_brain_server/providers/embedding/cohere.py +163 -0
- agent_brain_server/providers/embedding/ollama.py +150 -0
- agent_brain_server/providers/embedding/openai.py +118 -0
- agent_brain_server/providers/exceptions.py +95 -0
- agent_brain_server/providers/factory.py +157 -0
- agent_brain_server/providers/summarization/__init__.py +41 -0
- agent_brain_server/providers/summarization/anthropic.py +87 -0
- agent_brain_server/providers/summarization/gemini.py +96 -0
- agent_brain_server/providers/summarization/grok.py +95 -0
- agent_brain_server/providers/summarization/ollama.py +114 -0
- agent_brain_server/providers/summarization/openai.py +87 -0
- agent_brain_server/runtime.py +2 -2
- agent_brain_server/services/indexing_service.py +39 -0
- agent_brain_server/services/query_service.py +203 -0
- agent_brain_server/storage/__init__.py +18 -2
- agent_brain_server/storage/graph_store.py +519 -0
- agent_brain_server/storage/vector_store.py +35 -0
- agent_brain_server/storage_paths.py +5 -3
- agent_brain_rag-1.2.0.dist-info/RECORD +0 -31
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Health check endpoints."""
|
|
1
|
+
"""Health check endpoints with non-blocking queue status."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
from typing import Literal
|
|
@@ -20,6 +20,8 @@ router = APIRouter()
|
|
|
20
20
|
async def health_check(request: Request) -> HealthStatus:
|
|
21
21
|
"""Check server health status.
|
|
22
22
|
|
|
23
|
+
This endpoint never blocks and always returns quickly.
|
|
24
|
+
|
|
23
25
|
Returns:
|
|
24
26
|
HealthStatus with current status:
|
|
25
27
|
- healthy: Server is running and ready for queries
|
|
@@ -27,20 +29,35 @@ async def health_check(request: Request) -> HealthStatus:
|
|
|
27
29
|
- degraded: Server is up but some services are unavailable
|
|
28
30
|
- unhealthy: Server is not operational
|
|
29
31
|
"""
|
|
30
|
-
indexing_service = request.app.state.indexing_service
|
|
31
32
|
vector_store = request.app.state.vector_store
|
|
33
|
+
job_service = getattr(request.app.state, "job_service", None)
|
|
32
34
|
|
|
33
|
-
# Determine status
|
|
35
|
+
# Determine status using queue service (non-blocking)
|
|
34
36
|
status: Literal["healthy", "indexing", "degraded", "unhealthy"]
|
|
35
|
-
|
|
37
|
+
message: str
|
|
38
|
+
|
|
39
|
+
# Check queue status (non-blocking)
|
|
40
|
+
is_indexing = False
|
|
41
|
+
current_folder = None
|
|
42
|
+
if job_service:
|
|
43
|
+
try:
|
|
44
|
+
queue_stats = await job_service.get_queue_stats()
|
|
45
|
+
is_indexing = queue_stats.running > 0
|
|
46
|
+
if is_indexing and queue_stats.current_job_id:
|
|
47
|
+
# Get current job details for message
|
|
48
|
+
current_job = await job_service.get_job(queue_stats.current_job_id)
|
|
49
|
+
if current_job:
|
|
50
|
+
current_folder = current_job.folder_path
|
|
51
|
+
except Exception:
|
|
52
|
+
# Non-blocking: don't fail health check if queue service errors
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
if is_indexing:
|
|
36
56
|
status = "indexing"
|
|
37
|
-
message = f"Indexing in progress: {
|
|
57
|
+
message = f"Indexing in progress: {current_folder or 'unknown'}"
|
|
38
58
|
elif not vector_store.is_initialized:
|
|
39
59
|
status = "degraded"
|
|
40
60
|
message = "Vector store not initialized"
|
|
41
|
-
elif indexing_service.state.error:
|
|
42
|
-
status = "degraded"
|
|
43
|
-
message = f"Last indexing failed: {indexing_service.state.error}"
|
|
44
61
|
else:
|
|
45
62
|
status = "healthy"
|
|
46
63
|
message = "Server is running and ready for queries"
|
|
@@ -67,35 +84,82 @@ async def health_check(request: Request) -> HealthStatus:
|
|
|
67
84
|
"/status",
|
|
68
85
|
response_model=IndexingStatus,
|
|
69
86
|
summary="Indexing Status",
|
|
70
|
-
description="Returns detailed indexing status information.",
|
|
87
|
+
description="Returns detailed indexing status information. Never blocks.",
|
|
71
88
|
)
|
|
72
89
|
async def indexing_status(request: Request) -> IndexingStatus:
|
|
73
90
|
"""Get detailed indexing status.
|
|
74
91
|
|
|
92
|
+
This endpoint never blocks and always returns quickly, even during indexing.
|
|
93
|
+
|
|
75
94
|
Returns:
|
|
76
95
|
IndexingStatus with:
|
|
77
96
|
- total_documents: Number of documents indexed
|
|
78
97
|
- total_chunks: Number of chunks in vector store
|
|
79
98
|
- indexing_in_progress: Boolean indicating active indexing
|
|
99
|
+
- queue_pending: Number of pending jobs
|
|
100
|
+
- queue_running: Number of running jobs (0 or 1)
|
|
101
|
+
- current_job_running_time_ms: How long current job has been running
|
|
80
102
|
- last_indexed_at: Timestamp of last indexing operation
|
|
81
103
|
- indexed_folders: List of folders that have been indexed
|
|
82
104
|
"""
|
|
83
105
|
indexing_service = request.app.state.indexing_service
|
|
84
|
-
|
|
106
|
+
vector_store = request.app.state.vector_store
|
|
107
|
+
job_service = getattr(request.app.state, "job_service", None)
|
|
108
|
+
|
|
109
|
+
# Get vector store count (non-blocking read)
|
|
110
|
+
try:
|
|
111
|
+
total_chunks = (
|
|
112
|
+
await vector_store.get_count() if vector_store.is_initialized else 0
|
|
113
|
+
)
|
|
114
|
+
except Exception:
|
|
115
|
+
total_chunks = 0
|
|
116
|
+
|
|
117
|
+
# Get queue status (non-blocking)
|
|
118
|
+
queue_pending = 0
|
|
119
|
+
queue_running = 0
|
|
120
|
+
current_job_id = None
|
|
121
|
+
current_job_running_time_ms = None
|
|
122
|
+
progress_percent = 0.0
|
|
123
|
+
|
|
124
|
+
if job_service:
|
|
125
|
+
try:
|
|
126
|
+
queue_stats = await job_service.get_queue_stats()
|
|
127
|
+
queue_pending = queue_stats.pending
|
|
128
|
+
queue_running = queue_stats.running
|
|
129
|
+
current_job_id = queue_stats.current_job_id
|
|
130
|
+
current_job_running_time_ms = queue_stats.current_job_running_time_ms
|
|
131
|
+
|
|
132
|
+
# Get progress from current job
|
|
133
|
+
if current_job_id:
|
|
134
|
+
current_job = await job_service.get_job(current_job_id)
|
|
135
|
+
if current_job and current_job.progress:
|
|
136
|
+
progress_percent = current_job.progress.percent_complete
|
|
137
|
+
except Exception:
|
|
138
|
+
# Non-blocking: don't fail status if queue service errors
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
# Get indexing service status for historical data
|
|
142
|
+
# This is read-only and non-blocking
|
|
143
|
+
service_status = await indexing_service.get_status()
|
|
85
144
|
|
|
86
145
|
return IndexingStatus(
|
|
87
|
-
total_documents=
|
|
88
|
-
total_chunks=
|
|
89
|
-
total_doc_chunks=
|
|
90
|
-
total_code_chunks=
|
|
91
|
-
indexing_in_progress=
|
|
92
|
-
current_job_id=
|
|
93
|
-
progress_percent=
|
|
146
|
+
total_documents=service_status.get("total_documents", 0),
|
|
147
|
+
total_chunks=total_chunks,
|
|
148
|
+
total_doc_chunks=service_status.get("total_doc_chunks", 0),
|
|
149
|
+
total_code_chunks=service_status.get("total_code_chunks", 0),
|
|
150
|
+
indexing_in_progress=queue_running > 0,
|
|
151
|
+
current_job_id=current_job_id,
|
|
152
|
+
progress_percent=progress_percent,
|
|
94
153
|
last_indexed_at=(
|
|
95
|
-
datetime.fromisoformat(
|
|
96
|
-
if
|
|
154
|
+
datetime.fromisoformat(service_status["completed_at"])
|
|
155
|
+
if service_status.get("completed_at")
|
|
97
156
|
else None
|
|
98
157
|
),
|
|
99
|
-
indexed_folders=
|
|
100
|
-
supported_languages=
|
|
158
|
+
indexed_folders=service_status.get("indexed_folders", []),
|
|
159
|
+
supported_languages=service_status.get("supported_languages", []),
|
|
160
|
+
graph_index=service_status.get("graph_index"),
|
|
161
|
+
# Queue status (Feature 115)
|
|
162
|
+
queue_pending=queue_pending,
|
|
163
|
+
queue_running=queue_running,
|
|
164
|
+
current_job_running_time_ms=current_job_running_time_ms,
|
|
101
165
|
)
|
|
@@ -1,40 +1,52 @@
|
|
|
1
|
-
"""Indexing endpoints for document processing."""
|
|
1
|
+
"""Indexing endpoints for document processing with job queue support."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from fastapi import APIRouter, HTTPException, Request, status
|
|
6
|
+
from fastapi import APIRouter, HTTPException, Query, Request, status
|
|
7
7
|
|
|
8
|
+
from agent_brain_server.config import settings
|
|
8
9
|
from agent_brain_server.models import IndexRequest, IndexResponse
|
|
9
10
|
|
|
10
11
|
router = APIRouter()
|
|
11
12
|
|
|
13
|
+
# Maximum queue length for backpressure
|
|
14
|
+
MAX_QUEUE_LENGTH = settings.AGENT_BRAIN_MAX_QUEUE
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
@router.post(
|
|
14
18
|
"/",
|
|
15
19
|
response_model=IndexResponse,
|
|
16
20
|
status_code=status.HTTP_202_ACCEPTED,
|
|
17
21
|
summary="Index Documents",
|
|
18
|
-
description="
|
|
22
|
+
description="Enqueue a job to index documents from a folder.",
|
|
19
23
|
)
|
|
20
24
|
async def index_documents(
|
|
21
|
-
request_body: IndexRequest,
|
|
25
|
+
request_body: IndexRequest,
|
|
26
|
+
request: Request,
|
|
27
|
+
force: bool = Query(False, description="Bypass deduplication and force a new job"),
|
|
28
|
+
allow_external: bool = Query(
|
|
29
|
+
False, description="Allow paths outside the project directory"
|
|
30
|
+
),
|
|
22
31
|
) -> IndexResponse:
|
|
23
|
-
"""
|
|
32
|
+
"""Enqueue an indexing job for documents from the specified folder.
|
|
24
33
|
|
|
25
|
-
This endpoint
|
|
26
|
-
|
|
34
|
+
This endpoint accepts the request and returns immediately with a job ID.
|
|
35
|
+
The job is processed asynchronously by a background worker.
|
|
36
|
+
Use the /index/jobs/{job_id} endpoint to monitor progress.
|
|
27
37
|
|
|
28
38
|
Args:
|
|
29
39
|
request_body: IndexRequest with folder_path and optional configuration.
|
|
30
40
|
request: FastAPI request for accessing app state.
|
|
41
|
+
force: If True, bypass deduplication and create a new job.
|
|
42
|
+
allow_external: If True, allow indexing paths outside the project.
|
|
31
43
|
|
|
32
44
|
Returns:
|
|
33
45
|
IndexResponse with job_id and status.
|
|
34
46
|
|
|
35
47
|
Raises:
|
|
36
|
-
400: Invalid folder path
|
|
37
|
-
|
|
48
|
+
400: Invalid folder path or path outside project (without allow_external)
|
|
49
|
+
429: Queue is full (backpressure)
|
|
38
50
|
"""
|
|
39
51
|
# Validate folder path
|
|
40
52
|
folder_path = Path(request_body.folder_path).expanduser().resolve()
|
|
@@ -57,17 +69,20 @@ async def index_documents(
|
|
|
57
69
|
detail=f"Cannot read folder: {request_body.folder_path}",
|
|
58
70
|
)
|
|
59
71
|
|
|
60
|
-
# Get
|
|
61
|
-
|
|
72
|
+
# Get job service from app state
|
|
73
|
+
job_service = request.app.state.job_service
|
|
62
74
|
|
|
63
|
-
#
|
|
64
|
-
|
|
75
|
+
# Backpressure check (pending + running to prevent overflow)
|
|
76
|
+
stats = await job_service.get_queue_stats()
|
|
77
|
+
active_jobs = stats.pending + stats.running
|
|
78
|
+
if active_jobs >= MAX_QUEUE_LENGTH:
|
|
65
79
|
raise HTTPException(
|
|
66
|
-
status_code=status.
|
|
67
|
-
detail="
|
|
80
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
81
|
+
detail=f"Queue full ({stats.pending} pending, {stats.running} running). "
|
|
82
|
+
"Try again later.",
|
|
68
83
|
)
|
|
69
84
|
|
|
70
|
-
#
|
|
85
|
+
# Enqueue the job
|
|
71
86
|
try:
|
|
72
87
|
# Update request with resolved path
|
|
73
88
|
resolved_request = IndexRequest(
|
|
@@ -82,17 +97,37 @@ async def index_documents(
|
|
|
82
97
|
exclude_patterns=request_body.exclude_patterns,
|
|
83
98
|
generate_summaries=request_body.generate_summaries,
|
|
84
99
|
)
|
|
85
|
-
|
|
100
|
+
|
|
101
|
+
result = await job_service.enqueue_job(
|
|
102
|
+
request=resolved_request,
|
|
103
|
+
operation="index",
|
|
104
|
+
force=force,
|
|
105
|
+
allow_external=allow_external,
|
|
106
|
+
)
|
|
107
|
+
except ValueError as e:
|
|
108
|
+
# Path validation error (outside project)
|
|
109
|
+
raise HTTPException(
|
|
110
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
111
|
+
detail=str(e),
|
|
112
|
+
) from e
|
|
86
113
|
except Exception as e:
|
|
87
114
|
raise HTTPException(
|
|
88
115
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
89
|
-
detail=f"Failed to
|
|
116
|
+
detail=f"Failed to enqueue indexing job: {str(e)}",
|
|
90
117
|
) from e
|
|
91
118
|
|
|
119
|
+
# Build response message
|
|
120
|
+
if result.dedupe_hit:
|
|
121
|
+
message = (
|
|
122
|
+
f"Duplicate detected - existing job {result.job_id} is {result.status}"
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
message = f"Job queued for {request_body.folder_path}"
|
|
126
|
+
|
|
92
127
|
return IndexResponse(
|
|
93
|
-
job_id=job_id,
|
|
94
|
-
status=
|
|
95
|
-
message=
|
|
128
|
+
job_id=result.job_id,
|
|
129
|
+
status=result.status,
|
|
130
|
+
message=message,
|
|
96
131
|
)
|
|
97
132
|
|
|
98
133
|
|
|
@@ -101,10 +136,17 @@ async def index_documents(
|
|
|
101
136
|
response_model=IndexResponse,
|
|
102
137
|
status_code=status.HTTP_202_ACCEPTED,
|
|
103
138
|
summary="Add Documents",
|
|
104
|
-
description="
|
|
139
|
+
description="Enqueue a job to add documents from another folder.",
|
|
105
140
|
)
|
|
106
|
-
async def add_documents(
|
|
107
|
-
|
|
141
|
+
async def add_documents(
|
|
142
|
+
request_body: IndexRequest,
|
|
143
|
+
request: Request,
|
|
144
|
+
force: bool = Query(False, description="Bypass deduplication and force a new job"),
|
|
145
|
+
allow_external: bool = Query(
|
|
146
|
+
False, description="Allow paths outside the project directory"
|
|
147
|
+
),
|
|
148
|
+
) -> IndexResponse:
|
|
149
|
+
"""Enqueue a job to add documents from a new folder to the existing index.
|
|
108
150
|
|
|
109
151
|
This is similar to the index endpoint but adds to the existing
|
|
110
152
|
vector store instead of replacing it.
|
|
@@ -112,6 +154,8 @@ async def add_documents(request_body: IndexRequest, request: Request) -> IndexRe
|
|
|
112
154
|
Args:
|
|
113
155
|
request_body: IndexRequest with folder_path and optional configuration.
|
|
114
156
|
request: FastAPI request for accessing app state.
|
|
157
|
+
force: If True, bypass deduplication and create a new job.
|
|
158
|
+
allow_external: If True, allow indexing paths outside the project.
|
|
115
159
|
|
|
116
160
|
Returns:
|
|
117
161
|
IndexResponse with job_id and status.
|
|
@@ -131,12 +175,17 @@ async def add_documents(request_body: IndexRequest, request: Request) -> IndexRe
|
|
|
131
175
|
detail=f"Path is not a directory: {request_body.folder_path}",
|
|
132
176
|
)
|
|
133
177
|
|
|
134
|
-
|
|
178
|
+
# Get job service from app state
|
|
179
|
+
job_service = request.app.state.job_service
|
|
135
180
|
|
|
136
|
-
|
|
181
|
+
# Backpressure check (pending + running to prevent overflow)
|
|
182
|
+
stats = await job_service.get_queue_stats()
|
|
183
|
+
active_jobs = stats.pending + stats.running
|
|
184
|
+
if active_jobs >= MAX_QUEUE_LENGTH:
|
|
137
185
|
raise HTTPException(
|
|
138
|
-
status_code=status.
|
|
139
|
-
detail="
|
|
186
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
187
|
+
detail=f"Queue full ({stats.pending} pending, {stats.running} running). "
|
|
188
|
+
"Try again later.",
|
|
140
189
|
)
|
|
141
190
|
|
|
142
191
|
try:
|
|
@@ -151,17 +200,36 @@ async def add_documents(request_body: IndexRequest, request: Request) -> IndexRe
|
|
|
151
200
|
include_patterns=request_body.include_patterns,
|
|
152
201
|
exclude_patterns=request_body.exclude_patterns,
|
|
153
202
|
)
|
|
154
|
-
|
|
203
|
+
|
|
204
|
+
result = await job_service.enqueue_job(
|
|
205
|
+
request=resolved_request,
|
|
206
|
+
operation="add",
|
|
207
|
+
force=force,
|
|
208
|
+
allow_external=allow_external,
|
|
209
|
+
)
|
|
210
|
+
except ValueError as e:
|
|
211
|
+
raise HTTPException(
|
|
212
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
213
|
+
detail=str(e),
|
|
214
|
+
) from e
|
|
155
215
|
except Exception as e:
|
|
156
216
|
raise HTTPException(
|
|
157
217
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
158
|
-
detail=f"Failed to add
|
|
218
|
+
detail=f"Failed to enqueue add job: {str(e)}",
|
|
159
219
|
) from e
|
|
160
220
|
|
|
221
|
+
# Build response message
|
|
222
|
+
if result.dedupe_hit:
|
|
223
|
+
message = (
|
|
224
|
+
f"Duplicate detected - existing job {result.job_id} is {result.status}"
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
message = f"Job queued to add documents from {request_body.folder_path}"
|
|
228
|
+
|
|
161
229
|
return IndexResponse(
|
|
162
|
-
job_id=job_id,
|
|
163
|
-
status=
|
|
164
|
-
message=
|
|
230
|
+
job_id=result.job_id,
|
|
231
|
+
status=result.status,
|
|
232
|
+
message=message,
|
|
165
233
|
)
|
|
166
234
|
|
|
167
235
|
|
|
@@ -175,6 +243,7 @@ async def reset_index(request: Request) -> IndexResponse:
|
|
|
175
243
|
"""Reset the index by deleting all stored documents.
|
|
176
244
|
|
|
177
245
|
Warning: This permanently removes all indexed content.
|
|
246
|
+
Cannot be performed while jobs are running.
|
|
178
247
|
|
|
179
248
|
Args:
|
|
180
249
|
request: FastAPI request for accessing app state.
|
|
@@ -183,14 +252,17 @@ async def reset_index(request: Request) -> IndexResponse:
|
|
|
183
252
|
IndexResponse confirming the reset.
|
|
184
253
|
|
|
185
254
|
Raises:
|
|
186
|
-
409:
|
|
255
|
+
409: Jobs in progress
|
|
187
256
|
"""
|
|
257
|
+
job_service = request.app.state.job_service
|
|
188
258
|
indexing_service = request.app.state.indexing_service
|
|
189
259
|
|
|
190
|
-
if
|
|
260
|
+
# Check if any jobs are running
|
|
261
|
+
stats = await job_service.get_queue_stats()
|
|
262
|
+
if stats.running > 0:
|
|
191
263
|
raise HTTPException(
|
|
192
264
|
status_code=status.HTTP_409_CONFLICT,
|
|
193
|
-
detail="Cannot reset while indexing
|
|
265
|
+
detail="Cannot reset while indexing jobs are in progress.",
|
|
194
266
|
)
|
|
195
267
|
|
|
196
268
|
try:
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Job management endpoints for indexing job queue."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Query, Request, status
|
|
6
|
+
|
|
7
|
+
from agent_brain_server.job_queue.job_service import JobQueueService
|
|
8
|
+
from agent_brain_server.models.job import JobDetailResponse, JobListResponse
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get(
|
|
14
|
+
"/",
|
|
15
|
+
response_model=JobListResponse,
|
|
16
|
+
summary="List Jobs",
|
|
17
|
+
description="List all indexing jobs with pagination.",
|
|
18
|
+
)
|
|
19
|
+
async def list_jobs(
|
|
20
|
+
request: Request,
|
|
21
|
+
limit: int = Query(
|
|
22
|
+
50, ge=1, le=100, description="Maximum number of jobs to return"
|
|
23
|
+
),
|
|
24
|
+
offset: int = Query(0, ge=0, description="Number of jobs to skip"),
|
|
25
|
+
) -> JobListResponse:
|
|
26
|
+
"""List all jobs with pagination.
|
|
27
|
+
|
|
28
|
+
Returns a paginated list of jobs with summary information and queue statistics.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
request: FastAPI request for accessing app state.
|
|
32
|
+
limit: Maximum number of jobs to return (1-100, default 50).
|
|
33
|
+
offset: Number of jobs to skip for pagination (default 0).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
JobListResponse with list of job summaries and queue statistics.
|
|
37
|
+
"""
|
|
38
|
+
job_service: JobQueueService = request.app.state.job_service
|
|
39
|
+
return await job_service.list_jobs(limit=limit, offset=offset)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get(
|
|
43
|
+
"/{job_id}",
|
|
44
|
+
response_model=JobDetailResponse,
|
|
45
|
+
summary="Get Job Details",
|
|
46
|
+
description="Get detailed information about a specific job.",
|
|
47
|
+
)
|
|
48
|
+
async def get_job(job_id: str, request: Request) -> JobDetailResponse:
|
|
49
|
+
"""Get details for a specific job.
|
|
50
|
+
|
|
51
|
+
Returns full job information including progress, timestamps, and results.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
job_id: The unique job identifier.
|
|
55
|
+
request: FastAPI request for accessing app state.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
JobDetailResponse with full job details.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
404: Job not found.
|
|
62
|
+
"""
|
|
63
|
+
job_service: JobQueueService = request.app.state.job_service
|
|
64
|
+
job = await job_service.get_job(job_id)
|
|
65
|
+
if not job:
|
|
66
|
+
raise HTTPException(
|
|
67
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
68
|
+
detail=f"Job {job_id} not found",
|
|
69
|
+
)
|
|
70
|
+
return job
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.delete(
|
|
74
|
+
"/{job_id}",
|
|
75
|
+
summary="Cancel Job",
|
|
76
|
+
description="Cancel a pending or running job.",
|
|
77
|
+
)
|
|
78
|
+
async def cancel_job(job_id: str, request: Request) -> dict[str, Any]:
|
|
79
|
+
"""Cancel a job.
|
|
80
|
+
|
|
81
|
+
Cancellation behavior depends on job status:
|
|
82
|
+
- PENDING jobs are cancelled immediately
|
|
83
|
+
- RUNNING jobs have cancel_requested flag set; worker will stop at next checkpoint
|
|
84
|
+
- Completed/Failed/Cancelled jobs return 409 Conflict
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
job_id: The unique job identifier.
|
|
88
|
+
request: FastAPI request for accessing app state.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dictionary with cancellation status and message.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
404: Job not found.
|
|
95
|
+
409: Job cannot be cancelled (already completed, failed, or cancelled).
|
|
96
|
+
"""
|
|
97
|
+
job_service: JobQueueService = request.app.state.job_service
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
result = await job_service.cancel_job(job_id)
|
|
101
|
+
return result
|
|
102
|
+
except KeyError as e:
|
|
103
|
+
raise HTTPException(
|
|
104
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
105
|
+
detail=str(e),
|
|
106
|
+
) from e
|
|
107
|
+
except ValueError as e:
|
|
108
|
+
raise HTTPException(
|
|
109
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
110
|
+
detail=str(e),
|
|
111
|
+
) from e
|