From 6bb333826fb1bdb8c45c2e030178fa433d16e593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D0=B8=D0=BB=20=D0=92=D0=B8=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BE=D0=B2?= Date: Thu, 2 Jul 2026 08:38:40 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20main.py=20=E2=80=94=20=D0=9F=D0=BE=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BD=D1=8B=D0=B9=20=D1=8D=D0=BA=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD:=20FAQ-=D0=B1=D0=BE=D1=82=20=E2=80=94=20Chro?= =?UTF-8?q?maDB=20+=20=D0=BE=D0=B4=D0=B8=D0=BD=20MCP-tool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 195 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 99 insertions(+), 96 deletions(-) diff --git a/main.py b/main.py index 0b37309..41431b0 100644 --- a/main.py +++ b/main.py @@ -2,20 +2,18 @@ import os import asyncio import json from pathlib import Path -from dotenv import load_dotenv from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_chroma import Chroma from langchain_core.documents import Document from langchain.tools import tool +from langchain_text_splitter import RecursiveCharacterTextSplitter + from deepagents import create_deep_agent from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend from langchain_core.messages import HumanMessage -# Загрузка переменных окружения -load_dotenv() - -# Настройка LLM и эмбеддингов через OpenRouter +# ---------- LLM ---------- llm = ChatOpenAI( model="openai/gpt-oss-20b:free", base_url="https://openrouter.ai/api/v1", @@ -23,63 +21,7 @@ llm = ChatOpenAI( temperature=0.0, ) -embeddings = OpenAIEmbeddings( - model="text-embedding-3-small", - base_url="https://openrouter.ai/api/v1", - api_key=os.getenv("OPENAI_API_KEY"), -) - -# Инициализация Chroma -vector_store = Chroma( - collection_name="faq_collection", - embedding_function=embeddings, - persist_directory="./chroma_faq", -) - -# Загрузка FAQ из markdown файлов в Chroma -def load_faq_to_chroma() -> None: - data_dir = Path("data") - if not data_dir.exists(): - return - docs = [] - for md_file in data_dir.glob("*.md"): - content = md_file.read_text(encoding="utf-8") - title = md_file.stem - docs.append(Document(page_content=content, metadata={"title": title})) - if docs: - vector_store.add_documents(docs) - -# Tool: поиск по базе знаний -@tool -def search_course_docs(query: str, k: int = 3) -> str: - """Search the knowledge base for relevant information.""" - docs = vector_store.similarity_search(query, k=k) - return "\n".join(d.page_content for d in docs) if docs else "No results." - -# Tool: получение метаданных курса (MCP-стиль) -@tool -def fetch_course_meta(query: str) -> str: - """Fetch course metadata from local JSON.""" - meta_path = Path("meta.json") - if not meta_path.exists(): - return "Metadata file not found." - data = json.loads(meta_path.read_text(encoding="utf-8")) - # Простая фильтрация по ключевому слову в запросе - if "schedule" in query.lower(): - return json.dumps(data.get("schedule", []), indent=2) - if "instructor" in query.lower(): - return data.get("instructor", "Unknown") - return json.dumps(data, indent=2) - -# Системный промпт агента -system_prompt = ( - "You are a helpful FAQ bot. Use only one tool per query. " - "If the question is about course materials, use search_course_docs. " - "If about schedule or metadata, use fetch_course_meta. " - "Indicate source in answer: source: chroma | mcp_meta." -) - -# Backend для deepagents +# ---------- Backend ---------- backend = CompositeBackend( [ LocalShellBackend(workspace_dir="./workspace"), @@ -87,7 +29,74 @@ backend = CompositeBackend( ] ) -# Создание агента +# ---------- Embeddings ---------- +embeddings = OpenAIEmbeddings( + model="text-embedding-3-small", + base_url="https://openrouter.ai/api/v1", + api_key=os.getenv("OPENAI_API_KEY"), +) + +# ---------- Vector Store ---------- +vector_store = Chroma( + collection_name="faq", + embedding_function=embeddings, + persist_directory="./chroma_faq", +) + +# ---------- Load FAQ into Chroma ---------- +def load_faq_to_chroma() -> None: + """Read .md files from data/ and add them to the Chroma collection.""" + data_dir = Path("data") + if not data_dir.exists(): + return + splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) + docs = [] + for md_file in data_dir.glob("*.md"): + content = md_file.read_text(encoding="utf-8") + chunks = splitter.split_text(content) + for i, chunk in enumerate(chunks): + docs.append( + Document( + page_content=chunk, + metadata={"source": md_file.name, "chunk": i}, + ) + ) + if docs: + vector_store.add_documents(docs) + vector_store.persist() + +# ---------- Tools ---------- +@tool +def search_course_docs(query: str, k: int = 3) -> str: + """Search the knowledge base for relevant information.""" + docs = vector_store.similarity_search(query, k=k) + return "\n\n".join(d.page_content for d in docs) if docs else "No results found." + +# Load meta data once +META_PATH = Path("meta.json") +if META_PATH.exists(): + META_DATA = json.loads(META_PATH.read_text(encoding="utf-8")) +else: + META_DATA = {} + +@tool +def fetch_course_meta(query: str) -> str: + """Return course metadata (schedule, etc.).""" + # Simple lookup: return the whole meta if query matches a key + for key, value in META_DATA.items(): + if key.lower() in query.lower(): + return f"{key}: {value}" + # Fallback: return all metadata + return json.dumps(META_DATA, indent=2) + +# ---------- Agent ---------- +system_prompt = ( + "You are a helpful FAQ bot. Use search_course_docs for questions about course materials. " + "Use fetch_course_meta for questions about schedule or metadata. " + "Do not call both tools unless necessary. " + "In your answer, indicate source: chroma or mcp_meta." +) + agent = create_deep_agent( model=llm, tools=[search_course_docs, fetch_course_meta], @@ -95,44 +104,38 @@ agent = create_deep_agent( system_prompt=system_prompt, ) -# Предварительная загрузка FAQ -load_faq_to_chroma() - -# Предустановленные вопросы -preset_questions = [ - "Где находятся материалы по лекции 3?", - "Как изменить пароль?", - "Какой график занятий на следующую неделю?", +# ---------- CLI ---------- +PRESET_QUESTIONS = [ + "What topics are covered in the introductory module?", + "Explain the advanced algorithm discussed in chapter 3.", + "What is the schedule for the next semester?", ] -async def main() -> None: - print("FAQ Bot CLI") - print("1-3: Предустановленные вопросы") - print("4: Ввести собственный вопрос") - print("5: Выход") - while True: - choice = input("Выберите вариант (1-5): ").strip() - if choice == "5": - print("До свидания!") - break - if choice in {"1", "2", "3"}: - question = preset_questions[int(choice) - 1] - elif choice == "4": - question = input("Введите ваш вопрос: ").strip() - if not question: - print("Пустой вопрос, попробуйте снова.") - continue - else: - print("Неверный выбор, попробуйте снова.") - continue - - print(f"Вопрос: {question}") +async def run_preset(): + for q in PRESET_QUESTIONS: result = await agent.ainvoke( - {"messages": [HumanMessage(content=question)]}, + {"messages": [HumanMessage(content=q)]}, {"configurable": {"thread_id": "session-1"}}, ) - answer = result["messages"][-1].content - print(f"Ответ:\n{answer}\n") + print("\nQuestion:", q) + print("Answer:", result["messages"][-1].content) + +async def interactive_loop(): + print("\nEnter your question (type 'exit' to quit):") + while True: + user_input = input("> ") + if user_input.lower() in ("exit", "quit"): + break + result = await agent.ainvoke( + {"messages": [HumanMessage(content=user_input)]}, + {"configurable": {"thread_id": "session-1"}}, + ) + print("Answer:", result["messages"][-1].content) + +async def main(): + load_faq_to_chroma() + await run_preset() + await interactive_loop() if __name__ == "__main__": asyncio.run(main()) \ No newline at end of file