First Version

This commit is contained in:
aliakbar
2026-06-27 07:52:17 +03:30
commit f3d28cea99
97 changed files with 54562 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
g++ \
build-essential \
curl \
wget \
git \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir wheel setuptools && \
pip install --no-cache-dir -r requirements.txt && \
pip install fastapi==0.111.0
pip install torch==2.1.0 --index-url https://download.pytorch.org/whl/cpu
# Copy application code
COPY . .
# Create necessary directories
RUN mkdir -p /app/faiss_index /root/.cache/huggingface
# Expose port
EXPOSE 8000
# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
+371
View File
@@ -0,0 +1,371 @@
from typing_extensions import TypedDict, List
import os
import time
import uuid
from pydantic import BaseModel, Field
from langgraph.store.memory import InMemoryStore
from langmem import create_memory_store_manager
from langchain_openai import ChatOpenAI
import json
from pathlib import Path
import asyncio
import threading
from concurrent.futures import ThreadPoolExecutor
from functools import lru_cache
import pickle
from dataclasses import dataclass
from queue import Queue
import logging
from rag.rag import RAG
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class Episode(BaseModel):
observation: str = Field(..., description="The context and setup - what happened")
thoughts: str = Field(
...,
description="Internal reasoning process and observations of the agent in the episode that let it arrive"
' at the correct action and result. "I ..."',
)
action: str = Field(
...,
description="What was done, how, and in what format. (Include whatever is salient to the success of the action). I ..",
)
result: str = Field(
...,
description="Outcome and retrospective. What did you do well? What could you do better next time? I ...",
)
class StateAgent(TypedDict):
query: List[List[str]]
top_k: int
need_correction: bool
is_correct: str
class AsyncBackgroundProcessor:
"""Background processor for async operations"""
def __init__(self, max_workers=2):
self.executor = ThreadPoolExecutor(max_workers=max_workers)
self.store_queue = Queue()
self.running = True
self.worker_thread = threading.Thread(target=self._process_queue, daemon=True)
self.worker_thread.start()
def _process_queue(self):
"""Process store operations in background"""
while self.running:
try:
if not self.store_queue.empty():
func, args, kwargs = self.store_queue.get(timeout=0.1)
try:
func(*args, **kwargs)
except Exception as e:
logger.error(f"Error processing background task: {e}")
else:
time.sleep(0.01)
except Exception as e:
logger.error(f"Queue processing error: {e}")
def submit(self, func, *args, **kwargs):
"""Submit task to background queue"""
self.store_queue.put((func, args, kwargs))
def shutdown(self):
"""Shutdown background processor"""
self.running = False
self.executor.shutdown(wait=True)
if self.worker_thread.is_alive():
self.worker_thread.join(timeout=5)
class Agent:
def __init__(self, Config):
if not Config.API_KEY:
raise RuntimeError("Missing API_KEY. Set it in your .env or environment as OPENAI_API_KEY/API_KEY.")
os.environ.setdefault("OPENAI_API_KEY", Config.API_KEY)
os.environ.setdefault("OPENAI_BASE_URL", "https://api.avalai.ir/v1")
# Initialize components
self.rag = RAG(
Config.MODEL_NAME,
Config.RETREIVER_NAME,
Config.VECTOR_PATH,
Config.DATA_PATH,
Config.DEVICE
)
self.memory_llm = self.rag.generator
# Load store from file first to avoid duplicate loading
self.dump_file = Config.DUMP_FILE if hasattr(Config, 'DUMP_FILE') else "episodes_dump.json"
self.episode_cache = {} # Cache for episodes
self.search_cache = {} # Cache for search results
# Initialize store with optimized settings
self.store = InMemoryStore(
index={
"dims": 1536,
"embed": "openai:text-embedding-3-small",
"metric": "cosine", # Use cosine similarity
}
)
# Background processor for async operations
self.bg_processor = AsyncBackgroundProcessor(max_workers=2)
# Try to load store from file
if Path(self.dump_file).exists():
try:
self._fast_load_store(filepath=self.dump_file)
logger.info(f"Loaded {len(self.episode_cache)} episodes from cache")
except Exception as e:
logger.warning(f"Could not load previous content: {e}")
# Initialize chat with optimized settings
chat = ChatOpenAI(
model=self.memory_llm.model_name,
openai_api_key=os.environ["OPENAI_API_KEY"],
base_url=os.environ.get("OPENAI_BASE_URL", "https://api.avalai.ir/v1"),
temperature=0.2,
max_tokens=512,
timeout=30, # Add timeout
max_retries=2, # Limit retries
)
# Initialize memory manager
self.manager = create_memory_store_manager(
chat,
namespace=("memories", "episodes"),
schemas=[Episode],
instructions="Extract exceptional examples of noteworthy problem-solving scenarios",
enable_inserts=True,
store=self.store,
)
# Track store size for periodic dumping
self.operations_since_last_dump = 0
self.dump_threshold = 10 # Dump every 10 operations
def _fast_load_store(self, filepath: str = "episodes_dump.json"):
"""Fast loading of store data with caching"""
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
# Bulk load episodes into cache
for key, episode_data in data.items():
if 'content' in episode_data['value']:
self.episode_cache[key] = episode_data['value']['content']
logger.info(f"Loaded {len(self.episode_cache)} episodes into cache")
except Exception as e:
logger.error(f"Error loading store: {e}")
raise
@lru_cache(maxsize=100)
def _cached_search(self, query: str, limit: int):
"""Cached search function"""
cache_key = f"{query[:50]}_{limit}"
if cache_key in self.search_cache:
return self.search_cache[cache_key]
# Perform actual search
results = self.store.search(
("memories", "episodes"),
query=query,
limit=limit,
)
# Cache the results
self.search_cache[cache_key] = results
return results
def _generate_system_message(self, similar_episodes):
"""Generate system message from similar episodes"""
system_message = "You are a helpful assistant."
if similar_episodes:
system_message += "\n\n### EPISODIC MEMORY:"
for i, item in enumerate(similar_episodes, start=1):
episode = item.value["content"]
system_message += f"\n\nEpisode {i}:"
system_message += f"\nObservation: {episode.get('observation', 'N/A')}"
system_message += f"\nThought: {episode.get('thoughts', 'N/A')}"
system_message += f"\nAction: {episode.get('action', 'N/A')}"
system_message += f"\nResult: {episode.get('result', 'N/A')}"
return system_message
def app(self, messages: list, top_k: int, feedback: bool = False):
"""Optimized app method with async processing"""
logger.info('Starting processing...')
# Get user message
user_message = messages[-1]["content"]
# 1. Search for similar episodes
logger.info('Searching for similar episodes...')
similar = self._cached_search(user_message, top_k)
# 2. Generate response
logger.info('Generating response...')
system_message = self._generate_system_message(similar)
# Use thread pool for async generation
def generate_response():
return self.memory_llm.generate([
{"role": "system", "content": system_message},
*messages[:-1],
{"role": "user", "content": user_message}
])
# Run in thread pool to avoid blocking
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(generate_response)
response = future.result(timeout=30) # 30 second timeout
logger.info('Response generated')
# 3. Prepare messages for background processing
messages_with_assistant = [
*messages,
{"role": "assistant", "content": response}
]
# 4. Submit store operation to background processor (non-blocking)
if feedback:
self.bg_processor.submit(
self._process_store_operation,
messages_with_assistant,
user_message,
response
)
return response
def _process_store_operation(self, messages_with_assistant, user_message, response):
"""Process store operation in background"""
try:
logger.info('Processing store operation in background...')
self.manager.invoke({"messages": messages_with_assistant})
logger.info('Store operation completed')
# Increment operation counter
self.operations_since_last_dump += 1
# Periodic dumping
if self.operations_since_last_dump >= self.dump_threshold:
self._background_dump_store()
self.operations_since_last_dump = 0
except Exception as e:
logger.error(f"Store operation failed: {e}")
# Fallback: add correction directly
self._add_correction_direct(
user_message,
response
)
def _add_correction_direct(self, observation: str, result: str):
"""Add correction directly without manager"""
episode = Episode(
observation=observation,
thoughts="Saved via fallback due to upstream error.",
action="Generated assistant reply and stored episode directly.",
result=result,
)
key = f"episode:{uuid.uuid4()}"
self.store.put(
("memories", "episodes"),
key=key,
value={"content": episode.model_dump()}
)
# Update cache
self.episode_cache[key] = episode.model_dump()
# Clear search cache since we added new data
self.search_cache.clear()
# Schedule background dump
self.bg_processor.submit(self._background_dump_store)
def _background_dump_store(self, filepath: str = None):
"""Dump store to file in background"""
if filepath is None:
filepath = self.dump_file
try:
# Get all items efficiently
items = []
batch_size = 50
# Try to get items in batches if store supports it
try:
# Try batch retrieval if available
items = list(self.store.search(
("memories", "episodes"),
query="", # Empty query to get all
limit=1000 # Limit to prevent memory issues
))
except:
# Fallback to cache
items = [{
'key': key,
'namespace': ("memories", "episodes"),
'value': {'content': value}
} for key, value in self.episode_cache.items()]
# Prepare data for JSON
all_data = {}
for item in items:
if hasattr(item, 'key'):
key = item.key
value = item.value
else:
key = item['key']
value = item['value']
all_data[key] = {
"namespace": ("memories", "episodes"),
"value": value
}
# Write to file
temp_file = f"{filepath}.tmp"
with open(temp_file, 'w', encoding='utf-8') as f:
json.dump(all_data, f, indent=2, ensure_ascii=False)
# Atomic replace
os.replace(temp_file, filepath)
logger.info(f"Successfully dumped {len(all_data)} episodes")
except Exception as e:
logger.error(f"Error dumping store: {e}")
def search_episodes(self, query: str, limit: int = 5):
"""Optimized search method"""
return self._cached_search(query, limit)
def shutdown(self):
"""Clean shutdown"""
# Dump store before shutdown
self._background_dump_store()
# Shutdown background processor
self.bg_processor.shutdown()
logger.info("Agent shutdown complete")
if __name__ == "__main__":
from utils.config import Config
agent = Agent(Config)
result = agent.app([{"role": "user", "content": "hi how are you?"}], 5)
print(f"Response: {result}")
agent.shutdown()
File diff suppressed because one or more lines are too long
+16
View File
@@ -0,0 +1,16 @@
from utils.app import Application
import signal
import threading
def main():
app = Application()
try:
app.run_fastapi()
except KeyboardInterrupt:
print("\nFastAPI server interrupted.")
finally:
print("Application shutdown complete.")
if __name__ == "__main__":
main()
+148
View File
@@ -0,0 +1,148 @@
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from flask_bcrypt import Bcrypt
db = SQLAlchemy()
bcrypt = Bcrypt()
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
# فیلدهای جدید برای پنل ادمین
role = db.Column(db.String(20), default='user') # 'admin', 'user'
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def set_password(self, password):
"""Hash and set the password"""
self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
def check_password(self, password):
"""Check if the provided password matches the hash"""
return bcrypt.check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
class DataFile(db.Model):
__tablename__ = 'data_files'
id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False)
original_filename = db.Column(db.String(255), nullable=False)
file_path = db.Column(db.String(500), nullable=False)
file_size = db.Column(db.Integer, nullable=False)
file_type = db.Column(db.String(50), nullable=False) # 'json', 'pdf', etc.
uploader_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
status = db.Column(db.String(20), default='pending') # 'pending', 'processing', 'completed', 'error'
description = db.Column(db.Text)
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
processed_at = db.Column(db.DateTime)
# ارتباط با آپلودکننده
uploader = db.relationship('User', backref='uploaded_files')
class Article(db.Model):
__tablename__ = 'articles'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
article_id = db.Column(db.String(255), nullable=False)
name = db.Column(db.String(500), nullable=False)
institute = db.Column(db.String(500))
abstract = db.Column(db.Text)
# فیلدهای معیارها
measures_positive = db.Column(db.Text)
measures_negative = db.Column(db.Text)
responses_positive = db.Column(db.Text)
responses_negative = db.Column(db.Text)
scores_positive = db.Column(db.Text)
scores_negative = db.Column(db.Text)
# فیلدهای جدید برای پنل ادمین
pdf_path = db.Column(db.String(500))
data_file_id = db.Column(db.Integer, db.ForeignKey('data_files.id'))
# وضعیت تصحیح
correction_status = db.Column(db.String(20), default='pending') # 'pending', 'in_progress', 'completed', 'reviewed'
corrected_by = db.Column(db.Integer, db.ForeignKey('users.id'))
corrected_at = db.Column(db.DateTime)
reviewer_id = db.Column(db.Integer, db.ForeignKey('users.id'))
reviewed_at = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# ارتباط‌های موجود - مشخص کردن foreign_keys به صورت صریح
user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('articles', lazy=True))
# ارتباط‌های جدید برای پنل ادمین
corrector = db.relationship('User', foreign_keys=[corrected_by], backref='corrected_articles')
reviewer = db.relationship('User', foreign_keys=[reviewer_id], backref='reviewed_articles')
data_file = db.relationship('DataFile', backref='articles')
class ArticleResponse(db.Model):
__tablename__ = 'article_responses'
id = db.Column(db.Integer, primary_key=True)
article_id = db.Column(db.Integer, db.ForeignKey('articles.id'), nullable=False)
measure_index = db.Column(db.Integer, nullable=False)
measure_type = db.Column(db.String(10), nullable=False)
original_response = db.Column(db.Text)
edited_response = db.Column(db.Text)
ai_generated_response = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
article = db.relationship('Article', backref=db.backref('responses', lazy=True))
class ChatSession(db.Model):
__tablename__ = 'chat_sessions'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
# فیلد جدید برای ارتباط با مقاله
article_id = db.Column(db.Integer, db.ForeignKey('articles.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# ارتباط‌های موجود
user = db.relationship('User', backref=db.backref('chat_sessions', lazy=True))
messages = db.relationship('Message', backref='session', lazy=True, cascade='all, delete-orphan')
# ارتباط جدید
article = db.relationship('Article', backref='chat_sessions')
class Message(db.Model):
__tablename__ = 'messages'
id = db.Column(db.Integer, primary_key=True)
session_id = db.Column(db.Integer, db.ForeignKey('chat_sessions.id'), nullable=False)
role = db.Column(db.String(20), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class CorrectionLog(db.Model):
__tablename__ = 'correction_logs'
id = db.Column(db.Integer, primary_key=True)
article_id = db.Column(db.Integer, db.ForeignKey('articles.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
action = db.Column(db.String(50), nullable=False) # 'created', 'updated', 'corrected', 'reviewed'
field_name = db.Column(db.String(100))
old_value = db.Column(db.Text)
new_value = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# ارتباط‌ها
article = db.relationship('Article', backref='correction_logs')
user = db.relationship('User', backref='correction_actions')
+26
View File
@@ -0,0 +1,26 @@
from utils.config import Config
from openai import OpenAI
class Generator:
def __init__(self, model_name):
super().__init__()
self.model_name = model_name
self.model = self.config(model_name)
def generate(self, messages, top_k=None, top_p=None, do_sample=None, max_length=None):
print("sending prompt to deepseek")
response = self.model.chat.completions.create(
model=self.model_name,
messages=messages,
stream=False
)
return response.choices[0].message.content
def config(self, model_name):
return OpenAI(api_key=Config.API_KEY, base_url='https://api.avalai.ir/v1')
if __name__ == "__main__":
generator = Generator('deepseek-v3.1')
print(generator.generate("hi how are you?"))
+92
View File
@@ -0,0 +1,92 @@
from rag.generator import Generator
from rag.retriever import Retriver
import numpy as np
class RAG:
def __init__(self, model_name, retriever_name, vector_path, data_path, device):
self.model_name = model_name
self.retriever_name = retriever_name
self.vector_path = vector_path
self.data_path = data_path
self.generator = Generator(model_name)
# self.retriever = Retriver(vector_path, data_path, retriever_name, device)
def make_prompt(self, query, top_k):
results = self._retrieve(query, top_k)
if results:
data = [res.page_content.strip() for res in results]
else:
data = ["شما محتوایی بارگداری نکرده اید."]
prompt = f"""
محتوای ارایه شده : {data}
سوال : {query}
لطفا بر اساس محتوای ارایه شده به سوال کامل و دقیق جواب بده دقت کن که بیشتر جواب تولید کنی و دقیق باشی
"""
sanitized_results = []
for res in results:
meta = getattr(res, "metadata", {})
if isinstance(meta, dict):
clean_meta = {}
for k, v in meta.items():
if isinstance(v, np.generic):
clean_meta[k] = v.item()
else:
clean_meta[k] = v
else:
clean_meta = {}
sanitized_doc = {
"page_content": res.page_content,
"metadata": clean_meta
}
sanitized_results.append(sanitized_doc)
return prompt, sanitized_results
def query(self, query, top_k):
results = self._retrieve(query, top_k)
if results:
data = [res.page_content.strip() for res in results]
resp = self.make_response(query, data)
else:
data = ["شما محتوایی بارگداری نکرده اید."]
resp = self.make_response(query, data)
sanitized_results = []
for res in results:
meta = getattr(res, "metadata", {})
if isinstance(meta, dict):
clean_meta = {}
for k, v in meta.items():
if isinstance(v, np.generic):
clean_meta[k] = v.item()
else:
clean_meta[k] = v
else:
clean_meta = {}
sanitized_doc = {
"page_content": res.page_content,
"metadata": clean_meta
}
sanitized_results.append(sanitized_doc)
return sanitized_results, resp
def make_response(self, query, data):
prompt = f"""
محتوای ارایه شده : {data}
سوال : {query}
لطفا بر اساس محتوای ارایه شده به سوال کامل و دقیق جواب بده دقت کن که بیشتر جواب تولید کنی و دقیق باشی
"""
return self.generator.generate(prompt)
def _retrieve(self, query, top_k):
return self.retriever.retrieve(query, top_k)
def update_retriever(self):
"""Retriever را با اسناد جدید در data_path دوباره بارگذاری می‌کند."""
self.retriever.reload_documents()
+106
View File
@@ -0,0 +1,106 @@
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from typing import List, Dict, Any
import os
import sys
class Retriver:
def __init__(self, vector_path, data_path, retriever_name, device):
self.vector_path = vector_path
self.data_path = data_path
self.embedding_model = HuggingFaceEmbeddings(
model_name=retriever_name,
model_kwargs={"device": device},
encode_kwargs={"normalize_embeddings": True}
)
self.vectorstore = self.load_vectorstore(self.embedding_model, self.vector_path)
if self.vectorstore is None:
documents = self.load_documents(data_path)
chunks = self.split_documents(documents)
if chunks:
self.vectorstore = self.create_retriever(chunks, self.embedding_model)
self.save_vectorstore(self.vectorstore, self.vector_path)
def get_data_dir(self, data_dir=None):
import sys, os
if data_dir is not None:
return data_dir
if getattr(sys, 'frozen', False):
exe_dir = os.path.dirname(sys.executable)
else:
exe_dir = os.path.dirname(os.path.abspath(__file__))
data_dir_path = os.path.join(exe_dir, 'data')
return data_dir_path
def load_documents(self, data_dir=None):
data_dir = self.get_data_dir(data_dir)
pdf_loader = DirectoryLoader(data_dir, glob="*.pdf", loader_cls=PyPDFLoader)
txt_loader = DirectoryLoader(data_dir, glob="*.txt", loader_cls=TextLoader)
pdf_docs = pdf_loader.load()
txt_docs = txt_loader.load()
all_docs = pdf_docs + txt_docs
return all_docs
def retrieve(self, query, pdf_path, top_k=10):
if not self.vectorstore:
return []
filter_dict = {"source": pdf_path}
try:
docs_and_scores = self.vectorstore.similarity_search_with_score(
query,
k=top_k,
filter=filter_dict
)
except TypeError:
docs_and_scores = self.vectorstore.similarity_search_with_score(query, k=top_k*2)
docs_and_scores = [(doc, score) for doc, score in docs_and_scores
if doc.metadata.get("source") == pdf_path]
for doc, score in docs_and_scores:
doc.metadata["score"] = float(score)
return [doc for doc, _ in docs_and_scores[:top_k]]
def split_documents(self, documents, chunk_size=1000, chunk_overlap=200):
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=["\n\n", "\n", " ", ""]
)
chunks = text_splitter.split_documents(documents)
return chunks
def create_retriever(self, documents, embedding_model):
vectorstore = FAISS.from_documents(documents, embedding_model)
return vectorstore
def save_vectorstore(self, vectorstore, path="./faiss_index"):
vectorstore.save_local(path)
def load_vectorstore(self, embedding_model, path="./faiss_index"):
if os.path.exists(os.path.join(path, "index.faiss")):
vectorstore = FAISS.load_local(path, embedding_model, allow_dangerous_deserialization=True)
return vectorstore
else:
return None
def reload_documents(self):
"""
تمام اسناد موجود در self.data_path را دوباره بارگذاری کرده،
vectorstore را بازسازی و ذخیره می‌کند.
"""
print("🔄 Reloading documents and rebuilding vectorstore...")
documents = self.load_documents(self.data_path)
chunks = self.split_documents(documents)
if chunks:
self.vectorstore = self.create_retriever(chunks, self.embedding_model)
self.save_vectorstore(self.vectorstore, self.vector_path)
else:
self.vectorstore = None
print("⚠️ No documents found to reload.")
+119
View File
@@ -0,0 +1,119 @@
# Python Package Requirements (generated from conda 'nlp' environment)
# Note: System/Conda-specific packages (like cuda, blas, compilers) have been filtered out.
# It is recommended to use within a Python virtual environment.
# Core ML & Data Science
numpy==1.26.4
pandas==2.3.3
scipy==1.16.2
scikit-learn==1.7.2
# Deep Learning Frameworks & GPU
torch==2.7.1
transformers==4.57.1
datasets==4.3.0
accelerate==1.11.0
safetensors==0.6.2
bitsandbytes==0.48.1
tokenizers==0.22.1
faiss-cpu==1.9.0
hnswlib==0.8.0
# LangChain Ecosystem
langchain==0.3.27
langchain-core==0.3.79
langchain-community==0.3.27
langchain-openai==0.3.35
langchain-anthropic==0.3.22
langchain-huggingface==0.3.1
langchain-text-splitters==0.3.11
langgraph==1.0.1
langgraph-checkpoint==3.0.1
langgraph-sdk==0.2.9
langsmith==0.4.32
# LLM Libraries & APIs
openai==2.8.0
anthropic==0.73.0
tiktoken==0.12.0
huggingface-hub==0.36.0
sentence-transformers==5.1.2
llama-cpp-python==0.3.16
# Web & Async
fastapi==0.111.0
uvicorn==0.29.0
starlette==0.37.2
pydantic==2.12.3
pydantic-core==2.41.4
pydantic-settings==2.11.0
httpx==0.28.1
aiohttp==3.13.1
websockets==15.0.1
# Database
sqlalchemy==2.0.44
psycopg2-binary==2.9.11
pyarrow==22.0.0
# Flask Ecosystem (if you need it)
flask==3.0.3
flask-sqlalchemy==3.1.1
flask-login==0.6.3
flask-bcrypt==1.0.1
# Utilities & Helpers
click==8.3.0
colorama==0.4.6
loguru==0.7.3
tqdm==4.67.1
tenacity==9.1.2
python-dotenv==1.0.1
pyyaml==6.0.3
orjson==3.11.4
rich==14.2.0
typer==0.20.0
watchfiles==1.1.1
filelock==3.20.0
fsspec==2025.9.0
jiter==0.12.0
sqlite-vec==0.1.6
diskcache==5.6.3
docstring-parser==0.17.0
# Jupyter & Notebook (optional)
ipython==9.5.0
jupyterlab==4.4.7
ipykernel==6.30.1
traitlets==5.14.3
# Document Processing
pypdf==6.1.3
pdfplumber==0.11.5
pymupdf==1.24.1
python-docx==1.2.0
beautifulsoup4==4.13.5
lxml==6.0.2
# Other Dependencies
pillow==11.3.0
pybind11==3.0.1
optree==0.17.0
typing-extensions==4.15.0
importlib-metadata==8.7.0
packaging==25.0
requests==2.32.3
urllib3==2.5.0
certifi==2025.11.12
charset-normalizer==3.4.4
idna==3.11
cryptography==46.0.3
bcrypt==5.0.0
argon2-cffi==21.3.0
python-multipart==0.0.20
trustcall==0.0.39
dydantic==0.0.8
langmem==0.0.30
distro==1.9.0
webencodings==0.5.1
zstandard==0.25.0
+185
View File
@@ -0,0 +1,185 @@
from fastapi import FastAPI, HTTPException, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Dict, Any, Optional
import numpy as np
import os
import shutil
from agent.espisodic_agent import Agent
from utils.config import Config
app = FastAPI(title="YARA RAG API", description="Backend API for YARA RAG assistant")
agent = Agent(Config)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
os.makedirs(Config.DATA_PATH, exist_ok=True)
class ChunkMetadata(BaseModel):
score: Optional[float] = None
source: Optional[str] = None
page: Optional[int] = None
class Chunk(BaseModel):
page_content: str
metadata: ChunkMetadata
class QueryRequest(BaseModel):
message: str
# pdf_path: str
class QueryResponse(BaseModel):
response: str
def sanitize_metadata(metadata: Any) -> Dict[str, Any]:
if not isinstance(metadata, dict):
return {}
clean_meta = {}
for k, v in metadata.items():
if isinstance(v, np.generic):
v = v.item()
if k == "page":
if v is None:
clean_meta[k] = None
else:
try:
clean_meta[k] = int(v)
except (ValueError, TypeError):
clean_meta[k] = None
elif k == "score":
if v is None:
clean_meta[k] = None
else:
try:
clean_meta[k] = float(v)
except (ValueError, TypeError):
clean_meta[k] = None
elif k == "source":
clean_meta[k] = str(v) if v is not None else None
else:
if isinstance(v, (int, float, str, bool, type(None))):
clean_meta[k] = v
else:
clean_meta[k] = str(v)
return clean_meta
@app.post("/query", response_model=QueryResponse)
async def query_rag(request: QueryRequest):
if not request.message.strip():
raise HTTPException(status_code=400, detail="Message cannot be empty")
try:
print('query callled')
full_response = agent.app([{"role": "user", "content": request.message}], 5)
print("finisehed")
return QueryResponse(response=format_response_as_markdown(full_response))
except Exception as e:
raise HTTPException(status_code=500, detail=f"RAG processing failed: {str(e)}")
def format_response_as_markdown(response_text: str) -> str:
"""
فرمت کردن پاسخ به صورت Markdown برای نمایش زیباتر
"""
if not response_text:
return response_text
response_text = response_text.strip()
if any(char in response_text for char in ['#', '*', '`', '-']):
return response_text
paragraphs = [p.strip() for p in response_text.split('\n') if p.strip()]
formatted_paragraphs = []
for i, paragraph in enumerate(paragraphs):
if len(paragraph.split()) > 10:
formatted_paragraphs.append(paragraph)
else:
if paragraph.endswith(':') or any(keyword in paragraph.lower() for keyword in ['نکته', 'توجه', 'مهم', 'خلاصه']):
formatted_paragraphs.append(f"**{paragraph}**")
else:
formatted_paragraphs.append(paragraph)
final_paragraphs = []
for paragraph in formatted_paragraphs:
if any(indicator in paragraph for indicator in ['', '- ', '۱.', '۲.', '۳.']):
final_paragraphs.append(paragraph)
elif len(paragraph.split()) <= 8 and not paragraph.endswith(('.', ':', ';')):
final_paragraphs.append(f"- {paragraph}")
else:
final_paragraphs.append(paragraph)
return '\n\n'.join(final_paragraphs)
@app.post("/format_response")
async def format_response(request: dict):
"""
فرمت کردن پاسخ به صورت Markdown
"""
try:
response_text = request.get("response", "")
formatted_response = format_response_as_markdown(response_text)
return {
"original": response_text,
"formatted": formatted_response,
"status": "success"
}
except Exception as e:
return {
"original": request.get("response", ""),
"formatted": request.get("response", ""),
"status": "error",
"message": str(e)
}
@app.post("/upload")
async def upload_files(files: List[UploadFile] = File(...)):
if not files:
raise HTTPException(status_code=400, detail="No files uploaded")
saved_files = []
try:
for file in files:
if not file.filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail=f"Only PDF files allowed: {file.filename}")
file_path = os.path.join(Config.DATA_PATH, file.filename)
with open(file_path, "wb") as f:
shutil.copyfileobj(file.file, f)
saved_files.append(file.filename)
rag.update_retriever()
return {
"status": "success",
"message": f"{len(saved_files)} PDF file(s) uploaded and retriever updated.",
"files": saved_files
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Upload or update failed: {str(e)}")
@app.get("/health")
def health_check():
return {"status": "OK", "model_loaded": rag is not None}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
+20
View File
@@ -0,0 +1,20 @@
from service.rag_api import app as api
import uvicorn
from werkzeug.serving import make_server
class Application:
def run_fastapi(self):
print("FastAPI server starting on http://0.0.0.0:8000")
config = uvicorn.Config(
api,
host="0.0.0.0",
port=8000,
log_level="info",
)
self.uvicorn_server = uvicorn.Server(config)
self.uvicorn_server.run()
if __name__ == "__main__":
app = Application()
app.run_fastapi()
+33
View File
@@ -0,0 +1,33 @@
import os
from dotenv import load_dotenv
load_dotenv()
# Set Hugging Face mirror for better connectivity
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
class Config:
API_KEY = os.getenv("API_KEY")
MODEL_NAME = os.getenv("MODEL_NAME")
RETREIVER_NAME = os.getenv("RETREIVER_NAME")
VECTOR_PATH = os.getenv("VECTOR_PATH")
DATA_PATH = os.getenv("DATA_PATH")
DEVICE = os.getenv('DEVICE')
SECRET_KEY = os.environ.get('SECRET_KEY')
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///yara.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
RAG_API_URL = os.environ.get('RAG_API_URL') or 'http://localhost:8000/query'
SESSION_COOKIE_SECURE = os.environ.get('FLASK_ENV') == 'production'
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
SQLALCHEMY_TRACK_MODIFICATIONS = False
UPLOAD_FOLDER = 'uploads'
# MAX_CONTENT_LENGTH = 50 * 1024 * 1024
if __name__ == "__main__":
print(Config.API_KEY)
print(Config.DATA_PATH)
print(Config.RETREIVER_NAME)
print(Config.DEVICE)