<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[HeurisLabs - Medium]]></title>
        <description><![CDATA[Making Next Gen AI Integrations - Medium]]></description>
        <link>https://medium.com/heurislabs?source=rss----dce691425da1---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>HeurisLabs - Medium</title>
            <link>https://medium.com/heurislabs?source=rss----dce691425da1---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 03:07:58 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/heurislabs" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Building a SlackBot for workflow automation using Model Context Protocol(MCP)]]></title>
            <link>https://medium.com/heurislabs/configuring-slackbot-with-various-sources-using-mcp-3607b849a565?source=rss----dce691425da1---4</link>
            <guid isPermaLink="false">https://medium.com/p/3607b849a565</guid>
            <category><![CDATA[langgraph]]></category>
            <category><![CDATA[mcp-server]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[slack]]></category>
            <dc:creator><![CDATA[Atharva Gupta]]></dc:creator>
            <pubDate>Sat, 08 Mar 2025 06:27:09 GMT</pubDate>
            <atom:updated>2025-03-08T06:34:20.348Z</atom:updated>
            <content:encoded><![CDATA[<h3>Introduction</h3><p>Businesses rely on multiple tools to manage operations, from customer service to project management. What if you could bring all these services into a single conversational interface? This blog explores how we built a <strong>Slack bot</strong> that connects with <strong>Model Context Protocol(MCP)</strong>, allowing it to interact with various services be it GitHub, Jira or Stripe all at the same time. By leveraging <strong>LangGraph</strong>, we enable seamless processing and decision-making, making our bot truly intelligent. This blog will cover the motivation behind this project, the architecture of our solution, the technical implementation, challenges faced, and future improvements.</p><h3>MCP (Model Context Protocol)</h3><p>Model Content Protocol (MCP) is a standardized communication framework that allows AI-driven applications to interact with multiple external services, tools, and APIs in a structured manner. MCP enables seamless integration by defining a common interface for different systems, making it easier to retrieve, process, and share data across platforms.</p><p>In our case, MCP serves as the intermediary layer that allows our Slack bot to interact with various third-party services, such as GitHub, Jira, and Stripe. By leveraging MCP, we eliminate the need for hard-coded integrations, making the system highly scalable and adaptable.</p><h3>Understanding the Architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QnxlHQbBBiVlu4a3Vlia1g.png" /></figure><h3>1. Slack App</h3><p>The Slack App acts as the interface through which users communicate with our system. When a user sends a message, such as “@bot check GitHub issue #123”, Slack captures this interaction and forwards it to our <strong>Modal Server</strong>. The Slack App enables seamless interaction within Slack, ensuring users can issue commands in natural language without leaving their workspace.</p><h3>2. Modal Server (Event Processor)</h3><p>The Modal Server serves as the event processor for incoming Slack messages. It listens to and handles Slack events, extracting the key information from user messages. Once it processes a request, it packages the event into a structured chat message and forwards it to the <strong>LangGraph Server</strong> for further decision-making. This component ensures that every user interaction is translated into a format that the system can process efficiently.</p><h3>3. LangGraph Server</h3><p>The LangGraph Server functions as the central decision-making unit of the system. Upon receiving a request from the Modal Server, it determines the appropriate course of action by analyzing the user’s intent. It decides whether an MCP server is required, selects the appropriate tool or API to call, and manages the overall workflow. By using the <strong>MCP Adapter</strong>, the LangGraph Server facilitates communication with external MCP services, ensuring accurate responses are generated for user queries.</p><h3>4. MCP Adapter</h3><p>The MCP Adapter is a crucial intermediary that allows the LangGraph Server to interface with multiple MCP servers. It acts as a bridge between our bot and external services. This component dynamically connects to the required MCPs, retrieves the necessary data, and sends it back to the LangGraph Server for processing. This abstraction layer eliminates the need for direct integrations with individual services, making the system scalable and adaptable.</p><h3>5. Response Handling</h3><p>Once the LangGraph Server, with the help of the MCP Adapter, has processed the request, the final response is sent back to Slack. The Slack App receives this response and posts it in the appropriate conversation thread. For instance, if a user requested a GitHub issue update, they would receive a response like, “GitHub Issue #123 is assigned to John Doe.” This step ensures that users receive clear, actionable responses directly within their Slack workspace, improving efficiency and collaboration.</p><h3>Implementing the Slack Bot</h3><h3>Step 1: Setting Up the Slack App</h3><p>To interact with Slack, we created a Slack App and configured it to listen for messages.</p><ul><li>Enabled <strong>Event Subscriptions</strong>.</li><li>Defined a webhook endpoint /events/slack.</li><li>Configured bot permissions to <strong>read and post messages</strong>.</li></ul><h3>Step 2: Building the Modal Server</h3><p>The Modal Server is a <strong>FastAPI</strong> application that listens to Slack events and processes messages before sending them to LangGraph.</p><pre>@app.post(&quot;/events/slack&quot;)<br>async def slack_endpoint(req: Request):<br>    return await APP_HANDLER.handle(req)</pre><p>This ensures that messages mentioning the bot (e.g., “@bot check Jira”) are sent for processing.</p><h3>Step 3: Processing Requests in LangGraph</h3><p>LangGraph serves as the decision-making engine. It:</p><ul><li>Determines if the bot needs to call an MCP server.</li><li>Routes the request to the appropriate tool.</li><li>Uses AI-based reasoning to generate a response.</li></ul><p>Example logic for handling Slack messages:</p><pre>async def route(state: State, *, config: RunnableConfig) -&gt; dict[str, list[BaseMessage]]:<br>    prompt = ChatPromptTemplate.from_messages([<br>        (&quot;system&quot;, &quot;Routing user query to appropriate service.&quot;),<br>        (&quot;placeholder&quot;, &quot;{messages}&quot;),<br>    ])<br>    model = load_chat_model()<br>    response = await model.ainvoke(prompt.ainvoke(state.messages, config))<br>    return {&quot;messages&quot;: [response]}</pre><h3>Step 4: Using MCP Adapter to Connect with External Services</h3><p>The MCP Adapter allows our bot to interact with different services dynamically. It:</p><ul><li>Lists available tools.</li><li>Routes user queries to the correct API.</li><li>Retrieves and processes responses.</li></ul><p>For example, fetching tools from an MCP server:</p><pre>class GetTools(MCPSessionFunction):<br>    async def __call__(self, server_name: str, session: ClientSession) -&gt; list[dict[str, Any]]:<br>        tools = await session.list_tools()<br>        return [{&quot;name&quot;: tool.name, &quot;description&quot;: tool.description} for tool in tools.tools]</pre><h3>Step 5: Sending Responses Back to Slack</h3><p>Once LangGraph processes the request, the response is posted back to Slack:</p><pre>await APP_HANDLER.app.client.chat_postMessage(<br>    channel=channel_id,<br>    thread_ts=thread_ts,<br>    text=response_text,<br>)</pre><p>This ensures that users receive an immediate reply within their Slack channel.</p><h3>Demo Video</h3><p>To see the bot in action, check out our demo video below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*Vo2C4jE4Dre1RFgsjGpp9A.gif" /><figcaption>Slack Demo</figcaption></figure><h3>Challenges and Solutions</h3><h4>1. Handling Large Workflows</h4><ul><li><strong>Challenge:</strong> Some requests require multiple steps (e.g., fetching GitHub PR details and checking Jira status).</li><li><strong>Solution:</strong> We designed LangGraph to handle multi-step workflows efficiently, ensuring each request is processed in a structured manner.</li></ul><h4>2. Managing Multiple MCP Servers</h4><ul><li><strong>Challenge:</strong> Different MCP servers have unique APIs and data structures.</li><li><strong>Solution:</strong> The <strong>MCP Adapter</strong> abstracts the API differences, allowing seamless integration without requiring separate logic for each service.</li></ul><h4>3. Rate Limits on External APIs</h4><ul><li><strong>Challenge:</strong> APIs like Slack and GitHub impose rate limits, which could disrupt bot functionality.</li><li><strong>Solution:</strong> We implemented <strong>caching</strong> and <strong>batch processing</strong> to minimize API calls and ensure a smooth experience for users.</li></ul><h3>Conclusion</h3><p>This project showcases how AI-driven automation can <strong>boost productivity by integrating multiple services into Slack</strong>. With LangGraph and MCP, businesses can streamline workflows, reduce app-switching, and provide a smoother user experience.</p><p>If you’re looking to enhance your team’s efficiency, consider building a Slack bot that <strong>connects all your tools into a single conversational interface.</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3607b849a565" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heurislabs/configuring-slackbot-with-various-sources-using-mcp-3607b849a565">Building a SlackBot for workflow automation using Model Context Protocol(MCP)</a> was originally published in <a href="https://medium.com/heurislabs">HeurisLabs</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Universal Assistant to connect with any API]]></title>
            <link>https://medium.com/heurislabs/building-a-universal-assistant-to-connect-with-any-api-89d7c353e524?source=rss----dce691425da1---4</link>
            <guid isPermaLink="false">https://medium.com/p/89d7c353e524</guid>
            <category><![CDATA[model-context-protocol]]></category>
            <category><![CDATA[ai-integration]]></category>
            <category><![CDATA[anthropic-claude]]></category>
            <category><![CDATA[langgraph]]></category>
            <category><![CDATA[ai-assistant]]></category>
            <dc:creator><![CDATA[Pranav Dhoolia]]></dc:creator>
            <pubDate>Sat, 08 Feb 2025 06:34:42 GMT</pubDate>
            <atom:updated>2025-02-07T14:47:14.338Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LzdeOcwF_lE4KIscsqo3wg.png" /><figcaption>Universal Assistant connected to multiple APIs</figcaption></figure><p>In our previous blog, we built a <a href="https://github.com/esxr/langgraph-mcp">universal assistant</a> that could use <strong>Claude’s Model Context Protocol (MCP)</strong> to interface with different services (such as our filesystem, a sqlite database, etc.), and help make our daily tasks more efficient.</p><p>In this blog, we’ll extend our <a href="https://github.com/esxr/langgraph-mcp"><strong>Universal Assistant</strong></a> to be able to connect to almost any API and automatically call its endpoints!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/854/1*mSgJmL2lP-FeOiHaQlqZ4w.gif" /></figure><p>You can access the code for this blog <a href="https://github.com/esxr/langgraph-mcp/tree/ed8d6d5f2fcaa5a94d0a4236068199c7f473a3ea">here</a>.</p><h3>Steps</h3><blockquote>Note: I’ve included links for all the relevant changes for greater clarity.</blockquote><h4>Initial Setup</h4><p>In a regular MCP server setup, we’re limited by the number of MCP servers out there, or have to build our own. So adding support for each service we use with our assistant would mean creating a robust MCP implementation of the same. This approach might <em>currently</em> not be scalable.</p><p>Fortunately, there is an easier way of doing the same. Most of the popular APIs can be specified in a standard OpenAPI format. We just need a generic MCP server that can convert an OpenAPI spec into MCP tools.</p><p>After some research, I found <a href="https://github.com/snaggle-ai/openapi-mcp-server">this mcp server</a>, which takes an OpenAPI Spec and dynamically exposes its endpoints as tools. All we have to do is add a config to our <a href="https://github.com/esxr/langgraph-mcp/blob/ed8d6d5f2fcaa5a94d0a4236068199c7f473a3ea/mcp-servers-config.sample.json">mcp-servers-config.json</a> for our desired spec!</p><pre>{<br>  &quot;mcpServers&quot;: {<br>    ...<br>    &quot;jira&quot;: {<br>        &quot;description&quot;: &quot;Project management and issue tracking, e.g., get, modify, or search projects, issues, bugs, etc.&quot;,<br>        &quot;command&quot;: &quot;npx&quot;,<br>        &quot;args&quot;: [<br>            &quot;-y&quot;,<br>            &quot;openapi-mcp-server@1.1.0&quot;,<br>            &quot;/path/to/openapi-specs/jira-openapi.json&quot;<br>        ],<br>        &quot;env&quot;: {<br>            &quot;OPENAPI_MCP_HEADERS&quot;: &quot;{\&quot;Authorization\&quot;: \&quot;Basic [base64 encoded version of: &lt;YOUR.JIRA.EMAIL@YOUR.JIRA.TOKEN&gt;]\&quot;}&quot;,<br>            &quot;JIRA_HOST&quot;: &quot;your_workspace.atlassian.net&quot;,<br>            &quot;JIRA_EMAIL&quot;: &quot;you_email@example.com&quot;,<br>            &quot;JIRA_API_TOKEN&quot;: &quot;YOUR.JIRA.TOKEN&quot;<br>        }<br>    }<br>  }<br>}</pre><p>We’ve connected our JIRA workspace to our assistant using a simple config. Now, let’s tell our assistant to create a JIRA ticket for us.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*ZLJQXgCxbd7kzudPfWQAKw.gif" /></figure><p>It’s able to select the right tool (from a list of 500 or so tools!) most of the time. However, it cannot correctly map the information from the conversation context to tool arguments.</p><p>Tracing this with the help of LangSmith reveals the problem. The tool definitions returned by the MCP server has refs to the information schema (instead of the actual schema). Therefore, the target schema of tool input arguments is effectively not available to our tool orchestrator.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*snHD1O30cPPf1Xez0VSsQg.gif" /></figure><p>To address the problem, we need to make sure that the tool definition has resolved schema references.</p><h4>Improving the openapi-mcp-server integration</h4><p><strong>Openapi-mcp-server </strong>by itself doesn’t check for the presence of refs and resolve them. This is what I needed to correct. After going through LangChain <a href="https://python.langchain.com/api_reference/langchain/chains/langchain.chains.openai_functions.openapi.openapi_spec_to_openai_fn.html">docs</a>, I found a utility called <a href="https://github.com/esxr/langgraph-mcp/blob/ed8d6d5f2fcaa5a94d0a4236068199c7f473a3ea/src/langgraph_mcp/utils/openapi_utils.py">openapi_spec_to_openai_fn</a>, which automatically resolves the refs during the transformation.</p><p>So let’s extend GetTools of the mcp_wrapper.py with <a href="https://github.com/esxr/langgraph-mcp/blob/ed8d6d5f2fcaa5a94d0a4236068199c7f473a3ea/src/langgraph_mcp/mcp_wrapper.py#L91"><strong>GetOpenAPITools</strong></a>.</p><pre>class GetOpenAPITools(GetTools):<br>    def __init__(self, openapi_spec: dict, **kwargs):<br>        self.openapi_spec_dict = openapi_spec<br>        self.openapi_spec = OpenAPISpec.from_spec_dict(self.openapi_spec_dict)<br><br>    async def __call__(<br>        self, server_name: str, session: ClientSession<br>    ) -&gt; list[dict[str, Any]]:<br>        tools = await session.list_tools()<br>        openai_fns = openapi_spec_to_openai_fn(self.openapi_spec)[0]<br>        <br>        # Making the JSON schema given by openapi_spec_to_openai_fn<br>        # compatible to Runtool function (MCP) <br>        for fn, tool in zip(openai_fns, tools.tools):<br>            op_id = &quot;&quot;.join(tool.name.split(&quot;-&quot;)[1:])<br>            # This will be useful later<br>            fn[&quot;metadata&quot;] = {}<br>            fn[&quot;metadata&quot;][&quot;tool_info&quot;] = extract_inlined_operation_data(<br>                self.openapi_spec_dict, op_id<br>            )<br><br>            fn[&quot;parameters&quot;][&quot;properties&quot;] = merge_json_structure(<br>                fn[&quot;parameters&quot;][&quot;properties&quot;]<br>            )<br>            fn[&quot;name&quot;] = tool.name<br>            # truncate all openai_fn descriptions to 1024 characters<br>            if len(fn[&quot;description&quot;]) &gt; 1024:<br>                fn[&quot;description&quot;] = fn[&quot;description&quot;][:1021] + &quot;...&quot;<br><br>        return openai_fns</pre><p>The graph may now use GetOpenAPITools instead of the usual GetTools while asking for tools from an <strong>openapi-mcp-server.</strong></p><pre>async def mcp_orchestrator(<br>    state: State, *, config: RunnableConfig<br>) -&gt; dict[str, list[BaseMessage]]:<br>    &quot;&quot;&quot;Orchestrates MCP server processing.&quot;&quot;&quot;<br><br>    ...<br>    <br>    # Fetch tools from the MCP server conditionally<br>    tools = []<br>    args = (<br>        server_config[&quot;args&quot;][1:]<br>        if server_config[&quot;args&quot;][0] == &quot;-y&quot;<br>        else server_config[&quot;args&quot;]<br>    )<br><br>    # Separate integration for openapi-mcp-server@1.1.0<br>    # TODO: refactor this into an adapter pattern later<br>    # You can also contribute to &quot;Github: esxr/langgraph-mcp&quot; for this!<br>    if args[0] == &quot;openapi-mcp-server@1.1.0&quot;:<br>        openapi_path = args[1]<br><br>        # Get the openapi file as a json<br>        with open(openapi_path, &quot;r&quot;) as file:<br>            openapi_spec = json.load(file)  # Converts JSON to a Python dictionary<br><br>        # convert the spec to openai tools<br>        tools = await mcp.apply(<br>            server_name,<br>            server_config,<br>            mcp.GetOpenAPITools(openapi_spec),<br>        )<br>    else:<br>        tools = await mcp.apply(server_name, server_config, mcp.GetTools())<br><br><br>    ...</pre><p>This yields much better results!</p><p>However, though <a href="https://github.com/esxr/langgraph-mcp/blob/ed8d6d5f2fcaa5a94d0a4236068199c7f473a3ea/src/langgraph_mcp/utils/openapi_utils.py">openapi_spec_to_openai_fn</a> resolves the refs, it ignores some of the operation details available in the spec. E.g., it ignores <em>examples</em>, and any additional <em>annotations</em> that API spec providers may have added.</p><p>My first intuition was to enhance openapi_spec_to_openai_fn to include these, but for APIs like JIRA (with 500+ operations) that significantly increases the number of tokens with the bind tools. After some experimentation, the approach I chose was to selectively refine the schema details after the tool selection by the orchestrator.</p><p>The graph structure for that looks somewhat like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NSge-LWUz_Lg7MwqlsV2aw.png" /></figure><p>The <strong>refine_tool_call </strong>binds that single selected tool with the additional schema details and re-attempts the <a href="https://github.com/esxr/langgraph-mcp/blob/ed8d6d5f2fcaa5a94d0a4236068199c7f473a3ea/src/langgraph_mcp/prompts.py#L58">tool orchestration prompt</a>.</p><pre>TOOL_REFINER_PROMPT = &quot;&quot;&quot;You are an intelligent assistant with access to various specialized tools.<br><br>Objectives:<br>1. Analyze the conversation to understand the user&#39;s intent and context.<br>2. Select the most appropriate info from the conversation for the tool_call<br>3. Combine tool outputs logically to provide a clear and concise response.<br><br>Steps to follow:<br>1. Understand the conversation&#39;s context.<br>2. Select the most appropriate info from the conversation for the tool_call.<br>3. If there is a tool response, combine the tool&#39;s output to provide a clear and concise answer to the user&#39;s query, or attempt to select another tool if needed to provide a more comprehensive answer.<br><br>{tool_info}<br><br>System time: {system_time}<br>&quot;&quot;&quot;</pre><p>Here’s how the <strong>refine_tool_call</strong> node looks like in <a href="https://github.com/esxr/langgraph-mcp/blob/ed8d6d5f2fcaa5a94d0a4236068199c7f473a3ea/src/langgraph_mcp/assistant_graph.py#L271">assistant_graph.py</a></p><pre>async def refine_tool_call(<br>    state: State, *, config: RunnableConfig<br>) -&gt; dict[str, list[BaseMessage]]:<br>    &quot;&quot;&quot;Call the MCP server tool.&quot;&quot;&quot;<br><br>    if state.current_tool == None:<br>        return<br><br>    # Fetch the current state and configs<br>    ...<br><br>    # Get the tool info<br>    tool_info = state.current_tool.get(&quot;metadata&quot;, {}).get(&quot;tool_info&quot;, {})<br><br>    # Bind the tool call to the model<br>    # Prepare the LLM<br>    prompt = ChatPromptTemplate.from_messages(<br>        [<br>            (&quot;system&quot;, configuration.tool_refiner_prompt),<br>            (&quot;placeholder&quot;, &quot;{messages}&quot;),<br>        ]<br>    )<br>    model = load_chat_model(configuration.tool_refiner_model)<br>    message_value = await prompt.ainvoke(<br>        {<br>            &quot;messages&quot;: state.messages[:-1],<br>            &quot;tool_info&quot;: str(tool_info),<br>            &quot;system_time&quot;: datetime.now(tz=timezone.utc).isoformat(),<br>        },<br>        config,<br>    )<br><br>    # Replace the last tool call with this one<br>    # we don&#39;t want 2 tool calls in a line<br>    last_msg_id = state.messages[-1].id<br>    response = await model.bind_tools([state.current_tool]).ainvoke(<br>        message_value, config<br>    )<br>    response.id = last_msg_id<br><br>    return {<br>      &quot;messages&quot;: [response], <br>      &quot;current_tool&quot;: None # unset the current tool<br>    }</pre><p>You can view all the above changes together <a href="https://github.com/esxr/langgraph-mcp/commit/ed8d6d5f2fcaa5a94d0a4236068199c7f473a3ea">here</a>.</p><h3>Conclusion</h3><p>In this article we saw how to turn any server end-point with an Open API specification to an MCP server using a generic wrapper. This greatly enhances the current reach of our MCP based Universal assistant!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=89d7c353e524" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heurislabs/building-a-universal-assistant-to-connect-with-any-api-89d7c353e524">Building a Universal Assistant to connect with any API</a> was originally published in <a href="https://medium.com/heurislabs">HeurisLabs</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building our own Universal Assistant with LangGraph and MCP]]></title>
            <link>https://medium.com/heurislabs/building-our-own-universal-assistant-with-langgraph-and-mcp-50b503fea1f0?source=rss----dce691425da1---4</link>
            <guid isPermaLink="false">https://medium.com/p/50b503fea1f0</guid>
            <category><![CDATA[langgraph]]></category>
            <category><![CDATA[machine-learning-ai]]></category>
            <category><![CDATA[model-context-protocol]]></category>
            <category><![CDATA[ai-assistant]]></category>
            <category><![CDATA[anthropic-claude]]></category>
            <dc:creator><![CDATA[Pranav Dhoolia]]></dc:creator>
            <pubDate>Sat, 08 Feb 2025 06:34:24 GMT</pubDate>
            <atom:updated>2025-02-07T16:59:02.494Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LzdeOcwF_lE4KIscsqo3wg.png" /></figure><p><a href="https://modelcontextprotocol.io/introduction"><strong>Model Context Protocol (MCP)</strong></a> is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you’re building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need.</p><p>Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools.</p><p><a href="https://langchain-ai.github.io/langgraph/"><strong>LangGraph</strong></a> is a framework designed to enable seamless integration of language models into complex workflows and applications. It emphasizes modularity and flexibility. Workflows are represented as graphs. Nodes correspond to actions, tools, or model queries. Edges define the flow of information between them.</p><p>LangGraph provides a structured yet dynamic way to execute tasks, making it ideal for writing AI applications involving natural language understanding, automation, and decision-making.</p><p>In <a href="https://medium.com/@pranavdhoolia/building-and-deploying-a-virtual-assistant-with-langgraph-5c68dabd82db">this earlier article</a> we enhanced LangGraph’s retrieval agent template to develop and deploy an AI solution.</p><p>In this work, we combine LangGraph with MCP to build our own Universal Assistant. You can access the code <a href="https://github.com/esxr/langgraph-mcp">here</a>.</p><p>Before we get into it, let’s experience what we built in this demo:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fy6MG-aZqmFw%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dy6MG-aZqmFw&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fy6MG-aZqmFw%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/457c22e83b7bbccf43f86710c2ea86c8/href">https://medium.com/media/457c22e83b7bbccf43f86710c2ea86c8/href</a></iframe><blockquote>Note: In the <a href="https://medium.com/@pranavdhoolia/building-a-universal-assistant-to-connect-with-any-api-89d7c353e524">next blog</a>, we explore how to enhance the capabilities of this assistant even further, so that we can plug any OpenAPI spec for an API and connect it!</blockquote><p><a href="https://medium.com/@pranavdhoolia/building-a-universal-assistant-to-connect-with-any-api-89d7c353e524">Building a Universal Assistant to connect with any API</a></p><h3>Implementation</h3><p>For our universal assistant we implement a multi-agent pattern as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*LC60UdkVcymgurdLHTGjTg.png" /><figcaption>Assistant Flow</figcaption></figure><p>Assistant receives the user message and decides the agent to use. The agent node decides the right tool to use, and calls the tool on the MCP server. Since all our agents are based on MCP, a single MCP-Agent node is sufficient for LLM based orchestration, and another single node is sufficient to work with MCP servers to invoke their tools.</p><h3>Our Implementation Walk-thru</h3><p>There are 3 main parts to our implementation:</p><ol><li>Building the Router</li><li>The Assistant</li><li>A generic MCP wrapper</li></ol><h4>Building the Router</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/983/1*gJhlNLpF1JcKcfPJiJ_R7g.png" /><figcaption>Sequence Diagram for build_router_graph</figcaption></figure><p>Our graph to build the router is implemented in <a href="https://github.com/esxr/langgraph-mcp/blob/main/src/langgraph_mcp/build_router_graph.py">build_router_graph.py</a>. It collects routing information based on tools, prompts, and resources offered by each MCP server using our <a href="https://github.com/esxr/langgraph-mcp/blob/main/src/langgraph_mcp/mcp_wrapper.py">mcp_wrapper.py</a>. It indexes this routing information for each server in a vector database.</p><h4>The Assistant</h4><p>The assistant graph is implemented in <a href="https://github.com/esxr/langgraph-mcp/blob/main/src/langgraph_mcp/assistant_graph.py">assistant_graph.py</a>. The following animation describes the role of various nodes and the flow of control thru it, with the help of an example</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l2hl5yqygMgj2Nfdr0iVGw.gif" /></figure><h4>A Generic MCP Wrapper</h4><p><a href="https://github.com/esxr/langgraph-mcp/blob/main/src/langgraph_mcp/mcp_wrapper.py">mcp_wrapper.py</a> employs a Strategy Pattern using an abstract base class (MCPSessionFunction) to define a common interface for executing various operations on MCP servers. The pattern includes:</p><ol><li><strong>Abstract Interface</strong></li></ol><ul><li>MCPSessionFunction defines an async __call__ method as a contract for all session functions.</li></ul><p>2. <strong>Concrete Implementations</strong></p><ul><li>RoutingDescription class implements fetching routing information based on tools, prompts, and resources.</li><li>GetTools class implements fetching tools for the MCP server and transforming them to the format consumable by LangGraph.</li><li>RunTool class implements invoking a tool on MCP server and returning its output.</li></ul><p>3. <strong>Processor Function</strong></p><ul><li>apply serves as a unified executor. It:</li><li>Initialises a session using stdio_client from mcp library.</li><li>Delegates the actual operation to the provided MCPSessionFunction instance via await fn(server_name, session).</li></ul><p>4. <strong>Extensibility</strong></p><ul><li>New operations can be added by subclassing MCPSessionFunction without modifying the cor e processor logic. for e.g. we should be able to add support for getting tools and executing tools using this pattern.</li></ul><h3>Setting it up</h3><p>Create and activate a virtual environment</p><pre>git clone https://github.com/esxr/langgraph-mcp.git <br>cd langgraph-mcp python3 -m venv .venv <br>source .venv/bin/activate</pre><p>Install Langgraph CLI</p><pre>pip install -U &quot;langgraph-cli[inmem]&quot;</pre><blockquote><strong>Note:</strong> “inmem” extra(s) are needed to run LangGraph API server in development mode (without requiring Docker installation)</blockquote><p>Install the dependencies</p><pre>pip install -e .</pre><p>Configure environment variables</p><pre>cp env.example .env</pre><p>Add your OPENAI_API_KEY, GITHUB_PERSONAL_ACCESS_TOKEN etc. to the .env</p><blockquote><strong>Note</strong>: We have added support for <em>Milvus Lite Retriever</em> (support file based URI). Milvus Lite won’t work on Windows. For Windows you may need to use Milvus Server (Easy to start using Docker), and change the MILVUS_DB config to the server based URI. You may also enhance the <a href="https://github.com/esxr/langgraph-mcp/blob/main/src/langgraph_mcp/retriever.py">retriever.py</a> to add retrievers for your choice of vector databases!</blockquote><h3>Conclusion</h3><p>We can now try to connect it to services we use daily (e.g. JIRA, Slack, other channels etc) to help make our daily tasks more efficient!</p><p>We tackle that in the following blog:</p><p><a href="https://medium.com/@pranavdhoolia/building-a-universal-assistant-to-connect-with-any-api-89d7c353e524">Building a Universal Assistant to connect with any API</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=50b503fea1f0" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heurislabs/building-our-own-universal-assistant-with-langgraph-and-mcp-50b503fea1f0">Building our own Universal Assistant with LangGraph and MCP</a> was originally published in <a href="https://medium.com/heurislabs">HeurisLabs</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Host GPT Locally with Ollama]]></title>
            <link>https://medium.com/heurislabs/host-gpt-locally-with-ollama-0372a1e9a89e?source=rss----dce691425da1---4</link>
            <guid isPermaLink="false">https://medium.com/p/0372a1e9a89e</guid>
            <category><![CDATA[gpt]]></category>
            <category><![CDATA[local-llm]]></category>
            <category><![CDATA[ollama]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Pranav Dhoolia]]></dc:creator>
            <pubDate>Sun, 19 Jan 2025 16:40:36 GMT</pubDate>
            <atom:updated>2024-01-23T11:59:28.631Z</atom:updated>
            <content:encoded><![CDATA[<h4>Hosting LLMs locally with <a href="https://ollama.ai/">Ollama</a> and adding a UI using <a href="https://github.com/ollama-webui/ollama-webui">Ollama WebUI</a></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Lgz-7UROlu2p0Qd2.png" /></figure><p>Let’s declare independence. Independence from $20 a month for ChatGPT subscription. and independence from having to be connected all the time.</p><p>Empowered with an Apple M2 MacBook Air (and just 16 GB RAM), we start with local deployment and running of large language models!</p><p>You can check out <a href="https://esxr.io/">esxr.io</a> for more.</p><h3>Ollama</h3><p>Based on a quick research and exploration of <a href="https://docs.vllm.ai/">vLLM</a>, <a href="https://github.com/ggerganov/llama.cpp">llamaCPP</a>, and <a href="https://ollama.ai/">Ollama</a>, let me recommend Ollama! It is a great way to deploy quantized versions of LLMs on personal computers.</p><p>Let’s start by installing Ollama.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/631/0*FwsQV48WHwQCuIvI.png" /></figure><p>Let’s also take a quick look on <a href="https://huggingface.co/spaces/lmsys/chatbot-arena-leaderboard">leaderboard of LLMs</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*7WtnLT9YUqhc843P.png" /></figure><h3>Choosing a suitable model</h3><p>The 16 GB of RAM we have might pose a slight challenge. A 7 billion parameter model, at full 32-bit precision, would require approximately 28 GB of RAM. However, at half precision (16 bits), it would need around 14 GB, and at 8-bit precision, about 7 GB. Therefore, a 7 billion parameter model with 8-bit quantization appears to be a viable option for our hardware specifications. Let’s choose <strong>Zephyr-7B-beta</strong>!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*twK3U7Kog6nSjm8x.png" /></figure><h3>Quantization</h3><p>Now, we obviously can’t run a full model on our computer. We will use <strong>quantization</strong>.</p><blockquote><em>Model quantization in Large Language Models (LLMs) is like compressing a large, detailed photo into a smaller file size. It reduces the size of model’s “weight matrices” (by losing precision) to make it faster and use less computer memory while multiplying matrices in the background, similar to making a photo easier to send or load, but with slightly less detail. (for e.g. multiplying 2 doubles is more costly than multiplying 2 ints)</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/516/0*kms7QFxaMrZqPBoD.png" /></figure><p>You can know more about quantization in detail <a href="https://huggingface.co/docs/optimum/concept_guides/quantization">here</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*aQjnjUJviYPTLIVb.png" /></figure><h3>Getting a Quantized Model</h3><p>Upon conducting research, we discover <strong>TheBloke</strong> on Huggingface, who is well known for publishing quantized versions of various models. Our next step is to explore his workspace on Huggingface to identify suitable quantized models for our needs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*opS6e2XEB-f6iLTq.png" /></figure><p>Based on these use case notes it seems Q5_K_M quantization may be one of the best bargains for us.</p><p>Now let’s check, if Ollama has <a href="https://ollama.ai/library/zephyr/tags">this model</a>, in the quantization we want.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*_JulqGuEGeNfHBj7.png" /></figure><h3>Running and testing the Model</h3><p>Amazing! Let’s run a quick test. Let’s get our model to program Levenshtein distance function.</p><p>We type ollama run zephyr:7b-beta-q5_K_M in the terminal, and we get something like this.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Zp3lJW8qX3gOZwb8.png" /></figure><p>Now, we ask the question.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dZSYZ-XacFDMd3xS.png" /></figure><p>Amazing! We just ran an LLM model on our very own machine!</p><h3>Ollama WebUI</h3><p>While command line is good, let’s get ourselves a ChatGPT like user experience. Amidst several user experience options available, I really liked <a href="https://github.com/ollama-webui/ollama-webui">Ollama Web UI</a>. Let’s install and run it locally.</p><pre>git clone https://github.com/ollama-webui/ollama-webui.git<br>cd ollama-webui</pre><p>Follow the steps given in the <a href="https://github.com/ollama-webui/ollama-webui.git">repo</a> to setup and run the UI. You can also check out <a href="https://esxr.io/">esxr.io</a>.</p><p>Now let’s run the same query using our newly installed UI.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DiS1G-LHn5jlCZwm.png" /></figure><p>Like ChatGPT, this system also maintains a record of our queries, allowing us to revisit them later. With this capability, we now have a personal LLM to perform tasks for us within our own computing environment.</p><h3>Conclusion</h3><p>In this blog, we have successfully installed and run a Large Language Model on our personal machine, leveraging Ollama. For convenience, we integrated it with a ChatGPT-style user-interface. We also learnt about model quantization, a technique enabling us to scale down large models with a trade-off in precision. This allows models to be hosted on personal computing environments, which typically have lesser computational power.</p><p>This blog serves as a foundational guide for any future projects in the realm of generative AI and LLMs, providing a solid base for exploration and development in this exciting field.</p><p>You can check out <a href="https://esxr.io/">esxr.io</a> for more fun stuff!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0372a1e9a89e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heurislabs/host-gpt-locally-with-ollama-0372a1e9a89e">Host GPT Locally with Ollama</a> was originally published in <a href="https://medium.com/heurislabs">HeurisLabs</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Build and Deploy GenAI Solutions Locally]]></title>
            <link>https://medium.com/heurislabs/build-and-deploy-genai-solutions-locally-c9ee98257414?source=rss----dce691425da1---4</link>
            <guid isPermaLink="false">https://medium.com/p/c9ee98257414</guid>
            <category><![CDATA[retrieval-augmented-gen]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[ollama]]></category>
            <category><![CDATA[langchain]]></category>
            <category><![CDATA[genai]]></category>
            <dc:creator><![CDATA[Pranav Dhoolia]]></dc:creator>
            <pubDate>Sun, 19 Jan 2025 16:40:27 GMT</pubDate>
            <atom:updated>2024-06-24T10:05:52.923Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Bust the myth that AI is only for tech giants.</strong> The process of setting up and running sophisticated AI models at home is surprisingly simple!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/797/0*HyLYuZMjYbUd3XQz.png" /></figure><p>This series explains:</p><ul><li><a href="https://youtu.be/4Rl2r9JV-6Q">Locally running a base AI model on your desktop</a></li><li><a href="https://youtu.be/7S73a_XuTdg">Building any AI solution on top of that model, using your own data</a></li><li><a href="https://youtu.be/sZ1aJ0zfgmY">Attaching a UI to your solution within minutes</a></li></ul><p>Empowered with this knowledge, anyone can create their own AI solution, tailored to their specific needs. The future of AI is accessible to everyone, we just need to take the first step!</p><p><em>Originally published at </em><a href="https://navvy.co/build-and-deploy-genai-solutions-locally"><em>http://navvy.co</em></a><em> on January 28, 2024.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c9ee98257414" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heurislabs/build-and-deploy-genai-solutions-locally-c9ee98257414">Build and Deploy GenAI Solutions Locally</a> was originally published in <a href="https://medium.com/heurislabs">HeurisLabs</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Adding ChatGPT-Like functionality to MacOS Spotlight Search]]></title>
            <link>https://medium.com/heurislabs/building-a-chatgpt-like-search-for-your-macos-with-ollama-and-langchain-ff878c1a82c6?source=rss----dce691425da1---4</link>
            <guid isPermaLink="false">https://medium.com/p/ff878c1a82c6</guid>
            <category><![CDATA[langchain]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[langsmith]]></category>
            <category><![CDATA[genai]]></category>
            <category><![CDATA[ollama]]></category>
            <dc:creator><![CDATA[Pranav Dhoolia]]></dc:creator>
            <pubDate>Sun, 19 Jan 2025 16:40:19 GMT</pubDate>
            <atom:updated>2024-06-24T10:01:20.715Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Pr09E-NGy4brcjxl.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*rja2PX_7W38z_o4a.png" /></figure><p>Adding ChatGPT like search system to the MacOS filesystem with Spotlight, Ollama, and LangChain</p><h3>Introduction</h3><p>In one of my <a href="https://youtu.be/4Rl2r9JV-6Q">YouTube videos</a>, I locally hosted LLMs using <a href="https://ollama.ai/">Ollama</a>, and interacted with them in a ChatGPT like interface using <a href="https://github.com/ollama-webui/ollama-webui">Ollama Web UI</a> also hosted locally. This continues that journey. Here, I build a solution using a locally hosted LLM and <a href="https://langchain.com/">LangChain</a> (the most popular framework for building LLM solutions).</p><p>The Youtube video for the same is available <a href="https://youtu.be/7S73a_XuTdg">here</a></p><h3>Motivation</h3><p>I love the built-in SpotLight on Mac. It indexes my documents and allows me to search for keywords and find relevant documents. In a lot of cases, I am not looking for a document, I am looking for answers to my questions that I know are somewhere there in my documents.</p><p>LLMs are good at answering questions. But they don’t have my documents. So I want to build a solution that retrieves relevant documents using Spotlight, and then passes them to LLM to generate answers to my questions. Such technique where we first retrieve information from a database or corpus, and then pass it on to an LLM to generate a response is also referred to as a <strong>Retrieval Augmented Generation</strong> solution. Since we’ll do everything local, let’s call this <strong>Local RAG</strong>.</p><h3>Setup</h3><p>Let’s begin by creating a python environment. Let me use conda for this.</p><pre>conda create -n local_rag python=3.11<br>conda activate local_rag</pre><p>Let’s now install langchain</p><pre>pip install langchain</pre><p>For this exercise I want to search thru PDF documents. So let’s install pypdf. It will help extract text from PDF documents.</p><pre>pip install pypdf</pre><h3>Programmatic use of Spotlight</h3><p>Let’s first write a python program to use Spotlight programmatically. Let’s create a shell program which receives the query as an input.</p><pre>import argparse  # To parse command line arguments<br>def main():<br>    # Setup argument parser<br>    parser = argparse.ArgumentParser(description=&#39;Search files using Spotlight.&#39;)<br>    parser.add_argument(&#39;query&#39;, type=str, help=&#39;Search query&#39;)<br>    # Parse commandline arguments<br>    args = parser.parse_args()<br><br>if __name__ == &quot;__main__&quot;:      # Entry point of the program<br>    main()</pre><p>Let’s now write a function to search using spotlight for files matching this query. We’ll use the mdfind command and run it as a subprocess. We’ll then call this from our main function.</p><pre>import argparse  # To parse command line arguments<br>import subprocess  # To run spotlight as a subprocess<br>def search_with_spotlight(query):<br>    # Prepare the command<br>    command = [&quot;mdfind&quot;]<br>    command += [query]<br><br>    # Execute mdfind command and capture output<br>    result = subprocess.run(command, capture_output=True, text=True)<br><br>    # Check if the command was successful<br>    if result.returncode != 0:<br>        print(&quot;Error in executing search&quot;)<br>        return []<br><br>    # Split the output into lines to get individual file paths<br>    file_paths = result.stdout.strip().split(&#39;\n&#39;)<br>    return file_paths<br><br>def main():<br>    # Setup argument parser<br>    parser = argparse.ArgumentParser(description=&#39;Search files using Spotlight.&#39;)<br>    parser.add_argument(&#39;query&#39;, type=str, help=&#39;Search query&#39;)<br>    # Parse commandline arguments<br>    args = parser.parse_args()<br>    # Perform spotlight search<br>    search_results = search_with_spotlight(args.query)<br>    # Print search results<br>    print(search_results)<br><br>if __name__ == &quot;__main__&quot;:  # Entry point of the program<br>    main()</pre><p>Let’s run it now.</p><pre>python spotlight.py &quot;search_with_spotlight using mdfind&quot;</pre><p>We should see the program file in results.</p><p>Let me also extend this to support restricting the search to a specific folder. We’ll add a new argument to our program to optionally take in a folder. We’ll relay this folder to the search function. And use it to restrict the search to this folder.</p><pre>import argparse  # To parse command line arguments<br>import subprocess  # To run spotlight as a subprocess<br><br>def search_with_spotlight(query, folder=None):<br>    # Prepare the command<br>    command = [&quot;mdfind&quot;]<br>    if folder is not None:<br>        command += [&quot;-onlyin&quot;, folder]<br>    command += [query]<br>    # Execute mdfind command and capture output<br>    result = subprocess.run(command, capture_output=True, text=True)<br>    # Check if the command was successful<br>    if result.returncode != 0:<br>        print(&quot;Error in executing search&quot;)<br>        return []<br>    # Split the output into lines to get individual file paths<br>    file_paths = result.stdout.strip().split(&#39;\n&#39;)<br>    return file_paths<br><br>def main():<br>    # Setup argument parser<br>    parser = argparse.ArgumentParser(description=&#39;Search files using Spotlight.&#39;)<br>    parser.add_argument(&#39;--folder&#39;, type=str, help=&#39;Folder to search in&#39;, default=None)<br>    parser.add_argument(&#39;query&#39;, type=str, help=&#39;Search query&#39;)<br>    # Parse commandline arguments<br>    args = parser.parse_args()<br>    # Perform spotlight search<br>    search_results = search_with_spotlight(args.query, folder=args.folder)<br>    # Print search results<br>    print(search_results)<br><br>if __name__ == &quot;__main__&quot;:  # Entry point of the program<br>    main()</pre><p>We can now run our search in a specific folder like this:</p><pre>python spotlight.py --folder . &quot;search_with_spotlight using mdfind&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/650/0*ZO6NVhTIA-QQl_FA.png" /></figure><p>Since we restricted our search to the current folder, it got us just our program file (which has the query string in it).</p><h3>Layering LLM on top of Spotlight</h3><p>While Spotlight gets us the files of interest containing certain keywords. It doesn’t precisely answer our questions. Let’s say we have a bunch of resumes in a folder, and want to ask a complex question like “<em>Give me a short (about 100 words) summary, including contact details, of candidates having coursera certification in Generative AI</em>”. Based on keywords like <em>resume</em>, <em>coursera</em>, <em>generative AI</em>; Spotlight may get us the resume files of interest. But it will not give us a short summary including contact details for the candidates. Let’s now use LLMs to do this for us.</p><p>Let’s begin by writing our driver function.</p><pre>def generate_answer(question: str, file_paths: list[str], **kwargs: dict) -&gt; str:<br>    # Get Document objects with text content from file paths<br>    docs = get_docs(file_paths)<br><br>    # Construct QA chain<br>    qa_chain = build_qa_chain()<br>    # Prepare input<br>    input = {&quot;docs&quot;:docs, &quot;question&quot;:question}<br>    input.update(kwargs)<br>    # Invoke the chain<br>    return qa_chain.invoke(input)<br><br>if __name__ == &quot;__main__&quot;:  # Entry point of the program<br>    print(generate_answer(<br>        question=&quot;Give me a short, 100 words summary of each candidate having a coursera certification in Generative AI. Please include their contact details as well.&quot;,<br>        file_paths=[&quot;~/Documents/Resumes/Pranav Dhoolia.pdf&quot;],<br>        doc_type=&quot;Candidate Resume&quot;<br>    ))</pre><p>For this exercise let’s assume all our documents are PDFs. Let’s write the get_docs function to extract text documents from pdf files.</p><pre>from langchain.document_loaders import PyPDFLoader<br>from langchain.schema import Document<br>def get_docs(file_paths: list[str]) -&gt; list[Document]:<br>    docs = []<br>    for file_path in file_paths:<br>        loader = PyPDFLoader(file_path)<br>        pages = loader.load()<br>        docs.append(Document(<br>            page_content=&quot;\n&quot;.join(page.page_content for page in pages),<br>            metadata={&quot;source&quot;: file_path}<br>        ))<br>    return docs<br><br>def generate_answer(question: str, file_paths: list[str], **kwargs: dict) -&gt; str:<br>    ...</pre><p>Let’s now build our QA chain. We’ll use the LangChain Expression Language syntax for this. Let’s start by defining the Prompt, the LLM model, and the OutputParser</p><pre>from langchain.document_loaders import PyPDFLoader<br>from langchain.prompts import PromptTemplate<br>from langchain.schema import Document, StrOutputParser<br>from langchain_community.chat_models import ChatOllama<br>from langchain_core.runnables.base import RunnableSerializable<br>def get_docs(file_paths: list[str]) -&gt; list[Document]:<br>    ...<br><br>def build_qa_chain() -&gt; RunnableSerializable:<br>    return (<br>        {}  ## Build the data to be injected into the prompt<br>        | PromptTemplate.from_template(<br>            &quot;Use only the following context to answer the question at the end.&quot;<br>            &quot;\nDo not use anything other than the context below to answer the question.&quot;<br>            &quot;\nI&#39;ll repeat it is extremely important that you only use the provided context below to answer the question.&quot;<br>            &quot;\nIf the context below is not sufficient to answer, just say that you don&#39;t know, don&#39;t try to make up an answer.&quot;<br>            &quot;\n\nContext:\n\n{context}\n\nQuestion: {question}&quot;<br>        )<br>        | ChatOllama(model=&quot;zephyr:7b-beta-q5_K_M&quot;)<br>        | StrOutputParser()<br>    )<br><br>def generate_answer(question: str, file_paths: list[str], **kwargs: dict) -&gt; str:<br>    ...</pre><p>Let’s now build the data to be injected into the prompt. we’ll use the docs, question, and doc_type expected while invoking the chain.</p><pre>...<br>from langchain_core.prompts import format_document<br>...<br>def build_qa_chain() -&gt; RunnableSerializable:<br>    def build_context(docs: list[Document], doc_type: str=&quot;Document&quot;) -&gt; str:<br>        doc_prompt = PromptTemplate.from_template(&quot;{page_content}&quot;)<br>        context = &quot;&quot;<br>        for i, doc in enumerate(docs):<br>            context += f&quot;{doc_type} {i + 1}\n---\n{format_document(doc, doc_prompt)}\n\n&quot;<br>        return context<br>    return (<br>        {<br>            &quot;context&quot;: lambda input: build_context(input[&quot;docs&quot;], input.get(&quot;doc_type&quot;, &quot;Document&quot;)),<br>            &quot;question&quot;: lambda input: input[&quot;question&quot;]<br>        }<br>        | ...<br>    )</pre><h3>Debugging LLM solutions</h3><p>One of the reasons I really love LangChain, is that with LangSmith, it offers a very powerful debugging experience. So before running our LLM layer, let’s set up LangSmith.</p><ol><li>Go to <a href="https://smith.langchain.com/">LangSmith</a> and login with your credentials</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*soWtsF9Br6EPbqVP.jpg" /></figure><p>2. Create your API key for your Organization</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*65eWCaTdBxyxzk03.jpg" /></figure><p>3. Create a new project</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GHMM6keqTz51dEZo.jpg" /></figure><p>4. Setup the environment variables. I have set these up in my .zshrc.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xqhpKMn5sBlga0V5.jpg" /></figure><p>5. Let’s now run our LLM layer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BCHK9lAoS2ajqCd2.png" /></figure><p>It returns here a quick summary of the candidate matching the query, along with their contact details.</p><p>Let’s now goto LangSmith and see the various aspects of LLM execution that happened here!</p><ul><li>Let go back to our project and check Traces</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4GHzkz-NUiBJjKuK.png" /></figure><p>We see a trace for our chain execution. Let’s expand it</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mw2VjFCIIjBikEfl.png" /></figure><p>We can see the things corresponding to components we used in building the chains, i.e., the data, the prompt, the chat model, and the output parser</p><p>For the data, we can see the input, and what the context built from it looks like</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0ewyndBZlt7-MtR7.png" /></figure><p>For the prompt, we can see data input to the template, and the prompt generated from it</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hcYLooB2oizzm5Bd.png" /></figure><p>For the chat model, we can see the prompt input to the model, and the output generated by it</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*On5_9aZspHglUU6z.png" /></figure><p>For the output parser, we can see the input to it, and the output produced by it.</p><p>This offers a powerful general debugging experience.</p><p>Further, if we want to fine tune our model, based on the usage of our application over a period of time, we can select the traces we want to use for fine tuning, and easily construct a dataset to use for fine tuning from it like this.</p><p>Select the trace(s) and click on the Add to Dataset button</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2WCuPbjhZeOXqYII.png" /></figure><p>Since we don’t already have a dataset, we’ll create a new one</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*j5x-Ngh8QyKKZeeU.png" /></figure><p>And now we can see a preview of dataset being created as well. — In the preview here we can see as input columns: the context, the question, and the doc domain type, and as output the LLM’s response.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ob9nqt5_zqJ9PdYA.png" /></figure><p>Over a period of time, once we have enough traces, we can select and add all good traces to this dataset, and use it for fine tuning our model!</p><h3>Combining the two</h3><p>So we have built a programmatic interface to Spotlight, and we have built an LLM based QA chain that given some content can precisely answer a question based on that content. We now need to chain the two together. Before we do that, let’s first understand a little complication that we anticipate here. As we saw earlier, for our complex query, Spotlight didn’t return anything. That’s because it searched for files contained all the words in our query. But our query really wants a much smaller subset of keywords to be searched for. Most of the query is really a description of what we want from the files found containing that subset of keywords.</p><p>So we need to first extract the keywords from our query, and then use Spotlight to search for files containing those keywords. Once again an LLM chain is perhaps the best for identifying those keywords so let’s write first write a function to do that.</p><pre>import json<br>from langchain.prompts import PromptTemplate<br>from langchain.schema import StrOutputParser<br>from langchain_community.chat_models import ChatOllama<br><br>def extract_keywords(query: str) -&gt; list[str]:<br>    # Construct the keyword extraction chain<br>    keyword_extraction_chain = (<br>        {<br>            &quot;query&quot;: lambda query: query<br>        }<br>        | PromptTemplate.from_template(<br>            &quot;From the query below please extract search keywords. &quot;<br>            &quot;The search keywords must be part of the query string. &quot;<br>            &quot;Avoid words general to this domain such as: candidates, resume, contact, and details. &quot;<br>            &quot;Output a json formatted list of keyword strings. &quot;<br>            &quot;Each item in the output json should be a string.&quot;<br>            &quot;\n\nQuery:\n```{query}\n```&quot;<br>        )<br>        | ChatOllama(model=&quot;zephyr:7b-beta-q5_K_M&quot;)<br>        | StrOutputParser()<br>    )<br>    # Invoke the chain<br>    response = keyword_extraction_chain.invoke(query)<br>    # Get the substring between last `[` and `]`<br>    return json.loads(f&#39;[{response[response.rfind(&quot;[&quot;) + 1:response.rfind(&quot;]&quot;)].strip()}]&#39;)<br><br>if __name__ == &quot;__main__&quot;:  # Entry point of the program<br>    print(extract_keywords(&quot;Give me a short (about 100 words) summary of candidates having coursera certification in Generative AI. Please include their contact details.&quot;))</pre><p>Now that we have our search keywords extractor, let’s modify our main function to combine everything.</p><pre>...<br>from keywords import extract_keywords<br>from qa import generate_answer<br>def search_with_spotlight(query, folder=None):<br>    ...<br>    return file_paths<br><br>def main():<br>    ...<br>    args = parser.parse_args()<br>    # Let&#39;s extact keywords from the query<br>    keywords = extract_keywords(args.query)<br>    # Construct an OR query from the keywords<br>    query = &quot; OR &quot;.join(keywords)<br>    # Perform spotlight search<br>    search_results = search_with_spotlight(query, folder=args.folder)<br>    # Generate answer from the query and top 3 search results<br>    return generate_answer(args.query, search_results[:3], doc_type=&quot;Candidate&quot;)<br><br>if __name__ == &quot;__main__&quot;:  # Entry point of the program<br>    print(main())</pre><p>Let’s now run everything together!</p><pre>python spotlight.py --folder ../test/resume &quot;Give me a short (about 100 words) summary of candidates having coursera certification in Generative AI. Please include their contact details.&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LVwF28xobQP4DSrj.png" /></figure><p>Works like a charm!</p><h3>Conclusion</h3><p>We successfully used locally hosted LLMs along with Langchain framework to build an LLM solution. This solution provided amazing answers to quite complex questions on our local content. What’s more, we didn’t even have to setup any additional tools.</p><p>We used Spotlight to search for files containing certain keywords, and then used LLMs to precisely answer questions based on those files. We also saw the power of the LangChain framework, in how easy it was to use LangSmith to debug our LLM based solutions, and how easily we could use it to create datasets, based on the usage traces of our solution, to fine tune our models.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ff878c1a82c6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heurislabs/building-a-chatgpt-like-search-for-your-macos-with-ollama-and-langchain-ff878c1a82c6">Adding ChatGPT-Like functionality to MacOS Spotlight Search</a> was originally published in <a href="https://medium.com/heurislabs">HeurisLabs</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Making a ChatGPT-like UI for your application in 15 minutes]]></title>
            <link>https://medium.com/heurislabs/making-a-chatgpt-like-ui-for-your-application-in-15-minutes-c522746f0911?source=rss----dce691425da1---4</link>
            <guid isPermaLink="false">https://medium.com/p/c522746f0911</guid>
            <category><![CDATA[langchain]]></category>
            <category><![CDATA[genai]]></category>
            <category><![CDATA[open-webui]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[ollama]]></category>
            <dc:creator><![CDATA[Pranav Dhoolia]]></dc:creator>
            <pubDate>Sun, 19 Jan 2025 16:40:07 GMT</pubDate>
            <atom:updated>2024-06-24T09:59:57.591Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*zJD0PYmqFxLLGZl9.png" /></figure><p>Let us create a Generic Chat UI that we can use with ANY LLM solution we create in future!</p><p>In a <a href="https://esxr.io/local-llm-rag-solution-ollama-spotlight-langchain">previous blog</a>, we learnt how to extend the MacOS Spotlight search and build a smart solution on top of it that answers our questions by using the filesystem as a database.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UdvI2iCbRGreTGOw.png" /></figure><p>We tested it on resumes stored in a folder to only retrieve resumes matching a specific criteria. However, the only way we could interact with such a system was through a commandline interface. Not exactly the dream!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KFLRHgJciX8n5O7g.png" /></figure><p>We want our solution to look somewhat like that of ChatGPT! As we saw in the first video, Ollama WebUI offers a very similar user experience. However, Ollama WebUI has primarily been designed to allow interactions with raw, out-of-the-box LLMs. Our motivation here is to use Ollama WebUI as the UI for our custom local RAG solution. <strong>So let’s get started!</strong></p><p>The code to the repo is provided here:</p><p><a href="https://github.com/esxr/local_llms_rag_solution">https://github.com/esxr/local_llms_rag_solution</a></p><p>Our intuition here is that Ollama web UI expects certain API end-points to be available to interact with Ollama’s API, i.e., the chat, generate, list, … etc. The main API being the chat which is invoked when we send a message. So if can provide the same API, and wrap it around our custom solution, we should be able to use Ollama WebUI for our user experience. And that’s exactly what we pursue in this video.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QPPxVbWP1jPvHyGs.png" /></figure><p>If you haven’t seen the <a href="https://esxr.io/local-llm-rag-solution-ollama-spotlight-langchain">previous blog</a> or watched <a href="https://youtu.be/7S73a_XuTdg">the videos</a>, its recommended to watch them because this is an extension of that.</p><h3>Setup</h3><p>Let’s start by setting up the environment. To build our wrapper API we will use FastAPI. We will make pass-thru requests to Ollama’s API for all the other end-points that we don’t want to handle.</p><pre>conda create -n gen_ui python=3.11<br>conda activate gen_ui<br>pip install fastapi uvicorn httpx</pre><h3>Let’s build &amp; configure Ollama WebUI</h3><pre>git clone https://github.com/ollama-webui/ollama-webui.git<br>cd ollama-webui/</pre><pre># Copying required .env file<br>cp -RPp example.env .env</pre><p>Let’s now configure the .env file. We want our wrapper API to sit in middle of Ollama Web UI and Ollama. So we’ll modify the configuration here to point to our wrapper API instead of Ollama’s API.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*w2S-8e4lu789C0vz.jpg" /></figure><p>Let’s now build the Ollama WebUI frontend.</p><pre># Building Frontend Using Node<br>npm i<br>npm run build</pre><p>Let’s now install the Ollama WebUI backend dependencies.</p><pre>cd ./backend<br>pip install -r requirements.txt -U</pre><p>Let’s configure the backend to use our wrapper API instead.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0Z1NqRKfE0jfGOVu.jpg" /></figure><p>Let’s now serve Ollama WebUI backend.</p><pre># Serving Frontend with the Backend<br>sh start.sh</pre><h3>Pass-thru Wrapper</h3><p>Let’s now start with wrapper API. Let’s begin by writing a pass-thru wrapper first. FastAPI extends the startlette library, which provides us with base middleware classes. We’ll use the BaseHTTPMiddleware to intercept all client requests coming on to our server, send them to Ollama’s API, and return the response to the client. We’ll add this middleware to the FastAPI application. We’ll use uvicorn to run our FastAPI application on port 5000.</p><pre># main.py<br>import asyncio<br>import json<br>from datetime import datetime<br><br>import httpx<br>from fastapi import FastAPI, Request<br>from fastapi.responses import StreamingResponse<br>from starlette.middleware.base import BaseHTTPMiddleware<br>from starlette.responses import Response<br>app = FastAPI()<br>OLLAMA_SERVER_URL = &quot;http://localhost:11434&quot;<br>class RelayMiddleware(BaseHTTPMiddleware):<br>    async def dispatch(self, request: Request, call_next):<br>        other_server_url = f&#39;{OLLAMA_SERVER_URL}{request.url.path}&#39;<br>        body = b&quot;&quot;<br>        async for chunk in request.stream():<br>            body += chunk<br>        async with httpx.AsyncClient() as client:<br>            req_data = {<br>                &quot;method&quot;: request.method,<br>                &quot;url&quot;: other_server_url,<br>                &quot;headers&quot;: request.headers.raw,<br>                &quot;params&quot;: request.query_params,<br>                &quot;content&quot;: body<br>            }<br>            response = await client.request(**req_data)<br>            return Response(response.content, status_code=response.status_code, headers=dict(response.headers))<br># Add the middleware to the FastAPI application<br>app.add_middleware(RelayMiddleware)<br>if __name__ == &quot;__main__&quot;:<br>    import uvicorn<br>    uvicorn.run(app, host=&quot;0.0.0.0&quot;, port=5000, log_level=&quot;info&quot;)</pre><p>Let’s do a quick test of our wrapper. Make sure to have Ollama running on port 11434.</p><p>Go to the dashboard at: <a href="http://localhost:8080.">http://localhost:8080.</a> You should see the dashboard without any errors.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xNz3vby3kGeyrkx4.jpg" /></figure><p>Now select the model you want and chat. You should see the chat working as expected.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*zsSqrMCwyxHTMpsL.jpg" /></figure><p>Great! So our pass-thru wrapper is working as expected.</p><h3>Customizing /api/chat</h3><p>Let’s now override the /api/chat in our wrapper. Let’s begin by excluding /api/chat from the pass-thru wrapper. We’ll do this by adding a condition in the middleware to exclude /api/chat from the pass-thru wrapper.</p><pre>...<br>class RelayMiddleware(BaseHTTPMiddleware):<br>    async def dispatch(self, request: Request, call_next):<br>        # Check if the request is for the /api/chat route<br>        if request.url.path == &quot;/api/chat&quot;:<br>            # If so, just call the next item in the middleware stack<br>            return await call_next(request)<br><br># Otherwise, handle the request as before<br>        ...</pre><p>Before we customize the /api/chat endpoint, Let’s do a quick network analysis of Ollama WebUI to see what it expects from the /api/chat endpoint.</p><p>We find that:</p><ol><li>The /api/chat is a POST request with a text/event-stream content-type, and a application/x-ndjson response.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*g6mHJDqH9IeMCSOQ.jpg" /></figure><p>2. The request body is a JSON object with the following structure:</p><pre>{   <br>  &quot;model&quot;: &quot;&lt;model_name&gt;&quot;,   <br>  &quot;messages&quot;: [     <br>      {       <br>        &quot;role&quot;: &quot;&lt;user|assistant&gt;&quot;,       <br>        &quot;content&quot;: &quot;&lt;text&gt;&quot;     <br>      }   <br>  ] <br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DH_XHEJ-2d646UlY.jpg" /></figure><p>3. The response is a application/x-ndjson stream. With each message having line having the following structure:</p><pre>{     <br>  &quot;model&quot;: &quot;&lt;model_name&gt;&quot;,     <br>  &quot;created_at&quot;: &quot;&lt;datetime.utc.isoformat()&gt;&quot;,     <br>  &quot;message&quot;: {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;&lt;text&gt;&quot;},     <br>  &quot;done&quot;: &lt;False|True&gt; // True for the last message <br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*05yFXfh6_bsTYqfe.jpg" /></figure><p>4. We learnt that the chat endpoint is a streaming endpoint. We read the input as an <strong>event-stream</strong>, and we’ll format the dummy output to the <strong>Newline delimited JSON</strong> (application/x-ndjson) format. NDJSON is a convenient format for storing or streaming structured data that may be processed one record at a time. Each line in an NDJSON file is a valid JSON value, but the entire file is not a valid JSON array. This format is particularly useful for streaming LLM outputs. Ollama uses it as well.</p><p>Let’s now customize the /api/chat endpoint to first push some dummy response. To begin with the processing logic is just a placeholder, and pretty much echoes back the request content.</p><pre>...<br>@app.post(&quot;/api/chat&quot;)<br>async def chat(request: Request):<br>    def process(input_data):<br>        # Process the input_data and return a string<br>        # This is a placeholder for actual processing logic<br>        processed_string = &quot;Processed: &quot; + str(input_data)<br>        return processed_string<br><br>async def generate_ndjson(model: str, msg: str):<br>        for word in msg.split():<br>            yield json.dumps({<br>                &quot;model&quot;: model,<br>                &quot;created_at&quot;: datetime.utcnow().isoformat() + &quot;Z&quot;,<br>                &quot;message&quot;: {<br>                    &quot;role&quot;: &quot;assistant&quot;,<br>                    &quot;content&quot;: word + &quot; &quot;<br>                },<br>                &quot;done&quot;: False<br>            }) + &quot;\n&quot;<br>            await asyncio.sleep(0.1)<br>        yield json.dumps({<br>            &quot;model&quot;: model,<br>            &quot;created_at&quot;: datetime.utcnow().isoformat() + &quot;Z&quot;,<br>            &quot;message&quot;: {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;.&quot;},<br>            &quot;done&quot;: True<br>        }) + &quot;\n&quot;<br>    # initialize input<br>    input_string = &quot;&quot;<br>    # Extract input from Ollama WebUI request (MIME type: text/event-stream)<br>    async for bytes in request.stream():<br>        if bytes:<br>            input_string = bytes.decode()<br>        else:<br>            continue<br>    # Process input_data<br>    input_data = json.loads(input_string)<br>    output = process(input_data)<br>    # Stream output in Ollama WebUI desired format (MIME type: application/x-ndjson)<br>    return StreamingResponse(generate_ndjson(model=input_data[&quot;model&quot;], msg=output), media_type=&quot;application/x-ndjson&quot;)<br>...</pre><p>Let’s now test our customized /api/chat endpoint.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*f_Jhc2ANLDN-QX1-.jpg" /></figure><p>Voila, our dummy echo based response is working as expected.</p><h3>Wrap /api/chat around our RAG solution</h3><p>Let’s now modify the chat API to invoke our RAG solution. But before that, we’ll need to modify our RAG solution. If you can recall from the <a href="https://github.com/esxr/local_llms_rag_solution">repo</a>, the code to our main function looks somewhat like this:</p><pre>...<br>def main():<br>    # Setup argument parser<br>    parser = argparse.ArgumentParser(description=&quot;Search files using Spotlight.&quot;)<br>    parser.add_argument(&quot;--folder&quot;, type=str, help=&quot;Folder to search in&quot;, default=None)<br>    parser.add_argument(&quot;query&quot;, type=str, help=&quot;Search query&quot;)<br><br>    # Parse commandline arguments<br>    args = parser.parse_args()<br>    # Create search query<br>    keywords = extract_keywords(args.query)<br>    query = &quot; OR &quot;.join(keywords)<br>    # Perform spotlight search<br>    search_results = search_with_spotlight(query, folder=args.folder)<br>    # Generate answer for the original query and top 3 search results<br>    print(<br>        generate_answer(<br>            args.query,<br>            search_results[:3],<br>            doc_type=&quot;Candidate&quot;<br>        )<br>    )<br><br>if __name__ == &quot;__main__&quot;:  # Entry point of the program<br>    main()<br>...</pre><p>Let’s modify this code a bit so that it can be used with other programs as well. Our final code should look somewhat like this</p><pre>def chat(query, folder=None):<br>    # Let&#39;s extact keywords from the query<br>    keywords = extract_keywords(query)<br><br>    # Construct an OR query from the keywords<br>    search_query = &quot; OR &quot;.join(keywords)<br>    # Perform spotlight search<br>    search_results = search_with_spotlight(search_query, folder=folder)<br>    # Generate answer for the original query and top 3 search results<br>    print(generate_answer(query, search_results[:3], doc_type=&quot;Candidate&quot;))<br>    # Print search results<br>    print(search_results)<br><br>if __name__ == &quot;__main__&quot;:  # Entry point of the program<br>    # Setup argument parser<br>    parser = argparse.ArgumentParser(description=&#39;Search files using Spotlight.&#39;)<br>    parser.add_argument(&#39;--folder&#39;, type=str, help=&#39;Folder to search in&#39;, default=None)<br>    parser.add_argument(&#39;query&#39;, type=str, help=&#39;Search query&#39;)<br>    # Parse commandline arguments<br>    args = parser.parse_args()<br>    chat(args.query, args.folder)</pre><p>Note that we’ve changed the function signature, and simplified it, so that we’re left with this signature</p><pre>...<br>def chat(query, folder=None):<br>...</pre><p>We can now import and plug this function anywhere we want to invoke the LLM-powered search.</p><p>let’s go back to our main.py` file for this project, and change the process function like so, replacing</p><pre>def process(input_data):<br>        ...<br>        processed_string = &quot;Processed: &quot; + str(input_data)<br>        return processed_string</pre><p>with this</p><pre>def process(input_data):<br>        ...<br>        processed_string = chat(<br>            input_data[&quot;messages&quot;][-2][&quot;content&quot;],<br>            folder=[&quot;~/Documents/Resumes/&quot;]<br>        )<br>        return processed_string</pre><p>That’s it! That’s all that was required to link our RAG solution to the API endpoint. We can now test the solution to see if it works.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*WSYHl6lgIVrYUesj.png" /></figure><p>Works like a charm!</p><h3>Conclusion</h3><p>In this blog, we have successfully designed a rapid and efficient approach to provide a user interface for all future projects, utilizing an existing open-source project (Ollama WebUI) as a foundation. Additionally, we have gained substantial insights into streaming APIs, a commonly utilized element in the realm of generative AI and LLMs.</p><p>We have also explored the API structure required by the Ollama WebUI and developed a wrapper for Ollama. This wrapper intercepts requests intended for Ollama and redirects them to the appropriate endpoints in our solution, seamlessly integrating with the Ollama framework.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c522746f0911" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heurislabs/making-a-chatgpt-like-ui-for-your-application-in-15-minutes-c522746f0911">Making a ChatGPT-like UI for your application in 15 minutes</a> was originally published in <a href="https://medium.com/heurislabs">HeurisLabs</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building & Deploying an Agent with LangGraph]]></title>
            <link>https://medium.com/heurislabs/building-and-deploying-a-virtual-assistant-with-langgraph-5c68dabd82db?source=rss----dce691425da1---4</link>
            <guid isPermaLink="false">https://medium.com/p/5c68dabd82db</guid>
            <category><![CDATA[langgraph]]></category>
            <category><![CDATA[retrieval-augmented-gen]]></category>
            <category><![CDATA[langchain-agents]]></category>
            <dc:creator><![CDATA[Pranav Dhoolia]]></dc:creator>
            <pubDate>Sun, 19 Jan 2025 16:40:00 GMT</pubDate>
            <atom:updated>2025-01-17T05:57:30.707Z</atom:updated>
            <content:encoded><![CDATA[<p>This blog is part of a mini-series where I create and deploy a RAG (Retrieval Augmented Generation) based chatbot for Question Answering over website content using AI solution templates that I developed at AGAILE. This covers how to setup and deploy the backend on LangGraph.</p><p>LangGraph is the most popular platform for developing AI solutions. It provides ready templates for common AI solution patterns. We took its <a href="https://github.com/langchain-ai/retrieval-agent-template">retrieval agent template</a> and customised it for our client needs. In the subsequent sections I describe those customisations in detail.</p><p>Please refer to the code in <a href="https://github.com/esxr/retrieval-agent-template">this repo</a> to follow along.</p><h3>Start with langgraph template</h3><p>Let’s install the LangGraph CLI</p><pre>pip install langgraph-cli --upgrade</pre><p>Let’s now create a new app from a template</p><pre>langgraph new</pre><h3>Indexing Graph Enhancements</h3><p>Add starter_urls, and hops to IndexConfiguration in <a href="https://github.com/esxr/retrieval-agent-template/blob/main/src/retrieval_graph/configuration.py">configuration.py</a>. Also add a method to get the list of urls from the comma-separated starter_urls string.</p><pre>starter_urls: str = field(<br>	default=&quot;https://zohlar.com&quot;,<br>	metadata={<br>		&quot;description&quot;: &quot;Comma-separated string of starter URLs to crawl for indexing web pages.&quot;<br>	},<br>)<br><br>hops: int = field(<br> default=2,<br> metadata={<br>  &quot;description&quot;: &quot;Maximum number of hops to traverse pages linked to the starter URLs.&quot;<br> },<br>)<br><br>def parse_starter_urls(self) -&gt; list[str]:<br> &quot;&quot;&quot;Parse the starter URLs into a list.<br> Returns:<br>  list[str]: A list of URLs parsed from the comma-separated string.<br> &quot;&quot;&quot;<br> return [url.strip() for url in self.starter_urls.split(&quot;,&quot;) if url.strip()]</pre><h3>Support For Open-source Retriever (Milvus)</h3><p>Milvus is open source, and its Lite version can work off a file URI. This makes things economical (for dev), and handy for demos.</p><p>Let’s begin by adding langchain-milvus as a dependency in <a href="https://github.com/esxr/retrieval-agent-template/blob/main/pyproject.toml">pyproject.toml</a></p><p>Then add milvus to retriever_provider list</p><pre>retriever_provider: Annotated[<br>	Literal[&quot;elastic&quot;, &quot;elastic-local&quot;, &quot;pinecone&quot;, &quot;mongodb&quot;, &quot;milvus&quot;],<br>	{&quot;__template_metadata__&quot;: {&quot;kind&quot;: &quot;retriever&quot;}},<br>] = field(<br>	default=&quot;milvus&quot;,<br>	metadata={<br>		&quot;description&quot;: &quot;The vector store provider to use for retrieval. Options are &#39;elastic&#39;, &#39;pinecone&#39;, &#39;mongodb&#39;, or, &#39;milvus&#39;.&quot;<br>	},<br>)</pre><p>Now to add a new method to create a milvus retriever in <a href="https://github.com/esxr/retrieval-agent-template/blob/main/src/retrieval_graph/retrieval.py">retrieval.py</a></p><pre>@contextmanager<br>def make_milvus_retriever(<br>    configuration: IndexConfiguration, embedding_model: Embeddings<br>) -&gt; Generator[VectorStoreRetriever, None, None]:<br>    &quot;&quot;&quot;Configure this agent to use milvus lite file based uri to store the vector index.&quot;&quot;&quot;<br>    from langchain_milvus.vectorstores import Milvus<br><br>vstore = Milvus (<br>    embedding_function=embedding_model,<br>    collection_name=configuration.user_id,<br>    connection_args={&quot;uri&quot;: os.environ[&quot;MILVUS_DB&quot;]},<br>    auto_id=True<br>)<br>yield vstore.as_retriever()</pre><p>and then use this in the factory method</p><pre>@contextmanager<br>def make_retriever(<br>    config: RunnableConfig,<br>) -&gt; Generator[VectorStoreRetriever, None, None]:<br>    # ... same code as before<br><br>match configuration.retriever_provider:<br>      # ... same code as before<br>      case &quot;milvus&quot;:<br>          with make_milvus_retriever(configuration, embedding_model) as retriever:<br>              yield retriever<br>      case _:<br>          # ... as before</pre><p>We’ll also add the following file uri in .env to store the vector index:</p><pre>## Milvus<br>MILVUS_DB=milvus.db</pre><h3>Support for Crawling</h3><p>Out of the box implementation of index_graph (in <a href="https://github.com/esxr/retrieval-agent-template/blob/main/src/retrieval_graph/index_graph.py">index_graph.py</a>) expects as input all the documents to be indexed. Since we are enhancing the graph to include an ingestion pipeline that crawls starting at the specified URL, we’ll modify the index_docs node to kick start the crawl if docs list in the state is empty and starter_urls configuration has been provided.</p><h4>Custom Crawler (using Playright)</h4><p>We can create a Crawler Component using <a href="https://playwright.dev/python/">playwright</a>, which uses headless browsers (Note: Please add playwright &amp; requests as dependencies in <a href="https://github.com/esxr/retrieval-agent-template/blob/main/pyproject.toml">pyproject.toml</a>)</p><p>You can access the code to that <a href="https://github.com/esxr/retrieval-agent-template/blob/main/src/retrieval_graph/crawler.py">here</a>.</p><blockquote><strong><em>Note:</em></strong><em> Just adding </em><em>playwright package to the python environment is not sufficient. The headless browser, and its dependencies also need to be installed. So we need to run </em><em>playright install, and </em><em>playwright install-deps as well.</em></blockquote><blockquote><em>You may use LangGraph Studio app on mac, using docker to locally deploy your graphs. In this case </em><em>playwright install and </em><em>playwright install-deps need to happen in the docker. For that we’ll add the following to </em><a href="https://github.com/esxr/retrieval-agent-template/blob/main/langgraph.json"><em>langgraph.json</em></a></blockquote><blockquote><em>&quot;dockerfile_lines&quot;: [&quot;RUN pip install playwright&quot;, &quot;RUN python -m playwright install&quot;, &quot;RUN python -m playwright install-deps&quot;]</em></blockquote><h4>APIfy Crawler</h4><p>For simpler usecases, we can also use a readymade apify based crawler. We have to modify the indexx_graph.py like so:</p><pre># new imports<br>import json<br><br>from langchain_community.utilities import ApifyWrapper<br>from langchain_community.document_loaders import ApifyDatasetLoader<br># ... existing code<br>def load_site_dataset_map() -&gt; dict:<br>    with open(&quot;sites_dataset_map.json&quot;, &#39;r&#39;, encoding=&#39;utf-8&#39;) as file:<br>        return json.load(file)<br>def apify_crawl(tenant: str, starter_urls: list, hops: int):<br>    site_dataset_map = load_site_dataset_map()<br>    if dataset_id := site_dataset_map.get(tenant):<br>        loader = ApifyDatasetLoader(<br>            dataset_id=dataset_id,<br>            dataset_mapping_function=lambda item: Document(<br>                page_content=item[&quot;html&quot;] or &quot;&quot;, metadata={&quot;url&quot;: item[&quot;url&quot;]}<br>            ),<br>        )<br>    else:<br>        apify = ApifyWrapper()<br>        loader = apify.call_actor(<br>            actor_id=&quot;apify/website-content-crawler&quot;,<br>            run_input={<br>                &quot;startUrls&quot;: starter_urls,<br>                &quot;saveHtml&quot;: True,<br>                &quot;htmlTransformer&quot;: &quot;none&quot;<br>            },<br>            dataset_mapping_function=lambda item: Document(<br>                page_content=item[&quot;html&quot;] or &quot;&quot;, metadata={&quot;url&quot;: item[&quot;url&quot;]}<br>            ),<br>        )<br>        print(f&quot;Site: {tenant} crawled and loaded into Apify dataset: {loader.dataset_id}&quot;)<br>    return loader.load()<br># ... existing code<br>async def index_docs(<br>    state: IndexState, *, config: Optional[RunnableConfig] = None<br>) -&gt; dict[str, str]:<br>    # ... as before<br>    with retrieval.make_retriever(config) as retriever:<br>        # code to kick start crawl if required<br>        configuration = IndexConfiguration.from_runnable_config(config)<br>        if not state.docs and configuration.starter_urls:<br>            print(f&quot;starting crawl ...&quot;)<br>            # state.docs = await crawl (<br>            #     configuration.user_id,<br>            #     configuration.parse_starter_urls(),<br>            #     configuration.hops<br>            # )<br>            state.docs = apify_crawl (<br>                configuration.user_id,<br>                [{&quot;url&quot;: url} for url in configuration.parse_starter_urls()],<br>                configuration.hops<br>            )<br>        # rest remains the same as before<br>        stamped_docs = ensure_docs_have_user_id(state.docs, config)<br>        if configuration.retriever_provider == &quot;milvus&quot;:<br>            retriever.add_documents(stamped_docs)<br>        else:<br>            await retriever.aadd_documents(stamped_docs)<br>    return {&quot;docs&quot;: &quot;delete&quot;}<br># ... existing code</pre><p>I changed default values for parameters saveHtml, and htmlTransformer because, OpenAI embedding models understand HTML very well, and the clean up performed by html transformer loses some useful information.</p><h3>Handling Edge Cases</h3><h4>Splitting documents for fine-grained retrieval</h4><p>So far we directly embedded a crawled document. Crawled documents may be arbitrarily large.</p><ol><li>This puts an uncertainity on how many top_k retrieval results we should consider for response generation.</li><li>For complex queries, where composing high quality answers may need information from multiple documents, limiting top_k may lead to poor results.</li></ol><p>To address this, we split crawl documents into multiple documents as follows:</p><pre># src/retrieval_graph/index_graph.py<br><br># new import<br>from langchain_text_splitters import RecursiveCharacterTextSplitter<br># same as before<br>async def index_docs(<br>    state: IndexState, *, config: Optional[RunnableConfig] = None<br>) -&gt; dict[str, str]:<br>    if not config:<br>        raise ValueError(&quot;Configuration required to run index_docs.&quot;)<br>    with retrieval.make_retriever(config) as retriever:<br>        configuration = IndexConfiguration.from_runnable_config(config)<br>        if not state.docs and (configuration.starter_urls or configuration.apify_dataset_id):<br>            print(f&quot;starting crawl ...&quot;)<br>            crawled_docs = apify_crawl(configuration)<br>            # use a 1000 char size overlapping window based splitter to create smaller document chunks to index<br>            text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)<br>            state.docs = text_splitter.split_documents(crawled_docs)<br>            <br>            # same as before</pre><p>We can now retrieve a larger set of top_k results. Since the maximum chunk size here is 1000, we can deterministically configure the top_k based on the token limits supported by the language model used in the response generation node. E.g., if the token limit is 100,000 tokens, we can potentially use top_k upto 500 (assuming an average token size of 5 characters).</p><pre># src/retrieval_graph/retreival.py<br># ...<br>def make_milvus_retriever(<br>    configuration: IndexConfiguration, embedding_model: Embeddings, **kwargs<br>) -&gt; Generator[VectorStoreRetriever, None, None]:<br>    # ...<br>    yield vstore.as_retriever(search_kwargs=configuration.search_kwargs)<br># ...</pre><p>Here, search_kwargs = {&quot;k&quot;: 10} conveys that retriever will return top 10 results.</p><h4>Rate limits during embedding</h4><p>Embedding Models put limits on the number of token per unit time. Initially, we embedded the whole set of crawled (or split) documents in a single call. For large sites, this ran into rate limit errors.</p><p>With the following enhancement we can add a batch size configuration, split documents into multiple batches, and add a time delay to avoid rate limits.</p><pre># src/retrieval_graph/configuration.py<br># ...<br>@dataclass(kw_only=True)<br>class IndexConfiguration(CommonConfiguration):<br>    # ...<br>    batch_size: int = field(<br>        default=400,<br>        metadata={<br>            &quot;description&quot;: &quot;Number of documents to index in a single batch.&quot;<br>        },<br>    )<br>    # ...</pre><pre># src/retrieval_graph/index_graph.py<br># ...<br><br># generator function to create batches<br>def create_batches(docs, batch_size):<br>    &quot;&quot;&quot;Chunk documents into smaller batches.&quot;&quot;&quot;<br>    for i in range(0, len(docs), batch_size):<br>        yield docs[i:i + batch_size]<br>async def index_docs(<br>    state: IndexState, *, config: Optional[RunnableConfig] = None<br>) -&gt; dict[str, str]:<br>    if not config:<br>        raise ValueError(&quot;Configuration required to run index_docs.&quot;)<br>    with retrieval.make_retriever(config) as retriever:<br>        configuration = IndexConfiguration.from_runnable_config(config)<br>        # ...<br>        stamped_docs = ensure_docs_have_user_id(state.docs, config)<br>        # embed in batches to avoid rate limit errors<br>        batch_size = configuration.batch_size<br>        for i, batch in enumerate(create_batches(stamped_docs, batch_size)):<br>            if configuration.retriever_provider == &quot;milvus&quot;:<br>                retriever.add_documents(batch)<br>            else:<br>                await retriever.aadd_documents(batch)<br>            # sleep if there are more batches to embed<br>            if i &lt; (len(stamped_docs) // batch_size):<br>                time.sleep(60)<br>    return {&quot;docs&quot;: &quot;delete&quot;}<br># ...</pre><h3>Cloud Deployment</h3><p>We can now deploy the retrieval graph to langgraph cloud. The process is quite straight-forward:</p><ol><li>Create a Plus account on <a href="https://smith.langchain.com/">LangSmith</a>. This is necessary. Free-tier developer account does not allow deploying to LangGraph cloud. This costs 39 USD / month.</li><li>Now from LangSmith left-nav bar, go to <em>Deployments &gt; LangGraph Platform</em>. Here we can initiate a + New Deployment.</li><li>We can <em>Import from GitHub</em> my langGraph repo! And add environment configurations (the ones in .env file).</li><li>And just Submit to deploy! This step takes around 15+ minutes. Once this finishes, click on LangGraph Studio to access the studio to test the graphs!</li></ol><h3>References</h3><p>This blog was written in collaboration with Praneet Dhoolia (<a href="https://praneetdhoolia.github.io/">https://praneetdhoolia.github.io</a>) <a href="https://praneetdhoolia.github.io/2024/12/31/expanding-langgraph-retrieval-template.html">https://praneetdhoolia.github.io/2024/12/31/expanding-langgraph-retrieval-template.html</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5c68dabd82db" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heurislabs/building-and-deploying-a-virtual-assistant-with-langgraph-5c68dabd82db">Building &amp; Deploying an Agent with LangGraph</a> was originally published in <a href="https://medium.com/heurislabs">HeurisLabs</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>