Обновить solution.py
This commit is contained in:
+98
-95
@@ -1,108 +1,111 @@
|
|||||||
<|channel|>final code<|message|>import time
|
# solution.py
|
||||||
from typing import Callable, Awaitable
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import uvicorn
|
|
||||||
from fastapi import FastAPI, Request, Response, HTTPException
|
|
||||||
from fastapi.middleware.base import BaseHTTPMiddleware
|
|
||||||
from langchain.llms.openai import OpenAI
|
|
||||||
from qdrant_client import QdrantClient
|
|
||||||
from rich.console import Console
|
|
||||||
|
|
||||||
# Консоль для красивого логирования
|
|
||||||
console = Console()
|
|
||||||
|
|
||||||
# Инициализация клиента Qdrant (предполагается, что он уже запущен)
|
|
||||||
qdrant = QdrantClient(host="localhost", port=6333)
|
|
||||||
|
|
||||||
# Инициализация LLM (замените на свой ключ API)
|
|
||||||
llm = OpenAI(api_key="YOUR_OPENAI_API_KEY")
|
|
||||||
|
|
||||||
app = FastAPI(title="Human‑in‑the‑loop Middleware Demo")
|
|
||||||
|
|
||||||
|
|
||||||
class HumanInLoopMiddleware(BaseHTTPMiddleware):
|
|
||||||
"""
|
"""
|
||||||
Middleware, который позволяет прерывать и возобновлять обработку запросов.
|
Агент с HumanInTheLoopMiddleware: при каждом вызове инструмента
|
||||||
Если в заголовке `X-HITL-Interrupt` присутствует значение 'true',
|
агент останавливается, пользователь подтверждает (approve/reject),
|
||||||
запрос будет поставлен в очередь на паузу. При получении
|
после чего выполнение возобновляется через Command.
|
||||||
запроса с заголовком `X-HITL-Resume` выполнение продолжается.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, app: FastAPI):
|
import json
|
||||||
super().__init__(app)
|
from langchain_openai import ChatOpenAI
|
||||||
# Очередь для хранения приостановленных запросов (id -> request data)
|
from langchain_core.tools import tool
|
||||||
self.paused_requests = {}
|
from langchain.agents import create_agent
|
||||||
|
from langchain.agents.middleware import HumanInTheLoopMiddleware
|
||||||
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
|
from langgraph.types import Command
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
# 1. Инструмент
|
||||||
interrupt_flag = request.headers.get("X-HITL-Interrupt", "false").lower()
|
@tool
|
||||||
resume_flag = request.headers.get("X-HITL-Resume", "false").lower()
|
def get_weather(city: str, date: str = "сегодня") -> str:
|
||||||
|
"""Получить погоду в городе на указанную дату."""
|
||||||
|
return f"В городе {city} на {date}: солнечно, 25°C."
|
||||||
|
|
||||||
# Если запрос должен быть приостановлен
|
# 2. LLM
|
||||||
if interrupt_flag == "true":
|
llm = ChatOpenAI(
|
||||||
req_id = str(time.time())
|
model="gpt-4o-mini",
|
||||||
self.paused_requests[req_id] = {
|
temperature=0,
|
||||||
"method": request.method,
|
)
|
||||||
"url": request.url.path,
|
|
||||||
"body": await request.body(),
|
|
||||||
"headers": dict(request.headers),
|
|
||||||
}
|
|
||||||
console.log(f"[yellow]Request {req_id} paused[/]")
|
|
||||||
return Response(content=f"Request paused with id: {req_id}", status_code=202)
|
|
||||||
|
|
||||||
# Если запрос должен возобновиться
|
# 3. Память
|
||||||
if resume_flag == "true":
|
memory = MemorySaver()
|
||||||
req_id = request.query_params.get("resume_id")
|
|
||||||
if not req_id or req_id not in self.paused_requests:
|
|
||||||
raise HTTPException(status_code=400, detail="Invalid or missing resume_id")
|
|
||||||
|
|
||||||
paused = self.paused_requests.pop(req_id)
|
# 4. Агент с HumanInTheLoopMiddleware
|
||||||
console.log(f"[green]Resuming request {req_id}[/]")
|
agent = create_agent(
|
||||||
# Создаём новый Request объект из сохранённых данных
|
model=llm,
|
||||||
new_request = Request(
|
tools=[get_weather],
|
||||||
scope={
|
system_prompt="Ты полезный ассистент.",
|
||||||
"type": "http",
|
middleware=[
|
||||||
"method": paused["method"],
|
HumanInTheLoopMiddleware(
|
||||||
"path": paused["url"],
|
interrupt_on={
|
||||||
"headers": [(k.encode(), v.encode()) for k, v in paused["headers"].items()],
|
"get_weather": True,
|
||||||
"query_string": b"",
|
|
||||||
"client": request.client,
|
|
||||||
"server": request.scope.get("server"),
|
|
||||||
},
|
},
|
||||||
receive=lambda: {"type": "http.request", "body": paused["body"]},
|
description_prefix="Подтвердите вызов инструмента",
|
||||||
)
|
),
|
||||||
return await call_next(new_request)
|
|
||||||
|
|
||||||
# Нормальная обработка
|
|
||||||
response = await call_next(request)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
app.add_middleware(HumanInLoopMiddleware)
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/process")
|
|
||||||
async def process_endpoint(data: dict):
|
|
||||||
"""
|
|
||||||
Пример эндпоинта, который использует LLM и Qdrant.
|
|
||||||
"""
|
|
||||||
console.log("[blue]Processing request...[/]")
|
|
||||||
# Сохраняем запрос в Qdrant
|
|
||||||
qdrant.upsert(
|
|
||||||
collection_name="requests",
|
|
||||||
points=[
|
|
||||||
{
|
|
||||||
"id": str(time.time()),
|
|
||||||
"vector": [0.1, 0.2], # placeholder vector
|
|
||||||
"payload": data,
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
|
checkpointer=memory,
|
||||||
)
|
)
|
||||||
# Генерируем ответ через LLM
|
|
||||||
prompt = f"User asked: {data.get('question', '')}"
|
|
||||||
answer = llm(prompt)
|
|
||||||
console.log("[blue]LLM response generated[/]")
|
|
||||||
return {"answer": answer}
|
|
||||||
|
|
||||||
|
# 5. Сбор решений от пользователя
|
||||||
|
def get_user_decisions(action_requests: list[dict], review_configs: list[dict]) -> list[dict]:
|
||||||
|
decisions = []
|
||||||
|
for action, review_cfg in zip(action_requests, review_configs):
|
||||||
|
name = action.get("name", "unknown")
|
||||||
|
args = action.get("args", {})
|
||||||
|
description = action.get("description", "")
|
||||||
|
allowed = review_cfg.get("allowed_decisions", ["approve", "reject"])
|
||||||
|
|
||||||
|
print(f"\n--- Подтверждение ---")
|
||||||
|
print(f"Инструмент: {name}")
|
||||||
|
print(f"Аргументы: {json.dumps(args, ensure_ascii=False)}")
|
||||||
|
if description:
|
||||||
|
print(f"Описание: {description}")
|
||||||
|
print(f"Разрешённые решения: {', '.join(allowed)}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
choice = input("a=approve, r=reject: ").strip().lower()
|
||||||
|
if choice in ("a", "approve"):
|
||||||
|
decisions.append({"type": "approve"})
|
||||||
|
break
|
||||||
|
elif choice in ("r", "reject"):
|
||||||
|
msg = input("Причина отказа: ").strip()
|
||||||
|
decisions.append({
|
||||||
|
"type": "reject",
|
||||||
|
"message": msg or "Запрос отклонён пользователем",
|
||||||
|
})
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Неверный ввод. Введите 'a' или 'r'.")
|
||||||
|
return decisions
|
||||||
|
|
||||||
|
# 6. Основной цикл
|
||||||
|
def main() -> None:
|
||||||
|
config = {"configurable": {"thread_id": "session-1"}}
|
||||||
|
print("Привет! Введите запрос или 'выход' для завершения.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
user_input = input("\nВы: ").strip()
|
||||||
|
if user_input.lower() in {"выход", "exit", "quit"}:
|
||||||
|
print("Завершение работы.")
|
||||||
|
break
|
||||||
|
|
||||||
|
result = agent.invoke(
|
||||||
|
{"messages": [{"role": "human", "content": user_input}]},
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
|
||||||
|
while "__interrupt__" in result:
|
||||||
|
interrupt_value = result["__interrupt__"][0].value
|
||||||
|
action_requests = interrupt_value.get("action_requests", [])
|
||||||
|
review_configs = interrupt_value.get("review_configs", [])
|
||||||
|
|
||||||
|
decisions = get_user_decisions(action_requests, review_configs)
|
||||||
|
|
||||||
|
result = agent.invoke(
|
||||||
|
Command(resume={"decisions": decisions}),
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\nАгент: {result['messages'][-1].content}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
main()
|
||||||
Reference in New Issue
Block a user