First!
This commit is contained in:
144
agentic_rag_interface.py
Normal file
144
agentic_rag_interface.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import gradio as gr
|
||||||
|
import asyncio
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
class AgenticRAGInterface:
|
||||||
|
def __init__(self, agent_graph, knowledge_base):
|
||||||
|
self.agent = agent_graph
|
||||||
|
self.kb = knowledge_base
|
||||||
|
self.conversation_history = []
|
||||||
|
|
||||||
|
def process_query(self, query: str, use_rag: bool):
|
||||||
|
"""Обработка запроса пользователя"""
|
||||||
|
|
||||||
|
# Добавляем сообщение в историю
|
||||||
|
self.conversation_history.append({"role": "user", "content": query})
|
||||||
|
|
||||||
|
# Если нужен RAG, ищем в базе знаний
|
||||||
|
context = ""
|
||||||
|
if use_rag:
|
||||||
|
search_results = self.kb.search(query)
|
||||||
|
if search_results:
|
||||||
|
context = "\n".join([f"[{i+1}] {res['text'][:200]}..."
|
||||||
|
for i, res in enumerate(search_results)])
|
||||||
|
|
||||||
|
# Подготавливаем состояние
|
||||||
|
initial_state = {
|
||||||
|
"messages": [HumanMessage(content=query)],
|
||||||
|
"knowledge_base": context,
|
||||||
|
"needs_search": use_rag,
|
||||||
|
"current_step": "start",
|
||||||
|
"final_answer": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Запускаем агента
|
||||||
|
try:
|
||||||
|
result = self.agent.invoke(initial_state)
|
||||||
|
answer = result.get("final_answer", "Не удалось получить ответ")
|
||||||
|
|
||||||
|
# Добавляем ответ в историю
|
||||||
|
self.conversation_history.append({"role": "assistant", "content": answer})
|
||||||
|
|
||||||
|
# Форматируем историю для отображения
|
||||||
|
history_text = self._format_history()
|
||||||
|
|
||||||
|
return answer, history_text
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Ошибка: {str(e)}"
|
||||||
|
return error_msg, self._format_history()
|
||||||
|
|
||||||
|
def _format_history(self):
|
||||||
|
"""Форматирование истории разговора"""
|
||||||
|
formatted = []
|
||||||
|
for msg in self.conversation_history[-10:]: # Последние 10 сообщений
|
||||||
|
role = "👤 Пользователь" if msg["role"] == "user" else "🤖 Ассистент"
|
||||||
|
formatted.append(f"{role}: {msg['content']}")
|
||||||
|
return "\n\n".join(formatted)
|
||||||
|
|
||||||
|
def clear_history(self):
|
||||||
|
"""Очистка истории"""
|
||||||
|
self.conversation_history = []
|
||||||
|
return "История очищена", ""
|
||||||
|
|
||||||
|
# Создаем интерфейс
|
||||||
|
interface = AgenticRAGInterface(agent_graph, kb)
|
||||||
|
|
||||||
|
# Создаем Gradio интерфейс
|
||||||
|
def create_gradio_interface():
|
||||||
|
with gr.Blocks(title="Локальный Agentic RAG", theme=gr.themes.Soft()) as demo:
|
||||||
|
gr.Markdown("""
|
||||||
|
# 🤖 Локальный Agentic RAG Система
|
||||||
|
Полностью автономный AI-агент с базой знаний. Работает без интернета!
|
||||||
|
""")
|
||||||
|
|
||||||
|
with gr.Row():
|
||||||
|
with gr.Column(scale=2):
|
||||||
|
query_input = gr.Textbox(
|
||||||
|
label="Ваш запрос",
|
||||||
|
placeholder="Задайте вопрос или дайте задание...",
|
||||||
|
lines=3
|
||||||
|
)
|
||||||
|
|
||||||
|
rag_toggle = gr.Checkbox(
|
||||||
|
label="Использовать базу знаний (RAG)",
|
||||||
|
value=True
|
||||||
|
)
|
||||||
|
|
||||||
|
submit_btn = gr.Button("Отправить", variant="primary")
|
||||||
|
clear_btn = gr.Button("Очистить историю")
|
||||||
|
|
||||||
|
with gr.Column(scale=3):
|
||||||
|
answer_output = gr.Textbox(
|
||||||
|
label="Ответ агента",
|
||||||
|
lines=8,
|
||||||
|
interactive=False
|
||||||
|
)
|
||||||
|
|
||||||
|
history_output = gr.Textbox(
|
||||||
|
label="История разговора",
|
||||||
|
lines=12,
|
||||||
|
interactive=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Примеры запросов
|
||||||
|
gr.Examples(
|
||||||
|
examples=[
|
||||||
|
["Объясни концепцию machine learning простыми словами", True],
|
||||||
|
["Посчитай: (15 * 4) + (120 / 3)", False],
|
||||||
|
["Найди информацию о нейронных сетях в базе знаний", True],
|
||||||
|
["Спланируй изучение Python на месяц", False]
|
||||||
|
],
|
||||||
|
inputs=[query_input, rag_toggle],
|
||||||
|
label="Примеры запросов"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обработчики событий
|
||||||
|
submit_btn.click(
|
||||||
|
fn=interface.process_query,
|
||||||
|
inputs=[query_input, rag_toggle],
|
||||||
|
outputs=[answer_output, history_output]
|
||||||
|
)
|
||||||
|
|
||||||
|
clear_btn.click(
|
||||||
|
fn=interface.clear_history,
|
||||||
|
inputs=[],
|
||||||
|
outputs=[answer_output, history_output]
|
||||||
|
)
|
||||||
|
|
||||||
|
query_input.submit(
|
||||||
|
fn=interface.process_query,
|
||||||
|
inputs=[query_input, rag_toggle],
|
||||||
|
outputs=[answer_output, history_output]
|
||||||
|
)
|
||||||
|
|
||||||
|
return demo
|
||||||
|
|
||||||
|
# Запускаем интерфейс
|
||||||
|
if __name__ == "__main__":
|
||||||
|
demo = create_gradio_interface()
|
||||||
|
demo.launch(
|
||||||
|
server_name="0.0.0.0",
|
||||||
|
server_port=7860,
|
||||||
|
share=False # Не создавать публичную ссылку
|
||||||
|
)
|
||||||
136
local_knowledge_base.py
Normal file
136
local_knowledge_base.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import os
|
||||||
|
from qdrant_client import QdrantClient
|
||||||
|
from qdrant_client.models import Distance, VectorParams, PointStruct
|
||||||
|
|
||||||
|
class LocalKnowledgeBase:
|
||||||
|
def __init__(self, collection_name="documents", persist_dir="./qdrant_data"):
|
||||||
|
# Инициализируем клиент Qdrant
|
||||||
|
self.client = QdrantClient(
|
||||||
|
path=persist_dir, # Локальное хранение
|
||||||
|
prefer_grpc=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.collection_name = collection_name
|
||||||
|
self.embeddings = embeddings
|
||||||
|
|
||||||
|
# Создаем коллекцию если её нет
|
||||||
|
self._create_collection()
|
||||||
|
|
||||||
|
def _create_collection(self):
|
||||||
|
try:
|
||||||
|
self.client.get_collection(self.collection_name)
|
||||||
|
print(f"Коллекция {self.collection_name} уже существует")
|
||||||
|
except:
|
||||||
|
# Создаем новую коллекцию
|
||||||
|
self.client.create_collection(
|
||||||
|
collection_name=self.collection_name,
|
||||||
|
vectors_config=VectorParams(
|
||||||
|
size=768, # Размерность эмбеддингов nomic-embed-text
|
||||||
|
distance=Distance.COSINE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(f"Создана коллекция {self.collection_name}")
|
||||||
|
|
||||||
|
def load_documents(self, directory_path: str):
|
||||||
|
"""Загрузка документов из директории"""
|
||||||
|
loaders = {
|
||||||
|
'.pdf': PyPDFLoader,
|
||||||
|
'.txt': lambda path: DirectoryLoader(path, glob="**/*.txt"),
|
||||||
|
'.docx': lambda path: DirectoryLoader(path, glob="**/*.docx"),
|
||||||
|
}
|
||||||
|
|
||||||
|
all_documents = []
|
||||||
|
|
||||||
|
for ext, loader_class in loaders.items():
|
||||||
|
for file_path in os.listdir(directory_path):
|
||||||
|
if file_path.endswith(ext):
|
||||||
|
full_path = os.path.join(directory_path, file_path)
|
||||||
|
try:
|
||||||
|
loader = loader_class(full_path)
|
||||||
|
documents = loader.load()
|
||||||
|
all_documents.extend(documents)
|
||||||
|
print(f"Загружен {file_path}: {len(documents)} страниц")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка загрузки {file_path}: {e}")
|
||||||
|
|
||||||
|
# Разбиваем на чанки
|
||||||
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
|
chunk_size=1000,
|
||||||
|
chunk_overlap=200,
|
||||||
|
length_function=len
|
||||||
|
)
|
||||||
|
|
||||||
|
chunks = text_splitter.split_documents(all_documents)
|
||||||
|
print(f"Всего чанков: {len(chunks)}")
|
||||||
|
|
||||||
|
# Создаем эмбеддинги и сохраняем в Qdrant
|
||||||
|
self._index_documents(chunks)
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
def _index_documents(self, documents):
|
||||||
|
"""Индексация документов в Qdrant"""
|
||||||
|
points = []
|
||||||
|
|
||||||
|
for i, doc in enumerate(documents):
|
||||||
|
# Создаем эмбеддинг для каждого чанка
|
||||||
|
embedding = self.embeddings.embed_query(doc.page_content)
|
||||||
|
|
||||||
|
point = PointStruct(
|
||||||
|
id=i,
|
||||||
|
vector=embedding,
|
||||||
|
payload={
|
||||||
|
"text": doc.page_content,
|
||||||
|
"source": doc.metadata.get("source", "unknown"),
|
||||||
|
"page": doc.metadata.get("page", 0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
points.append(point)
|
||||||
|
|
||||||
|
# Пакетная загрузка каждые 100 точек
|
||||||
|
if len(points) >= 100:
|
||||||
|
self.client.upsert(
|
||||||
|
collection_name=self.collection_name,
|
||||||
|
points=points
|
||||||
|
)
|
||||||
|
points = []
|
||||||
|
print(f"Индексировано {i+1} документов")
|
||||||
|
|
||||||
|
# Загружаем оставшиеся
|
||||||
|
if points:
|
||||||
|
self.client.upsert(
|
||||||
|
collection_name=self.collection_name,
|
||||||
|
points=points
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Индексация завершена. Всего документов: {len(documents)}")
|
||||||
|
|
||||||
|
def search(self, query: str, top_k: int = 5):
|
||||||
|
"""Поиск в базе знаний"""
|
||||||
|
# Создаем эмбеддинг запроса
|
||||||
|
query_embedding = self.embeddings.embed_query(query)
|
||||||
|
|
||||||
|
# Ищем в Qdrant
|
||||||
|
search_result = self.client.search(
|
||||||
|
collection_name=self.collection_name,
|
||||||
|
query_vector=query_embedding,
|
||||||
|
limit=top_k
|
||||||
|
)
|
||||||
|
|
||||||
|
# Форматируем результаты
|
||||||
|
results = []
|
||||||
|
for hit in search_result:
|
||||||
|
results.append({
|
||||||
|
"text": hit.payload["text"],
|
||||||
|
"score": hit.score,
|
||||||
|
"source": hit.payload.get("source", "unknown")
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Инициализируем базу знаний
|
||||||
|
kb = LocalKnowledgeBase()
|
||||||
|
|
||||||
|
# Загружаем документы (если есть)
|
||||||
|
if os.path.exists("./documents"):
|
||||||
|
kb.load_documents("./documents")
|
||||||
132
main.py
Normal file
132
main.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
from typing import TypedDict, List, Optional, Annotated
|
||||||
|
import operator
|
||||||
|
from langchain_community.llms import Ollama
|
||||||
|
from langchain_community.embeddings import OllamaEmbeddings
|
||||||
|
from langchain_community.vectorstores import Qdrant
|
||||||
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
|
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
from langgraph.graph.message import add_messages
|
||||||
|
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
||||||
|
from langchain.tools import tool
|
||||||
|
from langchain.agents import AgentExecutor, create_react_agent
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Определяем состояние агента
|
||||||
|
class AgentState(TypedDict):
|
||||||
|
messages: Annotated[List, add_messages] # История сообщений
|
||||||
|
knowledge_base: Optional[str] # Релевантные документы
|
||||||
|
current_step: str # Текущий шаг выполнения
|
||||||
|
needs_search: bool # Нужен ли поиск в RAG
|
||||||
|
final_answer: Optional[str] # Финальный ответ
|
||||||
|
|
||||||
|
# Инициализация LLM через Ollama
|
||||||
|
llm = Ollama(
|
||||||
|
model="llama3.2:3b",
|
||||||
|
temperature=0.1, # Низкая температура для консистентности
|
||||||
|
num_predict=1024, # Максимальная длина ответа
|
||||||
|
)
|
||||||
|
|
||||||
|
# Инициализация эмбеддингов
|
||||||
|
embeddings = OllamaEmbeddings(
|
||||||
|
model="nomic-embed-text", # Хорошие локальные эмбеддинги
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем инструменты для агента
|
||||||
|
@tool
|
||||||
|
def search_knowledge_base(query: str) -> str:
|
||||||
|
"""Поиск информации в локальной базе знаний"""
|
||||||
|
# Здесь будет подключение к Qdrant
|
||||||
|
return "Найденная информация из базы знаний"
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def calculate(expression: str) -> str:
|
||||||
|
"""Выполнение математических вычислений"""
|
||||||
|
try:
|
||||||
|
result = eval(expression)
|
||||||
|
return f"Результат: {result}"
|
||||||
|
except:
|
||||||
|
return "Ошибка в выражении"
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def web_search(query: str) -> str:
|
||||||
|
"""Поиск в интернете (если нужно)"""
|
||||||
|
# Можно подключить локальный поиск через DuckDuckGo
|
||||||
|
return "Результаты поиска из интернета"
|
||||||
|
|
||||||
|
# Создаем граф агента
|
||||||
|
def create_agent_graph():
|
||||||
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
|
# Узел: анализ запроса
|
||||||
|
def analyze_query(state: AgentState):
|
||||||
|
messages = state["messages"]
|
||||||
|
last_message = messages[-1].content if messages else ""
|
||||||
|
|
||||||
|
# Простой анализ: проверяем, нужен ли поиск
|
||||||
|
search_keywords = ["информация", "документ", "найди", "ищи", "база знаний"]
|
||||||
|
needs_search = any(keyword in last_message.lower() for keyword in search_keywords)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"needs_search": needs_search,
|
||||||
|
"current_step": "analyzing_query"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Узел: поиск в RAG
|
||||||
|
def rag_search(state: AgentState):
|
||||||
|
if not state["needs_search"]:
|
||||||
|
return {"knowledge_base": None, "current_step": "generating_answer"}
|
||||||
|
|
||||||
|
# Получаем последний запрос
|
||||||
|
query = state["messages"][-1].content
|
||||||
|
|
||||||
|
# Здесь должен быть реальный поиск в Qdrant
|
||||||
|
# Пока заглушка
|
||||||
|
results = ["Документ 1: Информация о...", "Документ 2: Данные по..."]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"knowledge_base": "\n".join(results),
|
||||||
|
"current_step": "generating_answer"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Узел: генерация ответа
|
||||||
|
def generate_answer(state: AgentState):
|
||||||
|
messages = state["messages"]
|
||||||
|
knowledge = state.get("knowledge_base", "")
|
||||||
|
|
||||||
|
# Формируем промпт с контекстом
|
||||||
|
prompt = f"""Ты — интеллектуальный ассистент. Используй следующую информацию если она релевантна:
|
||||||
|
|
||||||
|
Контекст из базы знаний:
|
||||||
|
{knowledge}
|
||||||
|
|
||||||
|
История разговора:
|
||||||
|
{messages[-5:] if len(messages) > 5 else messages}
|
||||||
|
|
||||||
|
Текущий запрос: {messages[-1].content if messages else ''}
|
||||||
|
|
||||||
|
Ответь максимально полезно и точно:"""
|
||||||
|
|
||||||
|
response = llm.invoke(prompt)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"final_answer": response,
|
||||||
|
"current_step": "completed",
|
||||||
|
"messages": messages + [AIMessage(content=response)]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавляем узлы в граф
|
||||||
|
workflow.add_node("analyze", analyze_query)
|
||||||
|
workflow.add_node("search", rag_search)
|
||||||
|
workflow.add_node("generate", generate_answer)
|
||||||
|
|
||||||
|
# Определяем edges (переходы)
|
||||||
|
workflow.set_entry_point("analyze")
|
||||||
|
workflow.add_edge("analyze", "search")
|
||||||
|
workflow.add_edge("search", "generate")
|
||||||
|
workflow.add_edge("generate", END)
|
||||||
|
|
||||||
|
return workflow.compile()
|
||||||
|
|
||||||
|
# Создаем и компилируем граф
|
||||||
|
agent_graph = create_agent_graph()
|
||||||
Reference in New Issue
Block a user