From a5952c1a4d86805d76d635128f1df8ddc69ff20a Mon Sep 17 00:00:00 2001 From: pseco Date: Mon, 2 Mar 2026 12:41:27 +0100 Subject: [PATCH] working on agent in docker --- Docker/docker-compose.yaml | 2 +- Docker/requirements.txt | 177 +++++++++++++++++++++++++++++++++---- Docker/src/emb_factory.py | 67 ++++++++++++++ Docker/src/llm_factory.py | 72 +++++++++++++++ Docker/src/server copy.py | 140 +++++++++++++++++++++++++++++ Docker/src/server.py | 58 ++++++++---- scripts/start-tunnels.sh | 6 +- 7 files changed, 485 insertions(+), 37 deletions(-) create mode 100644 Docker/src/emb_factory.py create mode 100644 Docker/src/llm_factory.py create mode 100644 Docker/src/server copy.py diff --git a/Docker/docker-compose.yaml b/Docker/docker-compose.yaml index 3f4c0cb..8099823 100644 --- a/Docker/docker-compose.yaml +++ b/Docker/docker-compose.yaml @@ -9,7 +9,7 @@ services: environment: ELASTICSEARCH_URL: ${ELASTICSEARCH_URL} DATABASE_URL: ${DATABASE_URL} - LLM_BASE_URL: ${LLM_BASE_URL} + OLLAMA_URL: ${OLLAMA_URL} LANGFUSE_HOST: ${LANGFUSE_HOST} LANGFUSE_PUBLIC_KEY: ${LANGFUSE_PUBLIC_KEY} LANGFUSE_SECRET_KEY: ${LANGFUSE_SECRET_KEY} diff --git a/Docker/requirements.txt b/Docker/requirements.txt index 872b430..0091a18 100644 --- a/Docker/requirements.txt +++ b/Docker/requirements.txt @@ -1,5 +1,7 @@ # This file was autogenerated by uv via the following command: -# uv export --format requirements-txt --no-hashes --no-dev -o requirements.txt +# uv export --format requirements-txt --no-hashes --no-dev -o Docker/requirements.txt +accelerate==1.12.0 + # via assistance-engine aiohappyeyeballs==2.6.1 # via aiohttp aiohttp==3.13.3 @@ -12,6 +14,12 @@ anyio==4.12.1 # via httpx attrs==25.4.0 # via aiohttp +boto3==1.42.58 + # via langchain-aws +botocore==1.42.58 + # via + # boto3 + # s3transfer certifi==2026.1.4 # via # elastic-transport @@ -20,115 +28,211 @@ certifi==2026.1.4 # requests charset-normalizer==3.4.4 # via requests +click==8.3.1 + # via nltk colorama==0.4.6 ; sys_platform == 'win32' # via + # click # loguru # tqdm +cuda-bindings==12.9.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +cuda-pathfinder==1.3.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via cuda-bindings dataclasses-json==0.6.7 # via langchain-community elastic-transport==8.17.1 # via elasticsearch elasticsearch==8.19.3 # via langchain-elasticsearch +filelock==3.24.3 + # via + # huggingface-hub + # torch frozenlist==1.8.0 # via # aiohttp # aiosignal -greenlet==3.3.1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' +fsspec==2025.10.0 + # via + # huggingface-hub + # torch +greenlet==3.3.2 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' # via sqlalchemy -grpcio==1.78.0 +grpcio==1.78.1 # via # assistance-engine # grpcio-reflection # grpcio-tools -grpcio-reflection==1.78.0 +grpcio-reflection==1.78.1 # via assistance-engine -grpcio-tools==1.78.0 +grpcio-tools==1.78.1 # via assistance-engine h11==0.16.0 # via httpcore +hf-xet==1.3.0 ; platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' + # via huggingface-hub httpcore==1.0.9 # via httpx httpx==0.28.1 # via # langgraph-sdk # langsmith + # ollama httpx-sse==0.4.3 # via langchain-community +huggingface-hub==0.36.2 + # via + # accelerate + # langchain-huggingface + # tokenizers idna==3.11 # via # anyio # httpx # requests # yarl +jinja2==3.1.6 + # via torch +jmespath==1.1.0 + # via + # boto3 + # botocore +joblib==1.5.3 + # via nltk jsonpatch==1.33 # via langchain-core jsonpointer==3.0.0 # via jsonpatch langchain==1.2.10 # via assistance-engine +langchain-aws==1.3.1 + # via assistance-engine langchain-classic==1.0.1 # via langchain-community langchain-community==0.4.1 # via assistance-engine -langchain-core==1.2.11 +langchain-core==1.2.15 # via # langchain + # langchain-aws # langchain-classic # langchain-community # langchain-elasticsearch + # langchain-huggingface + # langchain-ollama # langchain-text-splitters # langgraph # langgraph-checkpoint # langgraph-prebuilt langchain-elasticsearch==1.0.0 # via assistance-engine -langchain-text-splitters==1.1.0 +langchain-huggingface==1.2.0 + # via assistance-engine +langchain-ollama==1.0.1 + # via assistance-engine +langchain-text-splitters==1.1.1 # via langchain-classic -langgraph==1.0.8 +langgraph==1.0.9 # via langchain langgraph-checkpoint==4.0.0 # via # langgraph # langgraph-prebuilt -langgraph-prebuilt==1.0.7 +langgraph-prebuilt==1.0.8 # via langgraph -langgraph-sdk==0.3.5 +langgraph-sdk==0.3.8 # via langgraph -langsmith==0.7.1 +langsmith==0.7.6 # via # langchain-classic # langchain-community # langchain-core loguru==0.7.3 # via assistance-engine +markupsafe==3.0.3 + # via jinja2 marshmallow==3.26.2 # via dataclasses-json +mpmath==1.3.0 + # via sympy multidict==6.7.1 # via # aiohttp # yarl mypy-extensions==1.1.0 # via typing-inspect +networkx==3.6.1 + # via torch +nltk==3.9.3 + # via assistance-engine numpy==2.4.2 # via + # accelerate # assistance-engine # elasticsearch + # langchain-aws # langchain-community # pandas + # torchvision +nvidia-cublas-cu12==12.8.4.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via + # nvidia-cudnn-cu12 + # nvidia-cusolver-cu12 + # torch +nvidia-cuda-cupti-cu12==12.8.90 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-cuda-nvrtc-cu12==12.8.93 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-cuda-runtime-cu12==12.8.90 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-cudnn-cu12==9.10.2.21 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-cufft-cu12==11.3.3.83 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-cufile-cu12==1.13.1.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-curand-cu12==10.3.9.90 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-cusolver-cu12==11.7.3.90 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-cusparse-cu12==12.5.8.93 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via + # nvidia-cusolver-cu12 + # torch +nvidia-cusparselt-cu12==0.7.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-nccl-cu12==2.27.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-nvjitlink-cu12==12.8.93 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via + # nvidia-cufft-cu12 + # nvidia-cusolver-cu12 + # nvidia-cusparse-cu12 + # torch +nvidia-nvshmem-cu12==3.4.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +nvidia-nvtx-cu12==12.8.90 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch +ollama==0.6.1 + # via langchain-ollama orjson==3.11.7 # via # langgraph-sdk # langsmith ormsgpack==1.12.2 # via langgraph-checkpoint -packaging==26.0 +packaging==24.2 # via + # accelerate + # huggingface-hub # langchain-core # langsmith # marshmallow -pandas==3.0.0 +pandas==3.0.1 # via assistance-engine +pillow==12.1.1 + # via torchvision propcache==0.4.1 # via # aiohttp @@ -137,20 +241,25 @@ protobuf==6.33.5 # via # grpcio-reflection # grpcio-tools +psutil==7.2.2 + # via accelerate pydantic==2.12.5 # via # langchain + # langchain-aws # langchain-classic # langchain-core # langgraph # langsmith + # ollama # pydantic-settings pydantic-core==2.41.5 # via pydantic -pydantic-settings==2.12.0 +pydantic-settings==2.13.1 # via langchain-community python-dateutil==2.9.0.post0 # via + # botocore # elasticsearch # pandas python-dotenv==1.2.1 @@ -159,20 +268,33 @@ python-dotenv==1.2.1 # pydantic-settings pyyaml==6.0.3 # via + # accelerate + # huggingface-hub # langchain-classic # langchain-community # langchain-core +rapidfuzz==3.14.3 + # via assistance-engine +regex==2026.2.19 + # via nltk requests==2.32.5 # via + # huggingface-hub # langchain-classic # langchain-community # langsmith # requests-toolbelt requests-toolbelt==1.0.0 # via langsmith +s3transfer==0.16.0 + # via boto3 +safetensors==0.7.0 + # via accelerate setuptools==82.0.0 - # via grpcio-tools -simsimd==6.5.12 + # via + # grpcio-tools + # torch +simsimd==6.5.13 # via elasticsearch six==1.17.0 # via python-dateutil @@ -180,22 +302,40 @@ sqlalchemy==2.0.46 # via # langchain-classic # langchain-community +sympy==1.14.0 + # via torch tenacity==9.1.4 # via # langchain-community # langchain-core -tqdm==4.67.3 +tokenizers==0.22.2 + # via langchain-huggingface +torch==2.10.0 + # via + # accelerate + # assistance-engine + # torchvision +torchvision==0.25.0 # via assistance-engine +tqdm==4.67.3 + # via + # assistance-engine + # huggingface-hub + # nltk +triton==3.6.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' + # via torch typing-extensions==4.15.0 # via # aiosignal # anyio # elasticsearch # grpcio + # huggingface-hub # langchain-core # pydantic # pydantic-core # sqlalchemy + # torch # typing-inspect # typing-inspection typing-inspect==0.9.0 @@ -208,9 +348,10 @@ tzdata==2025.3 ; sys_platform == 'emscripten' or sys_platform == 'win32' # via pandas urllib3==2.6.3 # via + # botocore # elastic-transport # requests -uuid-utils==0.14.0 +uuid-utils==0.14.1 # via # langchain-core # langsmith diff --git a/Docker/src/emb_factory.py b/Docker/src/emb_factory.py new file mode 100644 index 0000000..d9fb9de --- /dev/null +++ b/Docker/src/emb_factory.py @@ -0,0 +1,67 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict + + +class BaseEmbeddingFactory(ABC): + @abstractmethod + def create(self, model: str, **kwargs: Any): + raise NotImplementedError + + +class OpenAIEmbeddingFactory(BaseEmbeddingFactory): + def create(self, model: str, **kwargs: Any): + from langchain_openai import OpenAIEmbeddings + + return OpenAIEmbeddings(model=model, **kwargs) + + +class OllamaEmbeddingFactory(BaseEmbeddingFactory): + def create(self, model: str, **kwargs: Any): + from langchain_ollama import OllamaEmbeddings + + return OllamaEmbeddings(model=model, **kwargs) + + +class BedrockEmbeddingFactory(BaseEmbeddingFactory): + def create(self, model: str, **kwargs: Any): + from langchain_aws import BedrockEmbeddings + + return BedrockEmbeddings(model_id=model, **kwargs) + + +class HuggingFaceEmbeddingFactory(BaseEmbeddingFactory): + def create(self, model: str, **kwargs: Any): + from langchain_huggingface import HuggingFaceEmbeddings + + return HuggingFaceEmbeddings(model_name=model, **kwargs) + + +EMBEDDING_FACTORIES: Dict[str, BaseEmbeddingFactory] = { + "openai": OpenAIEmbeddingFactory(), + "ollama": OllamaEmbeddingFactory(), + "bedrock": BedrockEmbeddingFactory(), + "huggingface": HuggingFaceEmbeddingFactory(), +} + + +def create_embedding_model(provider: str, model: str, **kwargs: Any): + """ + Create an embedding model instance for the given provider. + + Args: + provider: The provider name (openai, ollama, bedrock, huggingface). + model: The model identifier. + **kwargs: Additional keyword arguments passed to the model constructor. + + Returns: + An embedding model instance. + """ + key = provider.strip().lower() + + if key not in EMBEDDING_FACTORIES: + raise ValueError( + f"Unsupported embedding provider: {provider}. " + f"Available providers: {list(EMBEDDING_FACTORIES.keys())}" + ) + + return EMBEDDING_FACTORIES[key].create(model=model, **kwargs) diff --git a/Docker/src/llm_factory.py b/Docker/src/llm_factory.py new file mode 100644 index 0000000..8b1c13c --- /dev/null +++ b/Docker/src/llm_factory.py @@ -0,0 +1,72 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict + + +class BaseProviderFactory(ABC): + @abstractmethod + def create(self, model: str, **kwargs: Any): + raise NotImplementedError + + +class OpenAIChatFactory(BaseProviderFactory): + def create(self, model: str, **kwargs: Any): + from langchain_openai import ChatOpenAI + + return ChatOpenAI(model=model, **kwargs) + + +class OllamaChatFactory(BaseProviderFactory): + def create(self, model: str, **kwargs: Any): + from langchain_ollama import ChatOllama + + return ChatOllama(model=model, **kwargs) + + +class BedrockChatFactory(BaseProviderFactory): + def create(self, model: str, **kwargs: Any): + from langchain_aws import ChatBedrockConverse + + return ChatBedrockConverse(model=model, **kwargs) + + +class HuggingFaceChatFactory(BaseProviderFactory): + def create(self, model: str, **kwargs: Any): + from langchain_huggingface import ChatHuggingFace, HuggingFacePipeline + + llm = HuggingFacePipeline.from_model_id( + model_id=model, + task="text-generation", + pipeline_kwargs=kwargs, + ) + return ChatHuggingFace(llm=llm) + + +CHAT_FACTORIES: Dict[str, BaseProviderFactory] = { + "openai": OpenAIChatFactory(), + "ollama": OllamaChatFactory(), + "bedrock": BedrockChatFactory(), + "huggingface": HuggingFaceChatFactory(), +} + + +def create_chat_model(provider: str, model: str, **kwargs: Any): + """ + Create a chat model instance for the given provider. + + Args: + provider: The provider name (openai, ollama, bedrock, huggingface). + model: The model identifier. + **kwargs: Additional keyword arguments passed to the model constructor. + + Returns: + A chat model instance. + """ + key = provider.strip().lower() + + if key not in CHAT_FACTORIES: + raise ValueError( + f"Unsupported chat provider: {provider}. " + f"Available providers: {list(CHAT_FACTORIES.keys())}" + ) + + return CHAT_FACTORIES[key].create(model=model, **kwargs) diff --git a/Docker/src/server copy.py b/Docker/src/server copy.py new file mode 100644 index 0000000..5679ef6 --- /dev/null +++ b/Docker/src/server copy.py @@ -0,0 +1,140 @@ +import logging +import os +import sys +from concurrent import futures +from pathlib import Path +from typing import Any + +import brunix_pb2 +import brunix_pb2_grpc +import grpc +from grpc_reflection.v1alpha import reflection +from langchain_core.prompts import ChatPromptTemplate +from langchain_elasticsearch import ElasticsearchStore + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("brunix-engine") + + +def _provider_kwargs(provider: str, base_url: str) -> dict[str, Any]: + if provider == "ollama": + return {"base_url": base_url} + return {} + + +class BrunixEngine(brunix_pb2_grpc.AssistanceEngineServicer): + def __init__(self): + from src.emb_factory import create_embedding_model + from src.llm_factory import create_chat_model + + self.base_url = os.getenv("OLLAMA_LOCAL_URL", "http://ollama-light-service:11434") + self.chat_provider = os.getenv("CHAT_PROVIDER", "ollama") + self.embedding_provider = os.getenv("EMBEDDING_PROVIDER", self.chat_provider) + self.chat_model_name = os.getenv("OLLAMA_MODEL_NAME") + self.embedding_model_name = os.getenv( + "OLLAMA_EMB_MODEL_NAME", self.chat_model_name + ) + + if not self.chat_model_name: + raise ValueError("OLLAMA_MODEL_NAME is required") + + logger.info("Starting server") + + self.llm = create_chat_model( + provider=self.chat_provider, + model=self.chat_model_name, + **_provider_kwargs(self.chat_provider, self.base_url), + ) + + self.embeddings = create_embedding_model( + provider=self.embedding_provider, + model=self.embedding_model_name, + **_provider_kwargs(self.embedding_provider, self.base_url), + ) + + es_url = os.getenv("ELASTICSEARCH_URL", "http://elasticsearch:9200") + logger.info("ElasticSearch on: %s", es_url) + + self.vector_store = ElasticsearchStore( + es_url=es_url, + index_name=os.getenv("ELASTICSEARCH_INDEX"), + embedding=self.embeddings, + query_field="text", + vector_query_field="embedding", + ) + + def format_context(self, docs) -> str: + parts = [] + for i, d in enumerate(docs, start=1): + meta = d.metadata or {} + source = meta.get("source", "unknown") + doc_id = meta.get("doc_id", "unknown") + chunk_id = meta.get("chunk_id", "unknown") + + parts.append( + f"[{i}] source={source} doc_id={doc_id} chunk_id={chunk_id}\n{d.page_content}" + ) + return "\n\n---\n\n".join(parts) + + def AskAgent(self, request, context): + logger.info(f"request {request.session_id}): {request.query[:50]}.") + + docs_and_scores = self.vector_store.similarity_search_with_score( + request.query, k=4 + ) + + try: + context_text = self.format_context([doc for doc, _ in docs_and_scores]) + # 4. Prompt Engineering + prompt = ChatPromptTemplate.from_template(""" + You are a helpful assistant. Use the following retrieved documents to answer the question. + If you don't know the answer, say you don't know. + + CONTEXT: + {context} + + QUESTION: + {question} + """) + + chain = prompt | self.llm + + result = chain.invoke({"context": context_text, "question": request.query}) + result_text = getattr(result, "content", str(result)) + yield brunix_pb2.AgentResponse( + text=str(result_text), avap_code="AVAP-2026", is_final=True + ) + + yield brunix_pb2.AgentResponse(text="", avap_code="", is_final=True) + + except Exception as e: + 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(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 Engine on port 50051") + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + serve() diff --git a/Docker/src/server.py b/Docker/src/server.py index 1dc76af..7f4e634 100644 --- a/Docker/src/server.py +++ b/Docker/src/server.py @@ -1,36 +1,63 @@ -import os -import grpc import logging +import os +import sys from concurrent import futures -from grpc_reflection.v1alpha import reflection +from pathlib import Path +from typing import Any + import brunix_pb2 import brunix_pb2_grpc - -from langchain_community.llms import Ollama -from langchain_community.embeddings import OllamaEmbeddings -from langchain_elasticsearch import ElasticsearchStore +import grpc +from grpc_reflection.v1alpha import reflection from langchain_core.prompts import ChatPromptTemplate +from langchain_elasticsearch import ElasticsearchStore + +# PROJECT_ROOT = Path(__file__).resolve().parents[2] +# if str(PROJECT_ROOT) not in sys.path: +# sys.path.insert(0, str(PROJECT_ROOT)) logging.basicConfig(level=logging.INFO) logger = logging.getLogger("brunix-engine") +def _provider_kwargs(provider: str, base_url: str) -> dict[str, Any]: + if provider == "ollama": + return {"base_url": base_url} + return {} + + class BrunixEngine(brunix_pb2_grpc.AssistanceEngineServicer): def __init__(self): + from emb_factory import create_embedding_model + from llm_factory import create_chat_model - self.base_url = os.getenv("LLM_BASE_URL", "http://ollama-light-service:11434") - self.model_name = os.getenv("OLLAMA_MODEL_NAME") + self.base_url = os.getenv("OLLAMA_LOCAL_URL", "http://ollama-light-service:11434") + self.chat_provider = os.getenv("CHAT_PROVIDER", "ollama") + self.embedding_provider = os.getenv("EMBEDDING_PROVIDER", self.chat_provider) + self.chat_model_name = os.getenv("OLLAMA_MODEL_NAME") + self.embedding_model_name = os.getenv( + "OLLAMA_EMB_MODEL_NAME", self.chat_model_name + ) - logger.info(f"Starting server") + if not self.chat_model_name: + raise ValueError("OLLAMA_MODEL_NAME is required") - self.llm = Ollama(base_url=self.base_url, model=self.model_name) + logger.info("Starting server") - self.embeddings = OllamaEmbeddings( - base_url=self.base_url, model=self.model_name + self.llm = create_chat_model( + provider=self.chat_provider, + model=self.chat_model_name, + **_provider_kwargs(self.chat_provider, self.base_url), + ) + + self.embeddings = create_embedding_model( + provider=self.embedding_provider, + model=self.embedding_model_name, + **_provider_kwargs(self.embedding_provider, self.base_url), ) es_url = os.getenv("ELASTICSEARCH_URL", "http://elasticsearch:9200") - logger.info(f"ElasticSearch on: {es_url}") + logger.info("ElasticSearch on: %s", es_url) self.vector_store = ElasticsearchStore( es_url=es_url, @@ -77,8 +104,9 @@ class BrunixEngine(brunix_pb2_grpc.AssistanceEngineServicer): chain = prompt | self.llm result = chain.invoke({"context": context_text, "question": request.query}) + result_text = getattr(result, "content", str(result)) yield brunix_pb2.AgentResponse( - text=str(result), avap_code="AVAP-2026", is_final=True + text=str(result_text), avap_code="AVAP-2026", is_final=True ) yield brunix_pb2.AgentResponse(text="", avap_code="", is_final=True) diff --git a/scripts/start-tunnels.sh b/scripts/start-tunnels.sh index 6538fec..aee1645 100755 --- a/scripts/start-tunnels.sh +++ b/scripts/start-tunnels.sh @@ -22,9 +22,9 @@ if [ ! -f "$KUBECONFIG_PATH" ]; then fi # 1. AI Model Tunnel (Ollama) -echo -e "${YELLOW}[1/3]${NC} Starting Ollama Light Service tunnel (localhost:11434)..." -kubectl port-forward --address 0.0.0.0 svc/ollama-light-service 11434:11434 -n brunix --kubeconfig "$KUBECONFIG_PATH" & -OLLAMA_PID=$! +# echo -e "${YELLOW}[1/3]${NC} Starting Ollama Light Service tunnel (localhost:11434)..." +# kubectl port-forward --address 0.0.0.0 svc/ollama-light-service 11434:11434 -n brunix --kubeconfig "$KUBECONFIG_PATH" & +# OLLAMA_PID=$! # 2. Knowledge Base Tunnel (Elasticsearch) echo -e "${YELLOW}[2/3]${NC} Starting Elasticsearch Vector DB tunnel (localhost:9200)..."