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.
@@ -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)