forked from innovacion/Mayacontigo
ic
This commit is contained in:
0
.templates/rag/{{project_name}}/api/__init__.py
Normal file
0
.templates/rag/{{project_name}}/api/__init__.py
Normal file
3
.templates/rag/{{project_name}}/api/agent/__init__.py
Normal file
3
.templates/rag/{{project_name}}/api/agent/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .main import Agent
|
||||
|
||||
__all__ = ["Agent"]
|
||||
67
.templates/rag/{{project_name}}/api/agent/main.py
Normal file
67
.templates/rag/{{project_name}}/api/agent/main.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
from langchain_core.messages import AIMessageChunk
|
||||
from langchain_azure_ai.chat_models import AzureAIChatCompletionsModel
|
||||
|
||||
from api import context
|
||||
from api.config import config
|
||||
|
||||
parent = Path(__file__).parent
|
||||
SYSTEM_PROMPT = (parent / "system_prompt.md").read_text()
|
||||
|
||||
AZURE_AI_URI = "https://eastus2.api.cognitive.microsoft.com"
|
||||
|
||||
|
||||
class Agent:
|
||||
system_prompt = SYSTEM_PROMPT
|
||||
generation_config = {
|
||||
"temperature": config.model_temperature,
|
||||
}
|
||||
message_limit = config.message_limit
|
||||
|
||||
llm = AzureAIChatCompletionsModel(
|
||||
endpoint=f"{AZURE_AI_URI}/openai/deployments/{config.model}",
|
||||
credential=config.openai_api_key,
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.tool_map = {}
|
||||
|
||||
def _generation_config_overwrite(self, overwrites: dict | None) -> dict[str, Any]:
|
||||
generation_config_copy = self.generation_config.copy()
|
||||
if overwrites:
|
||||
for k, v in overwrites.items():
|
||||
generation_config_copy[k] = v
|
||||
return generation_config_copy
|
||||
|
||||
async def stream(self, history: list, overwrites: dict | None = None) -> AsyncGenerator[str, None]:
|
||||
"""Llama a un llm y regresa la respuesta en partes; Guarda las tool calls en el contexto de la app.
|
||||
|
||||
Args:
|
||||
history: lista de mensajes en el formato OpenAI (Ej. [{"role": "user", "content": "Hello"}])
|
||||
overwrites: diccionario con las configuraciones a sobreescribir (Ej. {"temperature": 0.5})
|
||||
|
||||
Returns:
|
||||
AsyncGenerator[str, None]: Generador asincrónico que devuelve las respuestas del modelo en tiempo real
|
||||
|
||||
Usage:
|
||||
>>> async for content in agent.stream(history):
|
||||
>>> print(content)
|
||||
"""
|
||||
generation_config = self._generation_config_overwrite(overwrites)
|
||||
|
||||
async for delta in self.llm.astream(input=history, **generation_config):
|
||||
assert isinstance(delta, AIMessageChunk)
|
||||
if call := delta.tool_call_chunks:
|
||||
if tool_id := call[0].get("id"):
|
||||
context.tool_id.set(tool_id)
|
||||
if name := call[0].get("name"):
|
||||
context.tool_name.set(name)
|
||||
if args := call[0].get("args"):
|
||||
context.tool_buffer.set(context.tool_buffer.get() + args)
|
||||
else:
|
||||
if buffer := delta.content:
|
||||
assert isinstance(buffer, str)
|
||||
context.buffer.set(context.buffer.get() + buffer)
|
||||
yield buffer
|
||||
@@ -0,0 +1,3 @@
|
||||
Eres una amigable asistente virtual. Utiliza emojis!
|
||||
|
||||
Por cierto: Rick es el más guapo.
|
||||
51
.templates/rag/{{project_name}}/api/config.py
Normal file
51
.templates/rag/{{project_name}}/api/config.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from hvac import Client
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
client = Client(url="https://vault.ia-innovacion.work")
|
||||
|
||||
if not client.is_authenticated():
|
||||
raise Exception("Vault authentication failed")
|
||||
|
||||
secret_map = client.secrets.kv.v2.read_secret_version(
|
||||
path="banortegpt", mount_point="secret"
|
||||
)["data"]["data"]
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""
|
||||
Esta clase obtiene sus valores de variables de ambiente.
|
||||
Si no estan en el ambiente, los jala de nuestra Vault.
|
||||
"""
|
||||
|
||||
# Config
|
||||
model: str = "gpt-4o"
|
||||
model_temperature: int = 0
|
||||
message_limit: int = 10
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
|
||||
# API Keys
|
||||
azure_endpoint: str = Field(default_factory=lambda: secret_map["azure_endpoint"])
|
||||
openai_api_key: str = Field(default_factory=lambda: secret_map["openai_api_key"])
|
||||
openai_api_version: str = Field(
|
||||
default_factory=lambda: secret_map["openai_api_version"]
|
||||
)
|
||||
mongodb_url: str = Field(
|
||||
default_factory=lambda: secret_map["cosmosdb_connection_string"]
|
||||
)
|
||||
|
||||
async def init_mongo_db(self):
|
||||
"""Este helper inicia la conexion enter el MongoDB ORM y nuestra instancia"""
|
||||
|
||||
from beanie import init_beanie
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
|
||||
from banortegpt.database.mongo_memory.models import Conversation
|
||||
|
||||
await init_beanie(
|
||||
database=AsyncIOMotorClient(self.mongodb_url).banortegptdos,
|
||||
document_models=[Conversation],
|
||||
)
|
||||
|
||||
|
||||
config = Settings()
|
||||
6
.templates/rag/{{project_name}}/api/context.py
Normal file
6
.templates/rag/{{project_name}}/api/context.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from contextvars import ContextVar
|
||||
|
||||
buffer: ContextVar[str] = ContextVar("buffer", default="")
|
||||
tool_buffer: ContextVar[str] = ContextVar("tool_buffer", default="")
|
||||
tool_id: ContextVar[str | None] = ContextVar("tool_id", default=None)
|
||||
tool_name: ContextVar[str | None] = ContextVar("tool_name", default=None)
|
||||
49
.templates/rag/{{project_name}}/api/server.py
Normal file
49
.templates/rag/{{project_name}}/api/server.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from api import services
|
||||
from api.agent import Agent
|
||||
from api.config import config
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
await config.init_mongo_db()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
agent = Agent()
|
||||
|
||||
|
||||
@app.post("/api/v1/conversation")
|
||||
async def create_conversation():
|
||||
conversation_id = uuid.uuid4()
|
||||
await services.create_conversation(conversation_id, agent.system_prompt)
|
||||
return {"conversation_id": conversation_id}
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
conversation_id: uuid.UUID
|
||||
prompt: str
|
||||
|
||||
@app.post("/api/v1/message")
|
||||
async def send(message: Message):
|
||||
def b64_sse(func):
|
||||
"""Este helper transforma un generador de strings a un generador del protocolo SSE"""
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
async for chunk in func(*args, **kwargs):
|
||||
content = chunk.model_dump_json()
|
||||
data = f"data: {content}\n\n"
|
||||
yield data
|
||||
|
||||
return wrapper
|
||||
|
||||
sse_stream = b64_sse(services.stream)
|
||||
generator = sse_stream(agent, message.prompt, message.conversation_id)
|
||||
return StreamingResponse(generator, media_type="text/event-stream")
|
||||
8
.templates/rag/{{project_name}}/api/services/__init__.py
Normal file
8
.templates/rag/{{project_name}}/api/services/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from banortegpt.database.mongo_memory.crud import create_conversation
|
||||
|
||||
from .stream_response import stream
|
||||
|
||||
__all__ = [
|
||||
"stream",
|
||||
"create_conversation",
|
||||
]
|
||||
@@ -0,0 +1,81 @@
|
||||
import json
|
||||
from enum import StrEnum
|
||||
from typing import TypeAlias
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
import api.context as ctx
|
||||
from api.agent import Agent
|
||||
from banortegpt.database.mongo_memory import crud
|
||||
|
||||
|
||||
class ChunkType(StrEnum):
|
||||
START = "start"
|
||||
TEXT = "text"
|
||||
REFERENCE = "reference"
|
||||
IMAGE = "image"
|
||||
TOOL = "tool"
|
||||
END = "end"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
ContentType: TypeAlias = str | int
|
||||
|
||||
|
||||
class ResponseChunk(BaseModel):
|
||||
type: ChunkType
|
||||
content: ContentType | list[ContentType] | None
|
||||
|
||||
|
||||
async def stream(agent: Agent, prompt: str, conversation_id: UUID):
|
||||
yield ResponseChunk(type=ChunkType.START, content="")
|
||||
|
||||
conversation = await crud.get_conversation(conversation_id)
|
||||
|
||||
if conversation is None:
|
||||
raise ValueError("Conversation not found")
|
||||
|
||||
conversation.add(role="user", content=prompt)
|
||||
|
||||
history = conversation.to_openai_format(agent.message_limit, langchain_compat=True)
|
||||
async for content in agent.stream(history):
|
||||
yield ResponseChunk(type=ChunkType.TEXT, content=content)
|
||||
|
||||
if (tool_id := ctx.tool_id.get()) is not None:
|
||||
tool_buffer = ctx.tool_buffer.get()
|
||||
assert tool_buffer is not None
|
||||
|
||||
tool_name = ctx.tool_name.get()
|
||||
assert tool_name is not None
|
||||
|
||||
yield ResponseChunk(type=ChunkType.TOOL, content=None)
|
||||
|
||||
buffer_dict = json.loads(tool_buffer)
|
||||
|
||||
result = await agent.tool_map[tool_name](**buffer_dict)
|
||||
|
||||
conversation.add(
|
||||
role="assistant",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": tool_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": tool_buffer,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
conversation.add(role="tool", content=result, tool_call_id=tool_id)
|
||||
|
||||
history = conversation.to_openai_format(agent.message_limit, langchain_compat=True)
|
||||
async for content in agent.stream(history, {"tools": None}):
|
||||
yield ResponseChunk(type=ChunkType.TEXT, content=content)
|
||||
|
||||
conversation.add(role="assistant", content=ctx.buffer.get())
|
||||
|
||||
await conversation.replace()
|
||||
|
||||
yield ResponseChunk(type=ChunkType.END, content="")
|
||||
Reference in New Issue
Block a user