diff --git a/Dockerfile b/Dockerfile index fd014bb..541de53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,21 +8,19 @@ WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ curl \ - libpq-dev \ protobuf-compiler \ && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir --upgrade pip + RUN pip install --no-cache-dir \ langchain==0.1.0 \ - langfuse>=2.0.0 \ - langgraph \ - langchain-openai \ + langchain-community==0.0.10 \ langchain-elasticsearch \ grpcio \ grpcio-tools \ - psycopg2-binary \ - pydantic + grpcio-reflection \ + pydantic COPY ./protos ./protos COPY . . @@ -34,5 +32,5 @@ RUN python -m grpc_tools.protoc \ ./protos/brunix.proto EXPOSE 50051 -#CMD ["tail", "-f", "/dev/null"] -CMD ["python", "src/server.py"] + +CMD ["python", "src/server.py"] \ No newline at end of file diff --git a/README.md b/README.md index 6107db0..e1d8eb3 100644 --- a/README.md +++ b/README.md @@ -14,35 +14,21 @@ The following diagram illustrates the interaction between the AVAP technology, t ```mermaid graph TD - subgraph Client_Layer [External Interface] - Client[External Services / UI] + subgraph Local_Dev [Laptop Ivar/Rafael] + BE[Brunix Assistance Engine] + KT[Kubectl Tunnel] end - subgraph Engine_Layer - BE[Brunix Assistance Engine] - LG[LangGraph Logic] - LC[LangChain Framework] - end - - subgraph Intelligence_Layer - LLM[Fine-tuned Model / OpenAI or other] - Prompt[Prompt Engineering] - end - - subgraph Data_Observability_Layer [System Support] - EDB[(Elasticsearch Vector DB)] - LF[Langfuse Observability] + subgraph Vultr_K8s_Cluster [Production - Vultr Cloud] + EDB[(Elasticsearch Vector DB - HDD)] PG[(Postgres - System Data)] + LF[Langfuse UI] end - Client -- gRPC:50052 --> BE - BE --> LG - LG --> LC - LC --> LLM - LLM --> Prompt - LC -- Semantic Search --> EDB - LC -- Tracing/Metrics --> LF - LF -- Persistence --> PG + BE -- localhost:9200/5432 --> KT + KT -- Secure Tunnel --> EDB + KT -- Secure Tunnel --> PG + BE -- Public IP --> LF ``` --- diff --git a/docker-compose.yaml b/docker-compose.yaml index 38112d0..da90d10 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,57 +7,15 @@ services: ports: - "50052:50051" environment: - - ELASTICSEARCH_URL=http://elasticsearch:9200 + - ELASTICSEARCH_URL=http://host.docker.internal:9200 + - DATABASE_URL=postgresql://postgres:brunix_pass@host.docker.internal:5432/postgres + + - LANGFUSE_HOST=http://45.77.119.180 - LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY} - LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY} - - LANGFUSE_HOST=http://langfuse:3000 - - OPENAI_API_KEY=${OPENAI_API_KEY} # O el proveedor que elija Ivar - depends_on: - - elasticsearch - - langfuse - networks: - - avap-network + - LLM_BASE_URL=http://host.docker.internal:11434 + - OPENAI_API_KEY=${OPENAI_API_KEY} + + extra_hosts: + - "host.docker.internal:host-gateway" - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0 - container_name: brunix-vector-db - environment: - - discovery.type=single-node - - xpack.security.enabled=false - - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - ports: - - "9200:9200" - networks: - - avap-network - - langfuse: - image: langfuse/langfuse:2.33.0 - container_name: brunix-observability - ports: - - "3000:3000" - environment: - - DATABASE_URL=postgresql://postgres:brunix_pass@langfuse-db:5432/postgres - - NEXTAUTH_URL=http://localhost:3000 - - NEXTAUTH_SECRET=my_ultra_secret - - SALT=my_salt - depends_on: - - langfuse-db - networks: - - avap-network - - langfuse-db: - image: postgres:15 - container_name: brunix-postgres - environment: - - POSTGRES_PASSWORD=brunix_pass - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - avap-network - -networks: - avap-network: - driver: bridge - -volumes: - postgres_data: diff --git a/src/server.py b/src/server.py index e522dd2..52452a1 100644 --- a/src/server.py +++ b/src/server.py @@ -1,70 +1,87 @@ import os import grpc -from concurrent import futures import logging - +from concurrent import futures +from grpc_reflection.v1alpha import reflection import brunix_pb2 import brunix_pb2_grpc -#from langfuse.callback import CallbackHandler # Descomentar cuando este configurado -#from langchain_openai import ChatOpenAI # Cambiar por el que corresponda -from langchain_core.messages import HumanMessage + +from langchain_community.llms import Ollama +from langchain_community.embeddings import OllamaEmbeddings +from langchain_elasticsearch import ElasticsearchStore +from langchain_core.prompts import ChatPromptTemplate logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("BrunixServer") +logger = logging.getLogger("brunix-engine") -class BrunixService(brunix_pb2_grpc.AssistanceEngineServicer): +class BrunixEngine(brunix_pb2_grpc.AssistanceEngineServicer): def __init__(self): - #self.langfuse_handler = CallbackHandler( # Descomentar cuando este configurado - # public_key=os.getenv("LANGFUSE_PUBLIC_KEY"), - # secret_key=os.getenv("LANGFUSE_SECRET_KEY"), - # host=os.getenv("LANGFUSE_HOST") - #) - # AQUI IMPLEMENTAR EL MODELO QUE CORRESPONDA - IVAR - #self.llm = ChatOpenAI( - # model="gpt-4-turbo-preview", - # temperature=0.2, - # streaming=True - #) - logger.info("Brunix Engine initializing.") - def AskAgent(self, request, context): # PLACEHOLDER DE FUNCIONAMIENTO - logger.info(f"Request received: {request.query}") + self.base_url = os.getenv("LLM_BASE_URL", "http://ollama-light-service:11434") + self.model_name = os.getenv("LLM_MODEL", "qwen2.5:1.5b") + + logger.info(f"Starting server") + self.llm = Ollama(base_url=self.base_url, model=self.model_name) + + self.embeddings = OllamaEmbeddings(base_url=self.base_url, model="nomic-embed-text") + + es_url = os.getenv("ELASTICSEARCH_URL", "http://elasticsearch:9200") + logger.info(f"ElasticSearch on: {es_url}") + + self.vector_store = ElasticsearchStore( + es_url=es_url, + index_name="avap_manuals", + embedding=self.embeddings + ) + + def AskAgent(self, request, context): + logger.info(f"request {request.session_id}): {request.query[:50]}.") try: - - config = {"callbacks": [self.langfuse_handler], "run_name": "Brunix_Query"} - - full_response_text = "" + context_text = "AVAP is a virtual programming language for API development." + # 4. Prompt Engineering + prompt = ChatPromptTemplate.from_template(""" + You are Brunix, the 101OBEX artificial intelligence for the AVAP Sphere platform. Respond in a professional manner. - chunks = [ - {"text": ""}, - ] + CONTEXT: + {context} - for chunk in chunks: + QUESTION: + {question} + """) + + chain = prompt | self.llm + + for chunk in chain.stream({"context": context_text, "question": request.query}): yield brunix_pb2.AgentResponse( - text=chunk["text"], - avap_code="", - node_id=chunk["node"], + text=str(chunk), + avap_code="AVAP-2026", is_final=False ) - yield brunix_pb2.AgentResponse(text="", is_final=True) + yield brunix_pb2.AgentResponse(text="", avap_code="", is_final=True) except Exception as e: - logger.error(f"AGENT ERROR: {str(e)}") - context.set_details(str(e)) - context.set_code(grpc.StatusCode.INTERNAL) + logger.error(f"Error in AskAgent: {str(e)}") + yield brunix_pb2.AgentResponse(text=f"[Error Motor]: {str(e)}", is_final=True) def serve(): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - brunix_pb2_grpc.add_AssistanceEngineServicer_to_server(BrunixService(), server) + brunix_pb2_grpc.add_AssistanceEngineServicer_to_server(BrunixEngine(), server) + + SERVICE_NAMES = ( + brunix_pb2.DESCRIPTOR.services_by_name['AssistanceEngine'].full_name, + reflection.SERVICE_NAME, + ) + reflection.enable_server_reflection(SERVICE_NAMES, server) + server.add_insecure_port('[::]:50051') - logger.info("Brunix gRPC Server listen on port 50051") - + logger.info("Brunix Engine on port 50051") server.start() server.wait_for_termination() if __name__ == '__main__': - serve() + serve() \ No newline at end of file