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:
xiaowei 2026-05-25 02:25:01 +08:00
parent 961e071149
commit 9986e04d1b
9 changed files with 272 additions and 0 deletions

9
pyproject.toml Normal file
View File

@ -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
src/api/__init__.py Normal file
View File

View File

29
src/api/routes/admin.py Normal file
View File

@ -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'
}

49
src/api/routes/commit.py Normal file
View File

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

View File

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

View File

@ -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}

45
src/api/routes/recall.py Normal file
View File

@ -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]}

38
src/api/server.py Normal file
View File

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