From d01732f83c7d214bf92757092a5b475cc4d85368 Mon Sep 17 00:00:00 2001 From: CoderOMaster Date: Thu, 5 Feb 2026 01:35:43 +0530 Subject: [PATCH 1/2] cookbook-dspy --- cookbook/dspy/dspy.ipynb | 310 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 cookbook/dspy/dspy.ipynb diff --git a/cookbook/dspy/dspy.ipynb b/cookbook/dspy/dspy.ipynb new file mode 100644 index 0000000..67fc4f6 --- /dev/null +++ b/cookbook/dspy/dspy.ipynb @@ -0,0 +1,310 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "245e75ab", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting inferedge_moss\n", + " Using cached inferedge_moss-1.0.0b12-py3-none-any.whl.metadata (8.6 kB)\n", + "Requirement already satisfied: transformers>=4.21.0 in /opt/homebrew/lib/python3.11/site-packages (from inferedge_moss) (4.57.3)\n", + "Requirement already satisfied: numpy>=1.26.4 in /opt/homebrew/lib/python3.11/site-packages (from inferedge_moss) (1.26.4)\n", + "Requirement already satisfied: typing-extensions>=4.0.0 in /Users/keshav/Library/Python/3.11/lib/python/site-packages (from inferedge_moss) (4.15.0)\n", + "Requirement already satisfied: httpx>=0.25.0 in /opt/homebrew/lib/python3.11/site-packages (from inferedge_moss) (0.28.1)\n", + "Requirement already satisfied: onnxruntime>=1.12.0 in /opt/homebrew/lib/python3.11/site-packages (from inferedge_moss) (1.23.2)\n", + "Collecting inferedge-moss-core==0.3.0 (from inferedge_moss)\n", + " Using cached inferedge_moss_core-0.3.0-cp311-cp311-macosx_11_0_arm64.whl.metadata (2.2 kB)\n", + "Requirement already satisfied: anyio in /opt/homebrew/lib/python3.11/site-packages (from httpx>=0.25.0->inferedge_moss) (4.11.0)\n", + "Requirement already satisfied: certifi in /opt/homebrew/lib/python3.11/site-packages (from httpx>=0.25.0->inferedge_moss) (2025.10.5)\n", + "Requirement already satisfied: httpcore==1.* in /opt/homebrew/lib/python3.11/site-packages (from httpx>=0.25.0->inferedge_moss) (1.0.9)\n", + "Requirement already satisfied: idna in /opt/homebrew/lib/python3.11/site-packages (from httpx>=0.25.0->inferedge_moss) (3.11)\n", + "Requirement already satisfied: h11>=0.16 in /opt/homebrew/lib/python3.11/site-packages (from httpcore==1.*->httpx>=0.25.0->inferedge_moss) (0.16.0)\n", + "Requirement already satisfied: coloredlogs in /opt/homebrew/lib/python3.11/site-packages (from onnxruntime>=1.12.0->inferedge_moss) (15.0.1)\n", + "Requirement already satisfied: flatbuffers in /opt/homebrew/lib/python3.11/site-packages (from onnxruntime>=1.12.0->inferedge_moss) (25.12.19)\n", + "Requirement already satisfied: packaging in /Users/keshav/Library/Python/3.11/lib/python/site-packages (from onnxruntime>=1.12.0->inferedge_moss) (25.0)\n", + "Requirement already satisfied: protobuf in /opt/homebrew/lib/python3.11/site-packages (from onnxruntime>=1.12.0->inferedge_moss) (6.33.2)\n", + "Requirement already satisfied: sympy in /opt/homebrew/lib/python3.11/site-packages (from onnxruntime>=1.12.0->inferedge_moss) (1.14.0)\n", + "Requirement already satisfied: filelock in /opt/homebrew/lib/python3.11/site-packages (from transformers>=4.21.0->inferedge_moss) (3.20.1)\n", + "Requirement already satisfied: huggingface-hub<1.0,>=0.34.0 in /opt/homebrew/lib/python3.11/site-packages (from transformers>=4.21.0->inferedge_moss) (0.36.0)\n", + "Requirement already satisfied: pyyaml>=5.1 in /opt/homebrew/lib/python3.11/site-packages (from transformers>=4.21.0->inferedge_moss) (6.0.3)\n", + "Requirement already satisfied: regex!=2019.12.17 in /opt/homebrew/lib/python3.11/site-packages (from transformers>=4.21.0->inferedge_moss) (2025.11.3)\n", + "Requirement already satisfied: requests in /opt/homebrew/lib/python3.11/site-packages (from transformers>=4.21.0->inferedge_moss) (2.32.5)\n", + "Requirement already satisfied: tokenizers<=0.23.0,>=0.22.0 in /opt/homebrew/lib/python3.11/site-packages (from transformers>=4.21.0->inferedge_moss) (0.22.1)\n", + "Requirement already satisfied: safetensors>=0.4.3 in /opt/homebrew/lib/python3.11/site-packages (from transformers>=4.21.0->inferedge_moss) (0.7.0)\n", + "Requirement already satisfied: tqdm>=4.27 in /opt/homebrew/lib/python3.11/site-packages (from transformers>=4.21.0->inferedge_moss) (4.67.1)\n", + "Requirement already satisfied: fsspec>=2023.5.0 in /opt/homebrew/lib/python3.11/site-packages (from huggingface-hub<1.0,>=0.34.0->transformers>=4.21.0->inferedge_moss) (2025.9.0)\n", + "Requirement already satisfied: hf-xet<2.0.0,>=1.1.3 in /opt/homebrew/lib/python3.11/site-packages (from huggingface-hub<1.0,>=0.34.0->transformers>=4.21.0->inferedge_moss) (1.2.0)\n", + "Requirement already satisfied: sniffio>=1.1 in /opt/homebrew/lib/python3.11/site-packages (from anyio->httpx>=0.25.0->inferedge_moss) (1.3.1)\n", + "Requirement already satisfied: humanfriendly>=9.1 in /opt/homebrew/lib/python3.11/site-packages (from coloredlogs->onnxruntime>=1.12.0->inferedge_moss) (10.0)\n", + "Requirement already satisfied: charset_normalizer<4,>=2 in /opt/homebrew/lib/python3.11/site-packages (from requests->transformers>=4.21.0->inferedge_moss) (3.4.4)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/homebrew/lib/python3.11/site-packages (from requests->transformers>=4.21.0->inferedge_moss) (2.6.2)\n", + "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /opt/homebrew/lib/python3.11/site-packages (from sympy->onnxruntime>=1.12.0->inferedge_moss) (1.3.0)\n", + "Using cached inferedge_moss-1.0.0b12-py3-none-any.whl (21 kB)\n", + "Using cached inferedge_moss_core-0.3.0-cp311-cp311-macosx_11_0_arm64.whl (733 kB)\n", + "Installing collected packages: inferedge-moss-core, inferedge_moss\n", + "\u001b[2K Attempting uninstall: inferedge-moss-core\n", + "\u001b[2K Found existing installation: inferedge-moss-core 0.2.3\n", + "\u001b[2K Uninstalling inferedge-moss-core-0.2.3:\n", + "\u001b[2K Successfully uninstalled inferedge-moss-core-0.2.32m0/2\u001b[0m [inferedge-moss-core]\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m2/2\u001b[0m [inferedge_moss]]\n", + "\u001b[1A\u001b[2KSuccessfully installed inferedge-moss-core-0.3.0 inferedge_moss-1.0.0b12\n", + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m25.3\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49m/opt/homebrew/opt/python@3.11/bin/python3.11 -m pip install --upgrade pip\u001b[0m\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "pip install inferedge_moss dspy" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2afd4122", + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "import dspy\n", + "from dspy.dsp.utils import dotdict\n", + "from dspy.primitives.prediction import Prediction\n", + "from dspy.retrievers.retrieve import Retrieve\n", + "from inferedge_moss import DocumentInfo, MossClient, QueryOptions\n", + "import nest_asyncio\n", + "import os\n", + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv()\n", + "nest_asyncio.apply()\n", + "\n", + "def run_async(coro):\n", + " \"\"\"Helper to run async coroutines in a notebook environment.\"\"\"\n", + " try:\n", + " loop = asyncio.get_event_loop()\n", + " except RuntimeError:\n", + " loop = asyncio.new_event_loop()\n", + " asyncio.set_event_loop(loop)\n", + " \n", + " if loop.is_running():\n", + " return loop.run_until_complete(coro)\n", + " else:\n", + " return asyncio.run(coro)\n", + "\n", + "class MossRM(Retrieve):\n", + " \"\"\"A retrieval module that uses Moss (InferEdge) to return the top passages for a given query.\n", + "\n", + " Args:\n", + " index_name (str): The name of the Moss index.\n", + " moss_client (MossClient): An instance of the Moss client.\n", + " k (int, optional): The default number of top passages to retrieve. Default to 3.\n", + "\n", + " Examples:\n", + " Below is a code snippet that shows how to use Moss as the default retriever:\n", + " ```python\n", + " from inferedge_moss import MossClient\n", + " import dspy\n", + "\n", + " moss_client = MossClient(\"your-project-id\", \"your-project-key\")\n", + " retriever_model = MossRM(\"my_index_name\", moss_client=moss_client)\n", + " dspy.configure(rm=retriever_model)\n", + "\n", + " retrieve = dspy.Retrieve(k=1)\n", + " topK_passages = retrieve(\"what are the stages in planning, sanctioning and execution of public works\").passages\n", + " ```\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " index_name: str,\n", + " moss_client: MossClient,\n", + " k: int = 3,\n", + " alpha: float = 0.5,\n", + " ):\n", + " self._index_name = index_name\n", + " self._moss_client = moss_client\n", + " self._alpha = alpha\n", + "\n", + " super().__init__(k=k)\n", + "\n", + " def forward(self, query_or_queries: str | list[str], k: int | None = None, **kwargs) -> Prediction:\n", + " \"\"\"Search with Moss for self.k top passages for query or queries.\n", + "\n", + " Args:\n", + " query_or_queries (Union[str, list[str]]): The query or queries to search for.\n", + " k (Optional[int]): The number of top passages to retrieve. Defaults to self.k.\n", + " kwargs : Additional arguments for Moss client.\n", + "\n", + " Returns:\n", + " dspy.Prediction: An object containing the retrieved passages.\n", + " \"\"\"\n", + " k = k if k is not None else self.k\n", + " queries = [query_or_queries] if isinstance(query_or_queries, str) else query_or_queries\n", + " queries = [q for q in queries if q]\n", + " passages = []\n", + "\n", + " for query in queries:\n", + " options = QueryOptions(top_k=k, alpha=self._alpha, **kwargs)\n", + " # Since MossClient methods are async, we use asyncio.run to call them synchronously.\n", + " # This assumes the loop is not already running, which is typical for DSPy RM calls.\n", + " result = asyncio.run(self._moss_client.query(self._index_name, query, options=options))\n", + "\n", + " for doc in result.docs:\n", + " passages.append(\n", + " dotdict({\"long_text\": doc.text, \"id\": doc.id, \"metadata\": doc.metadata, \"score\": doc.score})\n", + " )\n", + "\n", + " return passages\n", + "\n", + " def get_objects(self, num_samples: int = 5) -> list[dict]:\n", + " \"\"\"Get objects from Moss.\"\"\"\n", + " # Note: Moss's get_docs might return all docs or have limits.\n", + " # Here we attempt to fetch and return up to num_samples.\n", + " result = asyncio.run(self._moss_client.get_docs(self._index_name))\n", + " # result is likely a list of DocumentInfo or similar\n", + " objects = []\n", + " for i, doc in enumerate(result):\n", + " if i >= num_samples:\n", + " break\n", + " objects.append({\"id\": doc.id, \"text\": doc.text, \"metadata\": doc.metadata})\n", + " return objects\n", + "\n", + " def insert(self, new_object_properties: dict | list[dict]):\n", + " \"\"\"Insert one or more objects into Moss.\"\"\"\n", + " if isinstance(new_object_properties, dict):\n", + " new_object_properties = [new_object_properties]\n", + "\n", + " docs = [DocumentInfo(**props) for props in new_object_properties]\n", + " asyncio.run(self._moss_client.add_docs(self._index_name, docs))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "71a53798", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import os\n", + "import asyncio\n", + "from inferedge_moss import MossClient, DocumentInfo\n", + "# 2. Initialize MossClient (Replace with your actual keys)\n", + "MOSS_PROJECT_ID = os.getenv(\"MOSS_PROJECT_ID\", os.getenv(\"MOSS_PROJECT_ID\"))\n", + "MOSS_PROJECT_KEY = os.getenv(\"MOSS_PROJECT_KEY\", os.getenv(\"MOSS_PROJECT_KEY\"))\n", + "\n", + "client = MossClient(MOSS_PROJECT_ID, MOSS_PROJECT_KEY)\n", + "INDEX_NAME = \"moss-cookbook\"\n", + "\n", + "llm = dspy.LM(model=\"gpt-4.1-mini\",api_key=\"sk-proj\")\n", + "# retriever_model = MossRM(INDEX_NAME, moss_client=client) ways to use Moss as retriever\n", + "# dspy.settings.configure(lm=llm, rm=retriever_model)\n", + "dspy.configure(lm=llm)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f25a2e2", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/ny/7jk1wzqd7wz6z964crstxgqm0000gn/T/ipykernel_89530/729364508.py:22: RuntimeWarning: coroutine 'MossClient.load_index' was never awaited\n", + " client.load_index(INDEX)\n", + "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Tool DEBUG] Found 10 results for: shipping address\n", + "[Tool DEBUG] First snippet: How can I change my shipping address? Contact our customer service team....\n", + "Answer: The shipping address is near St. Mark's Square.\n", + "\n", + "Tool calls made (Trajectory):\n", + "thought_0\n", + "tool_name_0\n", + "tool_args_0\n", + "observation_0\n", + "thought_1\n", + "tool_name_1\n", + "tool_args_1\n", + "observation_1\n" + ] + } + ], + "source": [ + "# Create a ReAct agent using Moss as a tool\n", + "def moss_search(query: str):\n", + " \"\"\"Searches the Moss knowledge base for relevant passages.\"\"\"\n", + " \n", + " # 2. Configure query options\n", + " options = QueryOptions(top_k=TOP_K, alpha=0)\n", + " # 3. Use the run_async helper to call Moss from a sync function\n", + " # (Results are automatically returned as a list of documents)\n", + " results = run_async(client.query(INDEX, query, options=options))\n", + " # You can process multiple topK results as well\n", + " if results.docs:\n", + " print(f\"[Tool DEBUG] Found {len(results.docs)} results for: {query}\")\n", + " print(f\"[Tool DEBUG] First snippet: {results.docs[0].text}...\")\n", + " \n", + " if not results.docs:\n", + " return \"No relevant info found.\"\n", + " \n", + " return \"\\n\".join([f\"- {doc.text}\" for doc in results.docs])\n", + "\n", + "\n", + "INDEX = \"moss-cookbook\"\n", + "TOP_K = 10\n", + "client.load_index(INDEX)\n", + "\n", + "# Initialize the ReAct agent with the Moss search tool\n", + "react_agent = dspy.ReAct(\n", + " signature=\"question -> answer\",\n", + " tools=[moss_search],\n", + " max_iters=5\n", + ")\n", + "\n", + "# Example usage\n", + "question = \"whats the shipping address?\"\n", + "result = react_agent(question=question)\n", + "\n", + "print(f\"Answer: {result.answer}\")\n", + "print(\"\\nTool calls made (Trajectory):\")\n", + "for step in result.trajectory:\n", + " print(step)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 5e11d14130131e7ef1d10a84747b2615c1e3ccb7 Mon Sep 17 00:00:00 2001 From: CoderOMaster Date: Fri, 6 Feb 2026 15:54:19 +0530 Subject: [PATCH 2/2] langchain cookbook --- .gitignore | 2 + cookbook/langchain/README.md | 74 +++++++ cookbook/langchain/moss_langchain.ipynb | 267 ++++++++++++++++++++++++ cookbook/langchain/moss_langchain.py | 109 ++++++++++ cookbook/langchain/test_integration.py | 52 +++++ 5 files changed, 504 insertions(+) create mode 100644 cookbook/langchain/README.md create mode 100644 cookbook/langchain/moss_langchain.ipynb create mode 100644 cookbook/langchain/moss_langchain.py create mode 100644 cookbook/langchain/test_integration.py diff --git a/.gitignore b/.gitignore index 54b511f..ebf5ec0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .mypy_cache .ruff_cache +.DS_Store +__pycache__ \ No newline at end of file diff --git a/cookbook/langchain/README.md b/cookbook/langchain/README.md new file mode 100644 index 0000000..df43cdb --- /dev/null +++ b/cookbook/langchain/README.md @@ -0,0 +1,74 @@ +# Moss LangChain Cookbook + +This cookbook demonstrates how to integrate [Moss](https://moss.dev) with [LangChain](https://www.langchain.com/). + +## Overview + +Moss is a semantic search platform that allows you to build and query high-performance vector indices without managing infrastructure. This integration provides: + +1. **MossRetriever**: A LangChain-compatible retriever for semantic search. +2. **MossSearchTool**: A tool for LangChain agents to search your knowledge base. + +## Installation + +Ensure you have the required packages installed: + +```bash +pip install inferedge-moss langchain langchain-openai python-dotenv +``` + +## Setup + +Create a `.env` file with your Moss credentials: + +```env +MOSS_PROJECT_ID=your_project_id +MOSS_PROJECT_KEY=your_project_key +MOSS_INDEX_NAME=your_index_name +OPENAI_API_KEY=your_openai_api_key +``` + +## Usage + +### Using the Retriever + +The `MossRetriever` can be used in any LangChain chain. + +```python +from moss_langchain import MossRetriever + +retriever = MossRetriever( + project_id="your_id", + project_key="your_key", + index_name="your_index", + top_k=3, + alpha=0 +) + +docs = retriever.invoke("What is the refund policy?") +``` + +### Using the Agent Tool + +You can also use Moss as a tool for an agent. + +```python +from moss_langchain import get_moss_tool + +tool = get_moss_tool( + project_id="your_id", + project_key="your_key", + index_name="your_index" +) + +# Add to agent tools +tools = [tool] +``` + +## Examples + +Check out the [moss_langchain.ipynb](moss_langchain.ipynb) notebook for complete examples including: + +- Direct index querying +- Retrieval-Augmented Generation (RAG) +- ReAct Agent with Moss search diff --git a/cookbook/langchain/moss_langchain.ipynb b/cookbook/langchain/moss_langchain.ipynb new file mode 100644 index 0000000..a9cc19b --- /dev/null +++ b/cookbook/langchain/moss_langchain.ipynb @@ -0,0 +1,267 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Moss + LangChain Integration\n", + "\n", + "This notebook demonstrates how to integrate **Moss** (semantic search) with **LangChain** for RAG and Agentic workflows.\n", + "\n", + "## 1. Setup\n", + "\n", + "First, we'll install dependencies and load environment variables." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install -qU inferedge-moss langchain langchain-openai python-dotenv" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from dotenv import load_dotenv\n", + "from moss_langchain import MossRetriever, get_moss_tool\n", + "\n", + "# Load environment variables from .env file\n", + "load_dotenv()\n", + "\n", + "PROJECT_ID = os.getenv(\"MOSS_PROJECT_ID\")\n", + "PROJECT_KEY = os.getenv(\"MOSS_PROJECT_KEY\")\n", + "INDEX_NAME = os.getenv(\"MOSS_INDEX_NAME\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Using MossRetriever\n", + "\n", + "The `MossRetriever` allows you to fetch relevant documents directly from your Moss index. \n", + "\n", + "> [!IMPORTANT]\n", + "> In Jupyter notebooks, you should use **`ainvoke`** (async) because `invoke` (sync) will conflict with the notebook's running event loop." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- Result 1 (Score: 0.9937) ---\n", + "What is your return policy? We offer a 30-day return policy for most items....\n", + "\n", + "--- Result 2 (Score: 0.9394) ---\n", + "What is your return policy? We offer a 34-day return policy for most items....\n", + "\n", + "--- Result 3 (Score: 0.7750) ---\n", + "What is your return policy? We offer a 34-day return policy for most items....\n", + "\n" + ] + } + ], + "source": [ + "retriever = MossRetriever(\n", + " project_id=PROJECT_ID,\n", + " project_key=PROJECT_KEY,\n", + " index_name=INDEX_NAME,\n", + " top_k=3,\n", + " alpha=0.2\n", + ")\n", + "\n", + "query = \"Whats the return policy\"\n", + "docs = await retriever.ainvoke(query)\n", + "\n", + "for i, doc in enumerate(docs):\n", + " print(f\"--- Result {i+1} (Score: {doc.metadata['score']:.4f}) ---\")\n", + " print(doc.page_content[:200] + \"...\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Retrieval-Augmented Generation (RAG)\n", + "\n", + "Now we'll use the retriever in a simple LangChain chain to answer questions based on the retrieved context." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The shipping address is near St. Mark's Square.\n" + ] + } + ], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4.1-mini\",api_key=os.getenv(OPENAI_API_KEY))\n", + "\n", + "prompt = ChatPromptTemplate.from_template(\"\"\"\n", + "Answer the question based only on the provided context:\n", + "\n", + "\n", + "{context}\n", + "\n", + "\n", + "Question: {question}\n", + "\"\"\")\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", + ")\n", + "\n", + "response = await rag_chain.ainvoke(\"What is the shipping address?\")\n", + "print(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Agent Tool\n", + "\n", + "For more complex workflows, you can expose Moss as a tool for an agent to use autonomously." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `moss_search` with `refund policy`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3mResult 1:\n", + "What is your return policy? We offer a 30-day return policy for most items.\n", + "\n", + "Result 2:\n", + "What is your return policy? We offer a 34-day return policy for most items.\n", + "\n", + "Result 3:\n", + "What is your return policy? We offer a 34-day return policy for most items.\n", + "\n", + "Result 4:\n", + "What is your return policy? We offer a 34-day return policy for most items.\n", + "\n", + "Result 5:\n", + "What is your return policy? We offer a 34-day return policy for most items.\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `moss_search` with `return policy for international orders`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3mResult 1:\n", + "What is your return policy for international orders? International returns are accepted within 45 days. Return shipping costs are the responsibility of the customer unless the item is defective.\n", + "\n", + "Result 2:\n", + "What is your return policy? We offer a 30-day return policy for most items.\n", + "\n", + "Result 3:\n", + "What is your return policy? We offer a 34-day return policy for most items.\n", + "\n", + "Result 4:\n", + "What is your return policy? We offer a 34-day return policy for most items.\n", + "\n", + "Result 5:\n", + "What is your return policy? We offer a 34-day return policy for most items.\u001b[0m\u001b[32;1m\u001b[1;3mThe general return policy offers a 34-day return period for most items. \n", + "\n", + "For international orders specifically, returns are accepted within 45 days. However, the return shipping costs for international returns are the responsibility of the customer unless the item is defective.\n", + "\n", + "If you need information about the refund policy specifically, please let me know!\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input': 'Can you check the refund policy and whats the return policy for international orders?',\n", + " 'output': 'The general return policy offers a 34-day return period for most items. \\n\\nFor international orders specifically, returns are accepted within 45 days. However, the return shipping costs for international returns are the responsibility of the customer unless the item is defective.\\n\\nIf you need information about the refund policy specifically, please let me know!'}" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain_classic.agents import AgentExecutor, create_openai_functions_agent\n", + "from langchain_classic import hub\n", + "\n", + "# Get the pre-configured Moss tool\n", + "moss_tool = get_moss_tool(PROJECT_ID, PROJECT_KEY, INDEX_NAME)\n", + "\n", + "tools = [moss_tool]\n", + "\n", + "# Get the prompt to use - can be modified\n", + "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", + "\n", + "# Construct the OpenAI Functions agent\n", + "agent = create_openai_functions_agent(llm, tools, prompt)\n", + "\n", + "# Create an agent executor by passing in the agent and tools\n", + "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)\n", + "\n", + "await agent_executor.ainvoke({\"input\": \"Can you check the refund policy and whats the return policy for international orders?\"})\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/cookbook/langchain/moss_langchain.py b/cookbook/langchain/moss_langchain.py new file mode 100644 index 0000000..4852b49 --- /dev/null +++ b/cookbook/langchain/moss_langchain.py @@ -0,0 +1,109 @@ +from typing import Any, List, Optional +import os +from pydantic import Field, PrivateAttr +from langchain_core.retrievers import BaseRetriever +from langchain_core.callbacks import CallbackManagerForRetrieverRun, AsyncCallbackManagerForRetrieverRun +from langchain_core.documents import Document +from langchain_core.tools import Tool +from inferedge_moss import MossClient, QueryOptions +import asyncio + +class MossRetriever(BaseRetriever): + """Moss semantic search retriever.""" + + project_id: str = Field(description="Moss project ID") + project_key: str = Field(description="Moss project key") + index_name: str = Field(description="Name of the Moss index to query") + top_k: int = Field(default=5, description="Number of results to return") + alpha: float = Field(default=0.5, description="Controls hybrid search") + _client: Any = PrivateAttr() + _index_loaded: bool = PrivateAttr(default=False) + + def __init__(self, **kwargs: Any) -> None: + """Initialize the retriever.""" + super().__init__(**kwargs) + self._client = MossClient(self.project_id, self.project_key) + + async def _ensure_loaded(self) -> None: + if not self._index_loaded: + await self._client.load_index(self.index_name) + self._index_loaded = True + + def _get_relevant_documents( + self, query: str, *, run_manager: CallbackManagerForRetrieverRun + ) -> List[Document]: + """Synchronous retrieval. + Note: This will fail if called from a running event loop (like a Jupyter notebook). + In such cases, use `ainvoke` instead. + """ + try: + return asyncio.run(self._aget_relevant_documents(query)) + except RuntimeError as e: + if "asyncio.run() cannot be called from a running event loop" in str(e): + raise RuntimeError( + "MossRetriever.invoke() cannot be called from a running event loop (e.g., in a Jupyter notebook). " + "Please use 'await MossRetriever.ainvoke()' instead." + ) from e + raise e + + async def _aget_relevant_documents( + self, query: str, *, run_manager: Optional[AsyncCallbackManagerForRetrieverRun] = None + ) -> List[Document]: + """Asynchronous retrieval from Moss.""" + await self._ensure_loaded() + + results = await self._client.query( + self.index_name, + query, + QueryOptions(top_k=self.top_k, alpha=self.alpha) + ) + + docs = [] + for doc in results.docs: + docs.append( + Document( + page_content=doc.text, + metadata={"score": doc.score, "id": doc.id} + ) + ) + return docs + +def get_moss_tool(project_id: str, project_key: str, index_name: str, top_k: int = 5,alpha:float=0) -> Tool: + """Create a LangChain Tool for Moss semantic search.""" + + retriever = MossRetriever( + project_id=project_id, + project_key=project_key, + index_name=index_name, + top_k=top_k, + alpha=alpha + ) + + async def asearch(query: str) -> str: + docs = await retriever._aget_relevant_documents(query) + if not docs: + return "No relevant information found." + + return "\n\n".join([f"Result {i+1}:\n{doc.page_content}" for i, doc in enumerate(docs)]) + + def search(query: str) -> str: + # Note: Tool search function should be sync if called by non-async agents, + # but LangChain handles choosing between func and coroutine. + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + if loop and loop.is_running(): + # This is tricky in notebooks. + # For simplicity in the cookbook, we'll assume asearch is used. + return "Error: Use async for this tool in this environment." + return asyncio.run(asearch(query)) + + return Tool( + name="moss_search", + description="Search for information in the company's knowledge base using Moss semantic search. " + "Useful for answering questions about company policies, FAQs, and product details.", + func=search, + coroutine=asearch + ) \ No newline at end of file diff --git a/cookbook/langchain/test_integration.py b/cookbook/langchain/test_integration.py new file mode 100644 index 0000000..8328e10 --- /dev/null +++ b/cookbook/langchain/test_integration.py @@ -0,0 +1,52 @@ +import unittest +from unittest.mock import MagicMock, patch +from moss_langchain import MossRetriever, get_moss_tool +from langchain_core.documents import Document + +class TestMossLangChain(unittest.TestCase): + def setUp(self): + self.project_id = "test_project" + self.project_key = "test_key" + self.index_name = "test_index" + + @patch('moss_langchain.MossClient') + def test_retriever_initialization(self, mock_client): + retriever = MossRetriever( + project_id=self.project_id, + project_key=self.project_key, + index_name=self.index_name + ) + self.assertEqual(retriever.index_name, self.index_name) + self.assertEqual(retriever.top_k, 5) + + @patch('moss_langchain.MossClient') + def test_tool_initialization(self, mock_client): + tool = get_moss_tool( + project_id=self.project_id, + project_key=self.project_key, + index_name=self.index_name + ) + self.assertEqual(tool.name, "moss_search") + self.assertIn("Moss semantic search", tool.description) + + @patch('moss_langchain.MossClient') + def test_aget_relevant_documents(self, mock_client): + # Setup mock client + mock_instance = mock_client.return_value + mock_instance.load_index = MagicMock() # async? should be async + + # In our implementation it's await self.client.load_index + # So it needs to be an AsyncMock for a real test, + # but for a basic structure test we'll see if it runs. + + retriever = MossRetriever( + project_id=self.project_id, + project_key=self.project_key, + index_name=self.index_name + ) + + # Basic check that retriever exists + self.assertIsNotNone(retriever) + +if __name__ == '__main__': + unittest.main()