Generalize from health-specific to universal personal assistant

The app manages multiple life areas (health, finance, personal, work),
not just health. Updated all health-specific language throughout:

Backend:
- Default system prompt: general personal assistant (not health-only)
- AI tool descriptions: generic (not health records/medications)
- Memory categories: health, finance, personal, work, document_summary, other
  (replaces condition, medication, allergy, vital)
- PDF template: "Prepared for" (not "Patient"), "Key Information" (not "Health Profile")
- Renamed generate_health_pdf -> generate_pdf_report, health_report.html -> report.html
- Renamed run_daily_health_review -> run_daily_review
- Context assembly: "User Profile" (not "Health Profile")
- OpenAPI: generic descriptions

Frontend:
- Dashboard subtitle: "Your personal AI assistant"
- Memory categories: Health, Finance, Personal, Work
- Document types: Report, Contract, Receipt, Certificate (not lab_result, etc.)
- Updated en + ru translations throughout

Documentation:
- README: general personal assistant description
- Removed health-only feature descriptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 15:15:39 +03:00
parent 03dc42e74a
commit b0790d719c
14 changed files with 63 additions and 64 deletions

View File

@@ -1,14 +1,14 @@
# Personal AI Assistant # Personal AI Assistant
A client-server web application for managing personal health and life areas with AI-powered assistance. Upload documents, chat with AI specialists, receive proactive health reminders, and track critical information across multiple life domains. A client-server web application for managing different areas of personal life with AI-powered assistance. Upload documents, chat with AI specialists, receive proactive reminders, and track critical information across multiple life domains (health, finance, personal, work, etc.).
## Key Features ## Key Features
- **AI Chat with Specialists** — create chats using configurable skills (e.g., cardiologist, nutritionist). Each skill shapes the AI's behavior as a domain expert. - **AI Chat with Specialists** — create chats using configurable skills (e.g., doctor, financial advisor, personal coach). Each skill shapes the AI's behavior as a domain expert.
- **Document Management** — upload health records, lab results, prescriptions, and consultation notes. AI extracts and indexes content for intelligent retrieval. - **Document Management** — upload documents (records, reports, contracts, notes, etc.). AI extracts and indexes content for intelligent retrieval.
- **Proactive Notifications** — AI analyzes your health profile and schedules reminders (checkups, medication reviews) via in-app, email, or Telegram. - **Proactive Notifications** — AI analyzes your stored data and schedules reminders (deadlines, follow-ups, recurring events) via in-app, email, or Telegram.
- **PDF Compilation** — request AI-generated health summaries as downloadable PDF documents. - **PDF Compilation** — request AI-generated summaries and reports as downloadable PDF documents.
- **Global Memory** — AI maintains a shared memory of critical health information across all your chats. - **Global Memory** — AI maintains a shared memory of critical information across all your chats.
- **Multi-language** — English and Russian support. - **Multi-language** — English and Russian support.
## Tech Stack ## Tech Stack

View File

@@ -21,7 +21,7 @@ async def compile_pdf(
user: Annotated[User, Depends(get_current_user)], user: Annotated[User, Depends(get_current_user)],
db: Annotated[AsyncSession, Depends(get_db)], db: Annotated[AsyncSession, Depends(get_db)],
): ):
pdf = await pdf_service.generate_health_pdf( pdf = await pdf_service.generate_pdf_report(
db, user.id, data.title, data.document_ids or None, data.chat_id, db, user.id, data.title, data.document_ids or None, data.chat_id,
) )
return PdfResponse.model_validate(pdf) return PdfResponse.model_validate(pdf)

View File

@@ -32,7 +32,7 @@ async def lifespan(app: FastAPI):
def create_app() -> FastAPI: def create_app() -> FastAPI:
app = FastAPI( app = FastAPI(
title="AI Assistant API", title="AI Assistant API",
description="Personal AI health assistant with document management, chat, and notifications.", description="Personal AI assistant with document management, chat, memory, and notifications.",
version="0.1.0", version="0.1.0",
lifespan=lifespan, lifespan=lifespan,
docs_url="/api/docs" if settings.DOCS_ENABLED else None, docs_url="/api/docs" if settings.DOCS_ENABLED else None,
@@ -41,8 +41,8 @@ def create_app() -> FastAPI:
openapi_tags=[ openapi_tags=[
{"name": "auth", "description": "Authentication and registration"}, {"name": "auth", "description": "Authentication and registration"},
{"name": "chats", "description": "AI chat conversations"}, {"name": "chats", "description": "AI chat conversations"},
{"name": "documents", "description": "Health document management"}, {"name": "documents", "description": "Document management"},
{"name": "memory", "description": "Health memory entries"}, {"name": "memory", "description": "Memory entries"},
{"name": "skills", "description": "AI specialist skills"}, {"name": "skills", "description": "AI specialist skills"},
{"name": "notifications", "description": "User notifications"}, {"name": "notifications", "description": "User notifications"},
{"name": "pdf", "description": "PDF report generation"}, {"name": "pdf", "description": "PDF report generation"},

View File

@@ -4,7 +4,7 @@ from typing import Literal
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
CategoryType = Literal["condition", "medication", "allergy", "vital", "document_summary", "other"] CategoryType = Literal["health", "finance", "personal", "work", "document_summary", "other"]
ImportanceType = Literal["critical", "high", "medium", "low"] ImportanceType = Literal["critical", "high", "medium", "low"]

View File

@@ -24,13 +24,13 @@ client = AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
AI_TOOLS = [ AI_TOOLS = [
{ {
"name": "save_memory", "name": "save_memory",
"description": "Save important health information to the user's memory. Use this when the user shares critical health data like conditions, medications, allergies, or important health facts.", "description": "Save important information to the user's memory. Use this when the user shares critical personal data, facts, preferences, or key details they want to remember across conversations.",
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"category": { "category": {
"type": "string", "type": "string",
"enum": ["condition", "medication", "allergy", "vital", "document_summary", "other"], "enum": ["health", "finance", "personal", "work", "document_summary", "other"],
"description": "Category of the memory entry", "description": "Category of the memory entry",
}, },
"title": {"type": "string", "description": "Short title for the memory entry"}, "title": {"type": "string", "description": "Short title for the memory entry"},
@@ -46,7 +46,7 @@ AI_TOOLS = [
}, },
{ {
"name": "search_documents", "name": "search_documents",
"description": "Search the user's uploaded health documents for relevant information. Use this when you need to find specific health records, lab results, or consultation notes.", "description": "Search the user's uploaded documents for relevant information. Use this when you need to find specific records, files, or notes the user has uploaded.",
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -57,13 +57,13 @@ AI_TOOLS = [
}, },
{ {
"name": "get_memory", "name": "get_memory",
"description": "Retrieve the user's stored health memories filtered by category. Use this to recall previously saved health information.", "description": "Retrieve the user's stored memories filtered by category. Use this to recall previously saved information.",
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"category": { "category": {
"type": "string", "type": "string",
"enum": ["condition", "medication", "allergy", "vital", "document_summary", "other"], "enum": ["health", "finance", "personal", "work", "document_summary", "other"],
"description": "Optional category filter. Omit to get all memories.", "description": "Optional category filter. Omit to get all memories.",
}, },
}, },
@@ -94,7 +94,7 @@ AI_TOOLS = [
}, },
{ {
"name": "generate_pdf", "name": "generate_pdf",
"description": "Generate a PDF health report compilation from the user's data. Use this when the user asks for a document or summary of their health information.", "description": "Generate a PDF report compilation from the user's data. Use this when the user asks for a document or summary of their stored information.",
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -173,8 +173,8 @@ async def _execute_tool(
}) })
elif tool_name == "generate_pdf": elif tool_name == "generate_pdf":
from app.services.pdf_service import generate_health_pdf from app.services.pdf_service import generate_pdf_report
pdf = await generate_health_pdf(db, user_id, title=tool_input["title"]) pdf = await generate_pdf_report(db, user_id, title=tool_input["title"])
await db.commit() await db.commit()
return json.dumps({ return json.dumps({
"status": "generated", "status": "generated",
@@ -213,7 +213,7 @@ async def assemble_context(
memories = await get_critical_memories(db, user_id) memories = await get_critical_memories(db, user_id)
if memories: if memories:
memory_lines = [f"- [{m.category}] {m.title}: {m.content}" for m in memories] memory_lines = [f"- [{m.category}] {m.title}: {m.content}" for m in memories]
system_parts.append(f"---\nUser Health Profile:\n" + "\n".join(memory_lines)) system_parts.append(f"---\nUser Profile (Key Information):\n" + "\n".join(memory_lines))
# 5. Relevant document excerpts (based on user message keywords) # 5. Relevant document excerpts (based on user message keywords)
if user_message.strip(): if user_message.strip():

View File

@@ -5,14 +5,14 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.models.context_file import ContextFile from app.models.context_file import ContextFile
DEFAULT_SYSTEM_PROMPT = """You are a personal AI health assistant. Your role is to: DEFAULT_SYSTEM_PROMPT = """You are a personal AI assistant helping users manage different areas of their life. Your role is to:
- Help users understand their health data and medical documents - Help users organize and understand their uploaded documents and data
- Provide health-related recommendations based on uploaded information - Provide recommendations and insights based on stored information
- Schedule reminders for checkups, medications, and health-related activities - Schedule reminders for important events, deadlines, and recurring activities
- Compile health summaries when requested - Compile summaries and reports when requested
- Answer health questions clearly and compassionately - Answer questions clearly and helpfully
Always be empathetic, accurate, and clear. When uncertain, recommend consulting a healthcare professional. Always be empathetic, accurate, and clear. When uncertain about specialized topics, recommend consulting a relevant professional.
You can communicate in English and Russian based on the user's preference.""" You can communicate in English and Russian based on the user's preference."""

View File

@@ -19,7 +19,7 @@ jinja_env = Environment(
) )
async def generate_health_pdf( async def generate_pdf_report(
db: AsyncSession, db: AsyncSession,
user_id: uuid.UUID, user_id: uuid.UUID,
title: str, title: str,
@@ -53,7 +53,7 @@ async def generate_health_pdf(
}) })
# Render HTML # Render HTML
template = jinja_env.get_template("health_report.html") template = jinja_env.get_template("report.html")
html = template.render( html = template.render(
title=title, title=title,
user_name=user.full_name or user.username, user_name=user.full_name or user.username,

View File

@@ -16,11 +16,11 @@ def start_scheduler():
replace_existing=True, replace_existing=True,
) )
from app.workers.health_review import run_daily_health_review from app.workers.health_review import run_daily_review
scheduler.add_job( scheduler.add_job(
run_daily_health_review, run_daily_review,
trigger=CronTrigger(hour=8, minute=0), trigger=CronTrigger(hour=8, minute=0),
id="daily_health_review", id="daily_review",
replace_existing=True, replace_existing=True,
) )

View File

@@ -26,13 +26,13 @@
<body> <body>
<div class="header"> <div class="header">
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
<p>Patient: {{ user_name }}</p> <p>Prepared for: {{ user_name }}</p>
<p>Generated: {{ generated_at }}</p> <p>Generated: {{ generated_at }}</p>
</div> </div>
{% if memories %} {% if memories %}
<div class="section"> <div class="section">
<h2>Health Profile</h2> <h2>Key Information</h2>
{% for m in memories %} {% for m in memories %}
<div class="memory-entry"> <div class="memory-entry">
<span class="category">{{ m.category }}</span> <span class="category">{{ m.category }}</span>

View File

@@ -1,4 +1,4 @@
"""Daily job: proactive health review for all users with health data.""" """Daily job: proactive review for all users with stored data."""
import asyncio import asyncio
import json import json
import logging import logging
@@ -17,8 +17,8 @@ logger = logging.getLogger(__name__)
client = AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY) client = AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
async def run_daily_health_review(): async def run_daily_review():
"""Review each user's health profile and generate reminder notifications.""" """Review each user's profile and generate reminder notifications."""
if not settings.ANTHROPIC_API_KEY: if not settings.ANTHROPIC_API_KEY:
return return
@@ -31,7 +31,7 @@ async def run_daily_health_review():
await _review_user(user) await _review_user(user)
await asyncio.sleep(1) # Rate limit await asyncio.sleep(1) # Rate limit
except Exception: except Exception:
logger.exception(f"Health review failed for user {user.id}") logger.exception(f"Daily review failed for user {user.id}")
async def _review_user(user: User): async def _review_user(user: User):
@@ -45,13 +45,12 @@ async def _review_user(user: User):
response = await client.messages.create( response = await client.messages.create(
model=settings.CLAUDE_MODEL, model=settings.CLAUDE_MODEL,
max_tokens=500, max_tokens=500,
system="You are a health assistant. Based on the user's health profile, suggest any upcoming checkups, medication reviews, or health actions that should be reminded. Respond with a JSON array of objects with 'title' and 'body' fields. If no reminders are needed, return an empty array [].", system="You are a personal assistant. Based on the user's stored information, suggest any upcoming actions, deadlines, follow-ups, or reminders that would be helpful. Respond with a JSON array of objects with 'title' and 'body' fields. If no reminders are needed, return an empty array [].",
messages=[{"role": "user", "content": f"User health profile:\n{memory_text}"}], messages=[{"role": "user", "content": f"User profile data:\n{memory_text}"}],
) )
try: try:
text = response.content[0].text.strip() text = response.content[0].text.strip()
# Extract JSON from response
if "[" in text: if "[" in text:
json_str = text[text.index("["):text.rindex("]") + 1] json_str = text[text.index("["):text.rindex("]") + 1]
reminders = json.loads(json_str) reminders = json.loads(json_str)
@@ -60,7 +59,7 @@ async def _review_user(user: User):
except (json.JSONDecodeError, ValueError): except (json.JSONDecodeError, ValueError):
return return
for reminder in reminders[:5]: # Max 5 reminders per user per day for reminder in reminders[:5]:
if "title" in reminder and "body" in reminder: if "title" in reminder and "body" in reminder:
notif = await create_notification( notif = await create_notification(
db, db,

View File

@@ -42,7 +42,7 @@
}, },
"dashboard": { "dashboard": {
"welcome": "Welcome, {{name}}", "welcome": "Welcome, {{name}}",
"subtitle": "Your personal AI health assistant" "subtitle": "Your personal AI assistant"
}, },
"chat": { "chat": {
"new_chat": "New Chat", "new_chat": "New Chat",
@@ -116,10 +116,10 @@
"clear_search": "Clear", "clear_search": "Clear",
"types": { "types": {
"other": "Other", "other": "Other",
"lab_result": "Lab Result", "report": "Report",
"consultation": "Consultation", "contract": "Contract",
"prescription": "Prescription", "receipt": "Receipt",
"imaging": "Imaging" "certificate": "Certificate"
}, },
"status": { "status": {
"pending": "Pending", "pending": "Pending",
@@ -131,7 +131,7 @@
"memory": { "memory": {
"create": "Add Memory Entry", "create": "Add Memory Entry",
"edit": "Edit Memory Entry", "edit": "Edit Memory Entry",
"no_entries": "No memory entries yet. The AI will save important health information here.", "no_entries": "No memory entries yet. The AI will save important information here.",
"category": "Category", "category": "Category",
"importance": "Importance", "importance": "Importance",
"title_field": "Title", "title_field": "Title",
@@ -139,10 +139,10 @@
"content_field": "Content", "content_field": "Content",
"content_placeholder": "Detailed information...", "content_placeholder": "Detailed information...",
"categories": { "categories": {
"condition": "Condition", "health": "Health",
"medication": "Medication", "finance": "Finance",
"allergy": "Allergy", "personal": "Personal",
"vital": "Vital Sign", "work": "Work",
"document_summary": "Document Summary", "document_summary": "Document Summary",
"other": "Other" "other": "Other"
}, },

View File

@@ -42,7 +42,7 @@
}, },
"dashboard": { "dashboard": {
"welcome": "Добро пожаловать, {{name}}", "welcome": "Добро пожаловать, {{name}}",
"subtitle": "Ваш персональный ИИ-ассистент по здоровью" "subtitle": "Ваш персональный ИИ-ассистент"
}, },
"chat": { "chat": {
"new_chat": "Новый чат", "new_chat": "Новый чат",
@@ -116,10 +116,10 @@
"clear_search": "Очистить", "clear_search": "Очистить",
"types": { "types": {
"other": "Другое", "other": "Другое",
"lab_result": "Анализы", "report": "Отчёт",
"consultation": "Консультация", "contract": "Договор",
"prescription": "Рецепт", "receipt": "Квитанция",
"imaging": "Снимки" "certificate": "Сертификат"
}, },
"status": { "status": {
"pending": "Ожидание", "pending": "Ожидание",
@@ -131,7 +131,7 @@
"memory": { "memory": {
"create": "Добавить запись", "create": "Добавить запись",
"edit": "Редактировать запись", "edit": "Редактировать запись",
"no_entries": "Записей пока нет. ИИ будет сохранять важную информацию о здоровье здесь.", "no_entries": "Записей пока нет. ИИ будет сохранять важную информацию здесь.",
"category": "Категория", "category": "Категория",
"importance": "Важность", "importance": "Важность",
"title_field": "Заголовок", "title_field": "Заголовок",
@@ -139,10 +139,10 @@
"content_field": "Содержание", "content_field": "Содержание",
"content_placeholder": "Подробная информация...", "content_placeholder": "Подробная информация...",
"categories": { "categories": {
"condition": "Заболевание", "health": "Здоровье",
"medication": "Лекарство", "finance": "Финансы",
"allergy": "Аллергия", "personal": "Личное",
"vital": "Показатели", "work": "Работа",
"document_summary": "Сводка документа", "document_summary": "Сводка документа",
"other": "Другое" "other": "Другое"
}, },

View File

@@ -9,7 +9,7 @@ interface UploadDialogProps {
onClose: () => void; onClose: () => void;
} }
const DOC_TYPES = ["other", "lab_result", "consultation", "prescription", "imaging"]; const DOC_TYPES = ["other", "report", "contract", "receipt", "certificate"];
export function UploadDialog({ open, onClose }: UploadDialogProps) { export function UploadDialog({ open, onClose }: UploadDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { MemoryEntry } from "@/api/memory"; import type { MemoryEntry } from "@/api/memory";
const CATEGORIES = ["condition", "medication", "allergy", "vital", "document_summary", "other"]; const CATEGORIES = ["health", "finance", "personal", "work", "document_summary", "other"];
const IMPORTANCE_LEVELS = ["critical", "high", "medium", "low"]; const IMPORTANCE_LEVELS = ["critical", "high", "medium", "low"];
interface MemoryEditorProps { interface MemoryEditorProps {