Files
microdao-daarion/gateway-bot/memory_client.py
Apple 4601c6fca8 feat: add Vision Encoder service + Vision RAG implementation
- Vision Encoder Service (OpenCLIP ViT-L/14, GPU-accelerated)
  - FastAPI app with text/image embedding endpoints (768-dim)
  - Docker support with NVIDIA GPU runtime
  - Port 8001, health checks, model info API

- Qdrant Vector Database integration
  - Port 6333/6334 (HTTP/gRPC)
  - Image embeddings storage (768-dim, Cosine distance)
  - Auto collection creation

- Vision RAG implementation
  - VisionEncoderClient (Python client for API)
  - Image Search module (text-to-image, image-to-image)
  - Vision RAG routing in DAGI Router (mode: image_search)
  - VisionEncoderProvider integration

- Documentation (5000+ lines)
  - SYSTEM-INVENTORY.md - Complete system inventory
  - VISION-ENCODER-STATUS.md - Service status
  - VISION-RAG-IMPLEMENTATION.md - Implementation details
  - vision_encoder_deployment_task.md - Deployment checklist
  - services/vision-encoder/README.md - Deployment guide
  - Updated WARP.md, INFRASTRUCTURE.md, Jupyter Notebook

- Testing
  - test-vision-encoder.sh - Smoke tests (6 tests)
  - Unit tests for client, image search, routing

- Services: 17 total (added Vision Encoder + Qdrant)
- AI Models: 3 (qwen3:8b, OpenCLIP ViT-L/14, BAAI/bge-m3)
- GPU Services: 2 (Vision Encoder, Ollama)
- VRAM Usage: ~10 GB (concurrent)

Status: Production Ready 
2025-11-17 05:24:36 -08:00

251 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Memory Service Client для Gateway
Використовується для отримання та збереження пам'яті діалогів
"""
import os
import logging
from typing import Optional, Dict, Any, List
from datetime import datetime
import httpx
logger = logging.getLogger(__name__)
MEMORY_SERVICE_URL = os.getenv("MEMORY_SERVICE_URL", "http://memory-service:8000")
class MemoryClient:
"""Клієнт для роботи з Memory Service"""
def __init__(self, base_url: str = MEMORY_SERVICE_URL):
self.base_url = base_url.rstrip("/")
self.timeout = 10.0
async def get_context(
self,
user_id: str,
agent_id: str,
team_id: str,
channel_id: Optional[str] = None,
limit: int = 10
) -> Dict[str, Any]:
"""
Отримати контекст пам'яті для діалогу
Повертає:
{
"facts": [...], # user_facts
"recent_events": [...], # останні agent_memory_events
"dialog_summaries": [...] # підсумки діалогів
}
"""
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
# Отримуємо user facts
facts_response = await client.get(
f"{self.base_url}/facts",
params={"user_id": user_id, "team_id": team_id, "limit": limit},
headers={"Authorization": f"Bearer {user_id}"} # Заглушка
)
facts = facts_response.json() if facts_response.status_code == 200 else []
# Отримуємо останні memory events
events_response = await client.get(
f"{self.base_url}/agents/{agent_id}/memory",
params={
"team_id": team_id,
"channel_id": channel_id,
"scope": "short_term",
"kind": "message",
"limit": limit
},
headers={"Authorization": f"Bearer {user_id}"}
)
events = events_response.json().get("items", []) if events_response.status_code == 200 else []
# Отримуємо dialog summaries
summaries_response = await client.get(
f"{self.base_url}/summaries",
params={
"team_id": team_id,
"channel_id": channel_id,
"agent_id": agent_id,
"limit": 5
},
headers={"Authorization": f"Bearer {user_id}"}
)
summaries = summaries_response.json().get("items", []) if summaries_response.status_code == 200 else []
return {
"facts": facts,
"recent_events": events,
"dialog_summaries": summaries
}
except Exception as e:
logger.warning(f"Memory context fetch failed: {e}")
return {
"facts": [],
"recent_events": [],
"dialog_summaries": []
}
async def save_chat_turn(
self,
agent_id: str,
team_id: str,
user_id: str,
message: str,
response: str,
channel_id: Optional[str] = None,
scope: str = "short_term"
) -> bool:
"""
Зберегти один turn діалогу (повідомлення + відповідь)
"""
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
# Зберігаємо повідомлення користувача
user_event = {
"agent_id": agent_id,
"team_id": team_id,
"channel_id": channel_id,
"user_id": user_id,
"scope": scope,
"kind": "message",
"body_text": message,
"body_json": {"type": "user_message", "source": "telegram"}
}
await client.post(
f"{self.base_url}/agents/{agent_id}/memory",
json=user_event,
headers={"Authorization": f"Bearer {user_id}"}
)
# Зберігаємо відповідь агента
agent_event = {
"agent_id": agent_id,
"team_id": team_id,
"channel_id": channel_id,
"user_id": user_id,
"scope": scope,
"kind": "message",
"body_text": response,
"body_json": {"type": "agent_response", "source": "telegram"}
}
await client.post(
f"{self.base_url}/agents/{agent_id}/memory",
json=agent_event,
headers={"Authorization": f"Bearer {user_id}"}
)
return True
except Exception as e:
logger.warning(f"Failed to save chat turn: {e}")
return False
async def create_dialog_summary(
self,
team_id: str,
channel_id: Optional[str],
agent_id: str,
user_id: Optional[str],
period_start: datetime,
period_end: datetime,
summary_text: str,
message_count: int = 0,
participant_count: int = 0,
topics: Optional[List[str]] = None,
summary_json: Optional[Dict[str, Any]] = None
) -> bool:
"""
Створити підсумок діалогу для масштабування без переповнення контексту
"""
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
f"{self.base_url}/summaries",
json={
"team_id": team_id,
"channel_id": channel_id,
"agent_id": agent_id,
"user_id": user_id,
"period_start": period_start.isoformat(),
"period_end": period_end.isoformat(),
"summary_text": summary_text,
"summary_json": summary_json,
"message_count": message_count,
"participant_count": participant_count,
"topics": topics or [],
"meta": {}
},
headers={"Authorization": f"Bearer {user_id or 'system'}"}
)
return response.status_code in [200, 201]
except Exception as e:
logger.warning(f"Failed to create dialog summary: {e}")
return False
async def upsert_fact(
self,
user_id: str,
fact_key: str,
fact_value: Optional[str] = None,
fact_value_json: Optional[Dict[str, Any]] = None,
team_id: Optional[str] = None
) -> bool:
"""
Створити або оновити факт користувача
"""
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
f"{self.base_url}/facts/upsert",
json={
"user_id": user_id,
"fact_key": fact_key,
"fact_value": fact_value,
"fact_value_json": fact_value_json,
"team_id": team_id
},
headers={"Authorization": f"Bearer {user_id}"}
)
return response.status_code in [200, 201]
except Exception as e:
logger.warning(f"Failed to upsert fact: {e}")
return False
async def get_fact(
self,
user_id: str,
fact_key: str,
team_id: Optional[str] = None
) -> Optional[Dict[str, Any]]:
"""
Отримати факт користувача
Returns:
Fact dict with fact_value and fact_value_json, or None if not found
"""
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.get(
f"{self.base_url}/facts/{fact_key}",
params={
"user_id": user_id,
"team_id": team_id
},
headers={"Authorization": f"Bearer {user_id}"}
)
if response.status_code == 200:
return response.json()
return None
except Exception as e:
logger.warning(f"Failed to get fact: {e}")
return None
# Глобальний екземпляр клієнта
memory_client = MemoryClient()