memgentic-api 0.4.4__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.
- memgentic_api/__init__.py +3 -0
- memgentic_api/auth.py +33 -0
- memgentic_api/deps.py +42 -0
- memgentic_api/main.py +291 -0
- memgentic_api/routes/__init__.py +0 -0
- memgentic_api/routes/collections.py +242 -0
- memgentic_api/routes/graph.py +50 -0
- memgentic_api/routes/import_export.py +98 -0
- memgentic_api/routes/ingestion.py +112 -0
- memgentic_api/routes/memories.py +523 -0
- memgentic_api/routes/skills.py +558 -0
- memgentic_api/routes/sources.py +32 -0
- memgentic_api/routes/stats.py +109 -0
- memgentic_api/routes/uploads.py +304 -0
- memgentic_api/routes/websocket.py +38 -0
- memgentic_api/schemas.py +451 -0
- memgentic_api-0.4.4.dist-info/METADATA +14 -0
- memgentic_api-0.4.4.dist-info/RECORD +19 -0
- memgentic_api-0.4.4.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Import and export endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import structlog
|
|
6
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
7
|
+
from memgentic.config import settings
|
|
8
|
+
from memgentic.models import ContentType, Platform
|
|
9
|
+
|
|
10
|
+
from memgentic_api.deps import MetadataStoreDep, PipelineDep, limiter
|
|
11
|
+
from memgentic_api.schemas import ImportMemoriesRequest
|
|
12
|
+
|
|
13
|
+
logger = structlog.get_logger()
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.post("/import/json", status_code=201)
|
|
18
|
+
@limiter.limit(lambda: f"{settings.rate_limit_import}/minute")
|
|
19
|
+
async def import_json(
|
|
20
|
+
request: Request,
|
|
21
|
+
body: ImportMemoriesRequest,
|
|
22
|
+
pipeline: PipelineDep,
|
|
23
|
+
) -> dict:
|
|
24
|
+
"""Import memories from a JSON array."""
|
|
25
|
+
imported = 0
|
|
26
|
+
errors = 0
|
|
27
|
+
|
|
28
|
+
for item in body.memories:
|
|
29
|
+
try:
|
|
30
|
+
ct = ContentType(item.content_type)
|
|
31
|
+
except ValueError:
|
|
32
|
+
ct = ContentType.FACT
|
|
33
|
+
try:
|
|
34
|
+
platform = Platform(item.source)
|
|
35
|
+
except ValueError:
|
|
36
|
+
platform = Platform.UNKNOWN
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
await pipeline.ingest_single(
|
|
40
|
+
content=item.content,
|
|
41
|
+
content_type=ct,
|
|
42
|
+
platform=platform,
|
|
43
|
+
topics=item.topics,
|
|
44
|
+
entities=item.entities,
|
|
45
|
+
)
|
|
46
|
+
imported += 1
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.warning("import.item_failed", error=str(e))
|
|
49
|
+
errors += 1
|
|
50
|
+
|
|
51
|
+
return {"imported": imported, "errors": errors, "total": len(body.memories)}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@router.get("/export")
|
|
55
|
+
@limiter.limit(lambda: f"{settings.rate_limit_default}/minute")
|
|
56
|
+
async def export_json(
|
|
57
|
+
request: Request,
|
|
58
|
+
metadata_store: MetadataStoreDep,
|
|
59
|
+
source: str | None = None,
|
|
60
|
+
) -> dict:
|
|
61
|
+
"""Export all memories as JSON."""
|
|
62
|
+
from memgentic.models import SessionConfig
|
|
63
|
+
|
|
64
|
+
config = SessionConfig()
|
|
65
|
+
if source:
|
|
66
|
+
try:
|
|
67
|
+
config.include_sources = [Platform(source)]
|
|
68
|
+
except ValueError:
|
|
69
|
+
raise HTTPException(
|
|
70
|
+
status_code=422, detail=f"Invalid source platform: {source}"
|
|
71
|
+
) from None
|
|
72
|
+
|
|
73
|
+
memories = await metadata_store.get_memories_by_filter(
|
|
74
|
+
session_config=config,
|
|
75
|
+
limit=10000,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"count": len(memories),
|
|
80
|
+
"memories": [
|
|
81
|
+
{
|
|
82
|
+
"id": m.id,
|
|
83
|
+
"content": m.content,
|
|
84
|
+
"content_type": m.content_type.value,
|
|
85
|
+
"platform": m.source.platform.value,
|
|
86
|
+
"platform_version": m.source.platform_version,
|
|
87
|
+
"session_id": m.source.session_id,
|
|
88
|
+
"session_title": m.source.session_title,
|
|
89
|
+
"capture_method": m.source.capture_method.value,
|
|
90
|
+
"topics": m.topics,
|
|
91
|
+
"entities": m.entities,
|
|
92
|
+
"confidence": m.confidence,
|
|
93
|
+
"status": m.status.value,
|
|
94
|
+
"created_at": m.created_at.isoformat(),
|
|
95
|
+
}
|
|
96
|
+
for m in memories
|
|
97
|
+
],
|
|
98
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Ingestion job routes — list, inspect, and cancel running ingestion jobs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
9
|
+
from memgentic.config import settings
|
|
10
|
+
from memgentic.events import EventType, MemgenticEvent, event_bus
|
|
11
|
+
from memgentic.models import IngestionJob, IngestionJobStatus
|
|
12
|
+
|
|
13
|
+
from memgentic_api.deps import MetadataStoreDep, limiter
|
|
14
|
+
from memgentic_api.schemas import IngestionJobListResponse, IngestionJobResponse
|
|
15
|
+
|
|
16
|
+
logger = structlog.get_logger()
|
|
17
|
+
router = APIRouter()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _job_to_response(job: IngestionJob) -> IngestionJobResponse:
|
|
21
|
+
"""Serialize an ``IngestionJob`` model into an API response."""
|
|
22
|
+
return IngestionJobResponse(
|
|
23
|
+
id=job.id,
|
|
24
|
+
source_type=job.source_type,
|
|
25
|
+
source_path=job.source_path,
|
|
26
|
+
status=job.status.value,
|
|
27
|
+
total_items=job.total_items,
|
|
28
|
+
processed_items=job.processed_items,
|
|
29
|
+
failed_items=job.failed_items,
|
|
30
|
+
error_message=job.error_message,
|
|
31
|
+
started_at=job.started_at,
|
|
32
|
+
completed_at=job.completed_at,
|
|
33
|
+
created_at=job.created_at,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("/ingestion/jobs")
|
|
38
|
+
@limiter.limit(lambda: f"{settings.rate_limit_default}/minute")
|
|
39
|
+
async def list_ingestion_jobs(
|
|
40
|
+
request: Request,
|
|
41
|
+
metadata_store: MetadataStoreDep,
|
|
42
|
+
limit: int = Query(default=50, ge=1, le=500),
|
|
43
|
+
offset: int = Query(default=0, ge=0),
|
|
44
|
+
) -> IngestionJobListResponse:
|
|
45
|
+
"""List ingestion jobs (most recent first, paginated)."""
|
|
46
|
+
jobs, total = await metadata_store.get_ingestion_jobs(limit=limit, offset=offset)
|
|
47
|
+
return IngestionJobListResponse(
|
|
48
|
+
jobs=[_job_to_response(j) for j in jobs],
|
|
49
|
+
total=total,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.get("/ingestion/jobs/{job_id}")
|
|
54
|
+
@limiter.limit(lambda: f"{settings.rate_limit_default}/minute")
|
|
55
|
+
async def get_ingestion_job(
|
|
56
|
+
request: Request,
|
|
57
|
+
job_id: str,
|
|
58
|
+
metadata_store: MetadataStoreDep,
|
|
59
|
+
) -> IngestionJobResponse:
|
|
60
|
+
"""Fetch a single ingestion job by id."""
|
|
61
|
+
job = await metadata_store.get_ingestion_job(job_id)
|
|
62
|
+
if not job:
|
|
63
|
+
raise HTTPException(status_code=404, detail="Ingestion job not found")
|
|
64
|
+
return _job_to_response(job)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@router.post("/ingestion/jobs/{job_id}/cancel")
|
|
68
|
+
@limiter.limit(lambda: f"{settings.rate_limit_default}/minute")
|
|
69
|
+
async def cancel_ingestion_job(
|
|
70
|
+
request: Request,
|
|
71
|
+
job_id: str,
|
|
72
|
+
metadata_store: MetadataStoreDep,
|
|
73
|
+
) -> IngestionJobResponse:
|
|
74
|
+
"""Cancel a running or queued ingestion job.
|
|
75
|
+
|
|
76
|
+
Terminal jobs (``completed``/``failed``) are returned unchanged with a
|
|
77
|
+
409 so that callers can distinguish a true cancel from a no-op.
|
|
78
|
+
"""
|
|
79
|
+
job = await metadata_store.get_ingestion_job(job_id)
|
|
80
|
+
if not job:
|
|
81
|
+
raise HTTPException(status_code=404, detail="Ingestion job not found")
|
|
82
|
+
|
|
83
|
+
if job.status in (IngestionJobStatus.COMPLETED, IngestionJobStatus.FAILED):
|
|
84
|
+
raise HTTPException(
|
|
85
|
+
status_code=409,
|
|
86
|
+
detail=f"Cannot cancel job in status '{job.status.value}'",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
now = datetime.now(UTC)
|
|
90
|
+
await metadata_store.update_ingestion_job(
|
|
91
|
+
job_id,
|
|
92
|
+
status=IngestionJobStatus.FAILED,
|
|
93
|
+
error_message="Cancelled by user",
|
|
94
|
+
completed_at=now,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
updated = await metadata_store.get_ingestion_job(job_id)
|
|
98
|
+
if not updated:
|
|
99
|
+
raise HTTPException(status_code=500, detail="Failed to reload cancelled job")
|
|
100
|
+
|
|
101
|
+
await event_bus.emit(
|
|
102
|
+
MemgenticEvent(
|
|
103
|
+
type=EventType.INGESTION_COMPLETED,
|
|
104
|
+
data={
|
|
105
|
+
"id": updated.id,
|
|
106
|
+
"status": updated.status.value,
|
|
107
|
+
"cancelled": True,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return _job_to_response(updated)
|