feat: Phase 1.3 基础 API — FastAPI 服务器
- src/api/server.py: FastAPI 主服务,端口 7821 - src/api/routes/commit.py: /commit, /batch-commit - src/api/routes/recall.py: /recall, /recall/batch - src/api/routes/conflicts.py: /conflicts/* 冲突检测 - src/api/routes/feedback.py: /feedback/* 反馈回路 - src/api/routes/admin.py: /stats, /config - pyproject.toml: 项目依赖定义 - .venv: FastAPI + uvicorn 环境
This commit is contained in:
parent
961e071149
commit
9986e04d1b
|
|
@ -0,0 +1,9 @@
|
|||
[project]
|
||||
name = 'zhiyi'
|
||||
version = '0.1.0'
|
||||
description = '织忆 MemoryWeave - 独立记忆服务'
|
||||
requires-python = '>=3.10'
|
||||
dependencies = ['fastapi', 'uvicorn']
|
||||
|
||||
[project.scripts]
|
||||
zhiyi = 'api.server:main'
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
from fastapi import APIRouter
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'src'))
|
||||
|
||||
from storage.jsonl_store import JSONLShardStore
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get('/stats')
|
||||
def get_stats():
|
||||
store = JSONLShardStore()
|
||||
episode_stats = store.get_stats('episodes')
|
||||
distilled_stats = store.get_stats('distilled')
|
||||
return {
|
||||
'episodes': episode_stats,
|
||||
'distilled': distilled_stats,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
@router.get('/config')
|
||||
def get_config():
|
||||
return {
|
||||
'decay_rate': 0.015,
|
||||
'decay_floor': 0.1,
|
||||
'distill_threshold': 0.7,
|
||||
'version': '2.6'
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'src'))
|
||||
|
||||
from models.episode import Episode
|
||||
from distill.engine import DistillEngine
|
||||
from storage.jsonl_store import JSONLShardStore
|
||||
|
||||
router = APIRouter()
|
||||
_store = JSONLShardStore()
|
||||
_engine = DistillEngine()
|
||||
|
||||
class CommitRequest(BaseModel):
|
||||
content: str
|
||||
source: str = 'hermes'
|
||||
entities: List[str] = []
|
||||
facts: List[str] = []
|
||||
metadata: dict = {}
|
||||
|
||||
class BatchCommitRequest(BaseModel):
|
||||
records: List[CommitRequest]
|
||||
|
||||
@router.post('/commit')
|
||||
def commit_memory(req: CommitRequest):
|
||||
episode = Episode.create(
|
||||
content=req.content,
|
||||
source=req.source,
|
||||
entities=req.entities,
|
||||
facts=req.facts,
|
||||
metadata=req.metadata
|
||||
)
|
||||
_store.append('episodes', episode.to_dict())
|
||||
_engine.queue.enqueue(episode)
|
||||
return {'status': 'ok', 'episode_id': episode.id}
|
||||
|
||||
@router.post('/batch-commit')
|
||||
def batch_commit(req: BatchCommitRequest):
|
||||
results = []
|
||||
for r in req.records:
|
||||
episode = Episode.create(content=r.content, source=r.source,
|
||||
entities=r.entities, facts=r.facts,
|
||||
metadata=r.metadata)
|
||||
_store.append('episodes', episode.to_dict())
|
||||
_engine.queue.enqueue(episode)
|
||||
results.append({'episode_id': episode.id})
|
||||
return {'status': 'ok', 'committed': len(results)}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'src'))
|
||||
|
||||
from distill.conflicts import ConflictDetector, ConflictType, ConflictStrategy
|
||||
from storage.jsonl_store import JSONLShardStore
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
_detector = ConflictDetector()
|
||||
_store = JSONLShardStore()
|
||||
_conflicts_store: List[dict] = []
|
||||
|
||||
class ResolveRequest(BaseModel):
|
||||
conflict_id: str
|
||||
resolution: dict
|
||||
|
||||
@router.get('/conflicts')
|
||||
def list_conflicts():
|
||||
return {'conflicts': _conflicts_store, 'count': len(_conflicts_store)}
|
||||
|
||||
@router.post('/conflicts/scan')
|
||||
def scan_conflicts():
|
||||
now = datetime.now()
|
||||
distilled_records = list(_store.read_month('distilled', now.year, now.month))
|
||||
|
||||
class FakeDistilled:
|
||||
def __init__(self, d):
|
||||
self.id = d.get('id', '')
|
||||
self.entities = d.get('entities', [])
|
||||
self.facts = d.get('facts', [])
|
||||
|
||||
all_conflicts = []
|
||||
for record in distilled_records:
|
||||
fd = FakeDistilled(record)
|
||||
conflicts = _detector.scan_conflicts(fd, distilled_records)
|
||||
for c in conflicts:
|
||||
c_dict = {
|
||||
'id': c.id,
|
||||
'type': c.type.value,
|
||||
'entity': c.entity,
|
||||
'entries': c.entries,
|
||||
'strategy': c.strategy.value,
|
||||
'status': c.status,
|
||||
'created_at': c.created_at.isoformat()
|
||||
}
|
||||
if not any(x['id'] == c_dict['id'] for x in _conflicts_store):
|
||||
_conflicts_store.append(c_dict)
|
||||
all_conflicts.append(c_dict)
|
||||
|
||||
return {'conflicts': all_conflicts, 'count': len(all_conflicts)}
|
||||
|
||||
@router.post('/conflicts/resolve')
|
||||
def resolve_conflict(req: ResolveRequest):
|
||||
for c in _conflicts_store:
|
||||
if c['id'] == req.conflict_id:
|
||||
c['status'] = 'resolved'
|
||||
c['resolution'] = req.resolution
|
||||
return {'status': 'ok', 'conflict_id': req.conflict_id}
|
||||
raise HTTPException(status_code=404, detail='Conflict not found')
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'src'))
|
||||
|
||||
from storage.tombstone import TombstoneStore
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ValidateRequest(BaseModel):
|
||||
distilled_id: str
|
||||
namespace: str = 'zhiyi'
|
||||
importance: Optional[int] = None
|
||||
|
||||
class CorrectRequest(BaseModel):
|
||||
distilled_id: str
|
||||
correction: dict
|
||||
reason: str = ''
|
||||
|
||||
class DeprecateRequest(BaseModel):
|
||||
distilled_id: str
|
||||
reason: str = ''
|
||||
suggested_replacement: Optional[str] = None
|
||||
|
||||
@router.post('/feedback/validate')
|
||||
def validate_memory(req: ValidateRequest):
|
||||
return {'status': 'ok', 'distilled_id': req.distilled_id, 'importance': req.importance}
|
||||
|
||||
@router.post('/feedback/correct')
|
||||
def correct_memory(req: CorrectRequest):
|
||||
return {'status': 'ok', 'distilled_id': req.distilled_id, 'correction': req.correction}
|
||||
|
||||
@router.post('/feedback/deprecate')
|
||||
def deprecate_memory(req: DeprecateRequest):
|
||||
t = TombstoneStore()
|
||||
t.mark_deleted(req.distilled_id, reason=req.reason)
|
||||
return {'status': 'ok', 'distilled_id': req.distilled_id}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'src'))
|
||||
|
||||
from storage.jsonl_store import JSONLShardStore
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
_store = JSONLShardStore()
|
||||
|
||||
class RecallRequest(BaseModel):
|
||||
query: str
|
||||
limit: int = 10
|
||||
namespace: Optional[str] = 'zhiyi'
|
||||
start_time: Optional[str] = None
|
||||
end_time: Optional[str] = None
|
||||
|
||||
@router.post('/recall')
|
||||
def recall_memory(req: RecallRequest):
|
||||
start = datetime.fromisoformat(req.start_time) if req.start_time else None
|
||||
end = datetime.fromisoformat(req.end_time) if req.end_time else None
|
||||
|
||||
results = []
|
||||
if start and end:
|
||||
records = _store.query_by_timerange('distilled', start, end)
|
||||
else:
|
||||
now = datetime.now()
|
||||
records = _store.read_month('distilled', now.year, now.month)
|
||||
|
||||
query_lower = req.query.lower()
|
||||
for record in records:
|
||||
content = str(record.get('summary', '')) + ' ' + ' '.join(str(f) for f in record.get('facts', []))
|
||||
if query_lower in content.lower():
|
||||
results.append(record)
|
||||
if len(results) >= req.limit:
|
||||
break
|
||||
|
||||
return {'results': results, 'count': len(results), 'query': req.query}
|
||||
|
||||
@router.post('/recall/batch')
|
||||
def recall_batch(requests: List[RecallRequest]):
|
||||
return {'results': [recall_memory(r) for r in requests]}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pathlib import Path
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
|
||||
|
||||
app = FastAPI(title='织忆 MemoryWeave API', version='2.6')
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=['*'],
|
||||
allow_credentials=True,
|
||||
allow_methods=['*'],
|
||||
allow_headers=['*'],
|
||||
)
|
||||
|
||||
from api.routes import commit, recall, conflicts, feedback, admin
|
||||
|
||||
app.include_router(commit.router, prefix='/api/v1', tags=['commit'])
|
||||
app.include_router(recall.router, prefix='/api/v1', tags=['recall'])
|
||||
app.include_router(conflicts.router, prefix='/api/v1', tags=['conflicts'])
|
||||
app.include_router(feedback.router, prefix='/api/v1', tags=['feedback'])
|
||||
app.include_router(admin.router, prefix='/api/v1', tags=['admin'])
|
||||
|
||||
@app.get('/health')
|
||||
def health():
|
||||
return {'status': 'ok', 'service': '织忆 MemoryWeave', 'version': '2.6'}
|
||||
|
||||
@app.get('/')
|
||||
def root():
|
||||
return {'service': '织忆 MemoryWeave', 'version': '2.6', 'docs': '/docs'}
|
||||
|
||||
def main():
|
||||
import uvicorn
|
||||
uvicorn.run(app, host='0.0.0.0', port=7821)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in New Issue