chore: establish Sovereign AI reference

This commit is contained in:
Hamza Ayed
2026-09-29 22:38:37 +03:00
commit ce1b22f100
147 changed files with 7394 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""SovereignAI Starter API package."""
+166
View File
@@ -0,0 +1,166 @@
"""SQLite storage for local users, conversations, and messages."""
from __future__ import annotations
import os
import sqlite3
from uuid import UUID
from pathlib import Path
from typing import Any
def _database_path() -> Path:
configured = os.getenv("SOVEREIGNAI_DATA_DIR")
if configured:
data_dir = Path(configured).expanduser()
else:
local_app_data = os.getenv("LOCALAPPDATA")
base_dir = Path(local_app_data) if local_app_data else Path.home() / ".local" / "share"
data_dir = base_dir / "SovereignAI" / "data"
data_dir.mkdir(parents=True, exist_ok=True)
return data_dir / "sovereign_ai.sqlite3"
DATABASE_PATH = _database_path()
LOCAL_USER_ID = str(
UUID(os.getenv("SOVEREIGNAI_LOCAL_USER_ID", "00000000-0000-4000-8000-000000000001"))
)
def _connect() -> sqlite3.Connection:
connection = sqlite3.connect(DATABASE_PATH, timeout=10)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys = ON")
return connection
def initialize_database() -> None:
with _connect() as connection:
connection.execute("PRAGMA journal_mode = WAL")
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS user_identities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider TEXT NOT NULL,
provider_subject TEXT NOT NULL,
email TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(provider, provider_subject)
);
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_conversations_user_updated
ON conversations(user_id, updated_at DESC);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK(role IN ('user', 'assistant')),
content TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_messages_conversation
ON messages(conversation_id, id);
"""
)
def ensure_user(user_id: str) -> None:
with _connect() as connection:
connection.execute("INSERT OR IGNORE INTO users(id) VALUES (?)", (user_id,))
def list_conversations(user_id: str) -> list[dict[str, Any]]:
with _connect() as connection:
rows = connection.execute(
"""
SELECT c.id, c.title, c.created_at, c.updated_at,
COUNT(m.id) AS message_count
FROM conversations AS c
LEFT JOIN messages AS m ON m.conversation_id = c.id
WHERE c.user_id = ?
GROUP BY c.id
ORDER BY c.updated_at DESC
""",
(user_id,),
).fetchall()
return [dict(row) for row in rows]
def get_conversation(user_id: str, conversation_id: str) -> dict[str, Any] | None:
with _connect() as connection:
conversation = connection.execute(
"""
SELECT id, title, created_at, updated_at
FROM conversations WHERE id = ? AND user_id = ?
""",
(conversation_id, user_id),
).fetchone()
if conversation is None:
return None
messages = connection.execute(
"SELECT role, content, created_at FROM messages "
"WHERE conversation_id = ? ORDER BY id",
(conversation_id,),
).fetchall()
return {**dict(conversation), "messages": [dict(row) for row in messages]}
def save_conversation(
user_id: str,
conversation_id: str,
title: str,
messages: list[dict[str, str]],
timestamp: str,
) -> None:
with _connect() as connection:
connection.execute("INSERT OR IGNORE INTO users(id) VALUES (?)", (user_id,))
owner = connection.execute(
"SELECT user_id FROM conversations WHERE id = ?", (conversation_id,)
).fetchone()
if owner is not None and owner["user_id"] != user_id:
raise PermissionError("Conversation does not belong to this user.")
connection.execute(
"""
INSERT INTO conversations(id, user_id, title, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET title = excluded.title,
updated_at = excluded.updated_at
""",
(conversation_id, user_id, title, timestamp, timestamp),
)
connection.execute("DELETE FROM messages WHERE conversation_id = ?", (conversation_id,))
connection.executemany(
"INSERT INTO messages(conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
[
(conversation_id, message["role"], message["content"], timestamp)
for message in messages
],
)
def delete_conversation(user_id: str, conversation_id: str) -> bool:
with _connect() as connection:
cursor = connection.execute(
"DELETE FROM conversations WHERE id = ? AND user_id = ?",
(conversation_id, user_id),
)
return cursor.rowcount > 0
initialize_database()
+349
View File
@@ -0,0 +1,349 @@
import json
import os
from datetime import datetime, timezone
from typing import Any
from uuid import UUID
import httpx
from fastapi import FastAPI, File, Form, Header, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from app import database
app = FastAPI(
title="SovereignAI Starter",
description="واجهة محلية تعليمية لمساعد ذكاء اصطناعي قابل للتوسع.",
version="0.1.0",
)
app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
allow_credentials=False,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "X-User-ID"],
)
class Message(BaseModel):
role: str = Field(description="system أو user أو assistant")
content: str
class ChatRequest(BaseModel):
model: str | None = Field(default=None, description="اسم النموذج المحلي؛ اتركه فارغًا لاستخدام النموذج الافتراضي")
messages: list[Message]
model_config = {
"json_schema_extra": {
"example": {
"model": "qwen2.5:1.5b-instruct-q4_K_M",
"messages": [{"role": "user", "content": "مرحبا، كيف حالك؟"}],
}
}
}
class AgentRequest(BaseModel):
task: str = Field(description="مهمة قصيرة للوكيل المحلي")
class StoredMessage(BaseModel):
role: str = Field(pattern="^(user|assistant)$")
content: str
class ConversationWrite(BaseModel):
title: str = Field(min_length=1, max_length=160)
messages: list[StoredMessage] = Field(min_length=1, max_length=2000)
def validate_user_id(value: str) -> str:
try:
return str(UUID(value))
except ValueError as exc:
raise HTTPException(status_code=400, detail="X-User-ID must be a UUID.") from exc
def validate_conversation_id(value: str) -> str:
try:
return str(UUID(value))
except ValueError as exc:
raise HTTPException(status_code=400, detail="Conversation ID must be a UUID.") from exc
async def get_completion(payload: dict[str, Any], base_url: str) -> dict[str, Any]:
try:
async with httpx.AsyncClient(timeout=180.0) as client:
response = await client.post(f"{base_url}/chat/completions", json=payload)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as exc:
detail = exc.response.text[:400] or "رفض خادم النموذج الطلب."
raise HTTPException(status_code=502, detail=f"خطأ من خادم النموذج المحلي: {detail}") from exc
except httpx.RequestError as exc:
raise HTTPException(status_code=503, detail="تعذر الاتصال بـ Ollama المحلي على العنوان المضبوط.") from exc
@app.get("/health")
def health() -> dict[str, str]:
return {
"status": "ok",
"model": os.getenv("LOCAL_MODEL", "qwen2.5:1.5b-instruct-q4_K_M"),
"backend": os.getenv("LOCAL_LLM_BASE_URL", "http://127.0.0.1:11434/v1"),
"groq_transcription": "configured" if os.getenv("GROQ_API_KEY") else "not_configured",
"conversation_database": "sqlite",
}
@app.get("/v1/local-user")
def get_local_user() -> dict[str, str]:
"""Return the single local development profile; authentication comes later."""
database.ensure_user(database.LOCAL_USER_ID)
return {"user_id": database.LOCAL_USER_ID, "mode": "local-development"}
def chat_payload(request: ChatRequest, *, stream: bool) -> dict[str, Any]:
model = request.model or os.getenv("LOCAL_MODEL", "qwen2.5:1.5b-instruct-q4_K_M")
return {
"model": model,
"messages": (
[message.model_dump() for message in request.messages]
if any(message.role == "system" for message in request.messages)
else [
{
"role": "system",
"content": (
"أنت مساعد ذكاء اصطناعي محلي يعمل عبر Ollama على جهاز المستخدم. "
"أجب بالعربية الواضحة وباختصار مناسب. إذا سُئلت أين أنت، أجب بهذه الصياغة: "
"أنا مساعد ذكاء اصطناعي يعمل على جهازك، ولا أملك وجودًا جسديًا أو موقع GPS. "
"ولا تدّع معرفة "
"موقع المستخدم أو حالة أي مكان. لا تدّع أنك زرت موقعًا أو اتصلت بالإنترنت "
"أو نفذت إجراءً ما لم يحدث ذلك فعلًا. إذا لم تعرف، قل ذلك بوضوح. "
"عند كتابة كود، ضعه في كتلة Markdown بثلاث علامات backtick "
"واكتب اسم اللغة بعد علامات البداية، مثل python أو dart."
),
},
*[message.model_dump() for message in request.messages],
]
),
"stream": stream,
}
@app.post("/v1/chat/completions")
async def chat(request: ChatRequest) -> dict[str, Any]:
"""ترحيل طلب المحادثة إلى النموذج المحلي."""
base_url = os.getenv("LOCAL_LLM_BASE_URL", "http://127.0.0.1:11434/v1").rstrip("/")
payload = chat_payload(request, stream=False)
return await get_completion(payload, base_url)
@app.post("/v1/chat/stream")
async def chat_stream(request: ChatRequest) -> StreamingResponse:
"""Pass Ollama token deltas to clients as newline-delimited JSON."""
base_url = os.getenv("LOCAL_LLM_BASE_URL", "http://127.0.0.1:11434/v1").rstrip("/")
payload = chat_payload(request, stream=True)
async def events():
try:
timeout = httpx.Timeout(connect=15.0, read=None, write=30.0, pool=30.0)
async with httpx.AsyncClient(timeout=timeout) as client:
async with client.stream(
"POST", f"{base_url}/chat/completions", json=payload
) as response:
if response.status_code >= 400:
detail = (await response.aread()).decode("utf-8", "replace")[:400]
yield json.dumps({"error": f"Ollama {response.status_code}: {detail}"}) + "\n"
return
async for line in response.aiter_lines():
if not line.startswith("data:"):
continue
raw = line[5:].strip()
if raw == "[DONE]":
yield '{"done":true}\n'
return
try:
event = json.loads(raw)
delta = event["choices"][0].get("delta", {}).get("content")
except (ValueError, KeyError, IndexError, TypeError):
continue
if delta:
yield json.dumps({"delta": delta}, ensure_ascii=False) + "\n"
yield '{"done":true}\n'
except httpx.RequestError:
yield json.dumps({"error": "تعذر الاتصال بـ Ollama المحلي."}, ensure_ascii=False) + "\n"
return StreamingResponse(
events(),
media_type="application/x-ndjson",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
@app.get("/v1/conversations")
def list_user_conversations(
x_user_id: str = Header(alias="X-User-ID"),
) -> list[dict[str, Any]]:
user_id = validate_user_id(x_user_id)
database.ensure_user(user_id)
return database.list_conversations(user_id)
@app.get("/v1/conversations/{conversation_id}")
def read_user_conversation(
conversation_id: str,
x_user_id: str = Header(alias="X-User-ID"),
) -> dict[str, Any]:
user_id = validate_user_id(x_user_id)
result = database.get_conversation(
user_id, validate_conversation_id(conversation_id)
)
if result is None:
raise HTTPException(status_code=404, detail="Conversation not found.")
return result
@app.put("/v1/conversations/{conversation_id}")
def write_user_conversation(
conversation_id: str,
request: ConversationWrite,
x_user_id: str = Header(alias="X-User-ID"),
) -> dict[str, str]:
user_id = validate_user_id(x_user_id)
timestamp = datetime.now(timezone.utc).isoformat()
try:
database.save_conversation(
user_id,
validate_conversation_id(conversation_id),
request.title.strip() or "محادثة جديدة",
[message.model_dump() for message in request.messages],
timestamp,
)
except PermissionError as exc:
raise HTTPException(status_code=404, detail="Conversation not found.") from exc
return {"status": "saved", "id": conversation_id}
@app.delete("/v1/conversations/{conversation_id}")
def remove_user_conversation(
conversation_id: str,
x_user_id: str = Header(alias="X-User-ID"),
) -> dict[str, str]:
user_id = validate_user_id(x_user_id)
deleted = database.delete_conversation(
user_id, validate_conversation_id(conversation_id)
)
if not deleted:
raise HTTPException(status_code=404, detail="Conversation not found.")
return {"status": "deleted", "id": conversation_id}
def safe_arithmetic(expression: str) -> float:
"""حساب تعبيرات رقمية بسيطة دون eval أو تنفيذ تعليمات عامة."""
allowed = set("0123456789+-*/(). %")
if not expression or any(char not in allowed for char in expression):
raise ValueError("مسموح بالأرقام والعمليات الحسابية الأساسية فقط.")
# Parser محدود يدعم الأرقام والأقواس والعمليات الأساسية فقط.
import ast
import operator
operations = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Mod: operator.mod,
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
def evaluate(node: ast.AST) -> float:
if isinstance(node, ast.Expression):
return evaluate(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return float(node.value)
if isinstance(node, ast.BinOp) and type(node.op) in operations:
return operations[type(node.op)](evaluate(node.left), evaluate(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in operations:
return operations[type(node.op)](evaluate(node.operand))
raise ValueError("التعبير غير مدعوم.")
return evaluate(ast.parse(expression, mode="eval"))
@app.post("/v1/agent/run")
async def run_agent(request: AgentRequest) -> dict[str, Any]:
"""وكيل صغير: يحسب التعبير الرياضي محليًا، ويرسل المهام النصية للنموذج."""
try:
result = safe_arithmetic(request.task)
return {"task": request.task, "tool": "calculator", "result": result}
except (ValueError, SyntaxError, ZeroDivisionError):
pass
base_url = os.getenv("LOCAL_LLM_BASE_URL", "http://127.0.0.1:11434/v1").rstrip("/")
model = os.getenv("LOCAL_MODEL", "qwen2.5:1.5b-instruct-q4_K_M")
payload = {
"model": model,
"messages": [
{"role": "system", "content": "أنت وكيل مساعد محلي. أجب بالعربية وباختصار شديد. إذا كانت المهمة حسابًا فاستعمل الآلة الحاسبة."},
{"role": "user", "content": request.task},
],
"stream": False,
}
completion = await get_completion(payload, base_url)
return {
"task": request.task,
"tool": "local-llm",
"model": model,
"result": completion["choices"][0]["message"]["content"],
}
@app.post("/v1/audio/transcriptions")
async def transcribe_audio(
file: UploadFile = File(...),
language: str | None = Form(default=None),
prompt: str | None = Form(default=None),
) -> dict[str, Any]:
"""Proxy microphone audio to Groq without exposing its key to the client."""
api_key = os.getenv("GROQ_API_KEY")
if not api_key:
raise HTTPException(status_code=503, detail="GROQ_API_KEY is not set in the server environment.")
audio = await file.read(25 * 1024 * 1024 + 1)
if not audio:
raise HTTPException(status_code=400, detail="Audio file is empty.")
if len(audio) > 25 * 1024 * 1024:
raise HTTPException(status_code=413, detail="Audio exceeds the 25 MB upload limit.")
form = {
"model": "whisper-large-v3-turbo",
"temperature": "0",
"response_format": "verbose_json",
}
if language:
form["language"] = language
if prompt:
form["prompt"] = prompt
files = {
"file": (file.filename or "recording.wav", audio, file.content_type or "audio/wav"),
}
try:
async with httpx.AsyncClient(timeout=180.0) as client:
response = await client.post(
"https://api.groq.com/openai/v1/audio/transcriptions",
headers={"Authorization": f"Bearer {api_key}"},
data=form,
files=files,
)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as exc:
raise HTTPException(
status_code=502,
detail=f"Groq transcription failed ({exc.response.status_code}): {exc.response.text[:400]}",
) from exc
except httpx.RequestError as exc:
raise HTTPException(status_code=502, detail="Could not reach Groq transcription service.") from exc