<?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[Stories by James Chen on Medium]]></title>
        <description><![CDATA[Stories by James Chen on Medium]]></description>
        <link>https://medium.com/@jameschen_78678?source=rss-c16867ccea73------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*SzPGtdaPS0Zh41UrcrDiyA.jpeg</url>
            <title>Stories by James Chen on Medium</title>
            <link>https://medium.com/@jameschen_78678?source=rss-c16867ccea73------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 16:36:27 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@jameschen_78678/feed" 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 an End-to-End RAG System for the Enterprise — part II]]></title>
            <link>https://medium.com/@jameschen_78678/building-an-end-to-end-rag-system-for-the-enterprise-part-ii-62901a4925a7?source=rss-c16867ccea73------2</link>
            <guid isPermaLink="false">https://medium.com/p/62901a4925a7</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[James Chen]]></dc:creator>
            <pubDate>Wed, 12 Aug 2026 13:49:40 GMT</pubDate>
            <atom:updated>2026-08-12T14:15:33.422Z</atom:updated>
            <content:encoded><![CDATA[<p>This post focuses on the design aspect of an enterprise RAG system.</p><p>We have covered the fundamentals in <a href="https://medium.com/@jameschen_78678/building-an-end-to-end-rag-system-for-the-enterprise-b821c88527f6?source=friends_link&amp;sk=e31184099c9724c6b90079915fc5b123">part I</a>, and we will walk through how to implement these ideas into an actual RAG system, from a design aspect.</p><p>This is a 4-week rapid prototype we built for a client demonstrating our capabilities to quickly develop a use case with IBM watsonx.</p><iframe src="https://cdn.embedly.com/widgets/media.html?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D3Q1Jd7kSfZE&amp;type=text%2Fhtml&amp;schema=youtube&amp;display_name=YouTube&amp;src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F3Q1Jd7kSfZE%3Ffeature%3Doembed" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/8ae611c7854abeb0837fb227b49a8863/href">https://medium.com/media/8ae611c7854abeb0837fb227b49a8863/href</a></iframe><h3>Core features</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*whmQdT-QCgolWapn63uZJA.png" /><figcaption>High level user stories highlighting core features</figcaption></figure><h3>Mockups</h3><p>Wireframes and markup illustrations were instrumental in aligning with the client team on the final deliverables for the POC. The design and approach underwent several iterations before we reached consensus on the final user interface.</p><p>We use the <a href="https://carbondesignsystem.com/">Carbon Design System</a> from IBM to build the mockups for a more professional touch and feel. There’s also a <a href="https://react.carbondesignsystem.com/?path=/docs/getting-started-welcome--welcome">React component library</a> for quick visual explorations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2rSJ3xxF7z4PsErUDB_MGg.png" /><figcaption>Figma design of the search feature returning the most relevant sections</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sjfTsg_LE3Ty07qGICekcA.png" /><figcaption>Figma design of AI-generated response along with retrieved results</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aUGmAECPsx4WPQw3aoCJag.png" /><figcaption>Figma design of the deep-dive page</figcaption></figure><h3>Finding information</h3><p>We want to create a similar experience with Google, where users will be able to use it as a normal search engine, but with AI-generated response as a summary to user’s question.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vor5XeqGNhip_h86e3ojeQ.png" /><figcaption>Design inspiration from Google search</figcaption></figure><p>As we know, responses from LLM may contain hallucination, and we should always verify the underlying data sources. By displaying all relevant sections in a list, we can examine if there’s any irrelevant results, and exclude them if we want a more concise response from LLM.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RISI_2jC1Kh7ssMZAuLnlA.png" /><figcaption>Screenshot of selecting/deselecting relevant sections to regenerate a more concise LLM response</figcaption></figure><h3>Deep-dive</h3><p>When user clicks on the section in retrieved results, they will be directed to the deep-dive page, where we implement an auto-scroll for them, based on page number extracted in the meta data, so they will be looking at the exact page number of the section, instead of from page 1. This is implemented with query parameters from the URL, which got carried over to the next page.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ba8Gk8UkUDQcf98OVRenHA.png" /><figcaption>Screenshot of query parameters carried over to the deep-dive page</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0ozwz9N9DOSlt-qCOQrwQw.png" /><figcaption>Screenshot of the deep-dive summary</figcaption></figure><p>On the right, we can perform another LLM call by selecting full document or only specific pages, if we want a more overall summary, instead of only specific sections from the previous search page.</p><h3>Managing files</h3><p>A separate tab is created for users to upload documents for ingestion and extraction. Users can see a quick overview of all uploaded documents, as well as deleting obsolete ones. When they delete documents, a cleanup action on the vector database is also performed to wipe out the index.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7gZu0X5gyC1z6JheIZ0Egw.png" /><figcaption>Screenshot of deleting an uploaded document</figcaption></figure><h3>Backend API</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OYKN8NZJtPo9EDPrhYE1pQ.png" /><figcaption>Screenshot of the Swagger UI covering backend APIs</figcaption></figure><h4>Authentication</h4><ul><li>Accepts OAuth2PasswordRequestForm; verifies credentials against Redis hashes seeded at startup</li><li>Issues a signed HS256 JWT (sub = username); all other endpoints require Bearer token via OAuth2PasswordBearer</li><li>Stateless — no session storage; token expiry relies on JWT TTL</li></ul><h4>Document ingestion</h4><ul><li>Accepts one or more multipart/form-data files; streams each directly to MinIO via upload_fileobj</li><li>Generates a UUID-prefixed object key ({uuid}_{filename}) per file to avoid collisions</li><li>Pushes each object key onto the Redis job_queue list via RPUSH — decoupling upload latency from processing time</li></ul><h4>Hybrid search</h4><ul><li>Runs two Elasticsearch queries <strong>concurrently</strong> via asyncio.gather:<br><strong>BM25</strong>: multi_match across combined_text, content, source, headings, content_with_context<br><strong>Semantic</strong>: cosine similarity script score against combined_text_vector using a watsonx-generated query embedding</li><li>Both queries filter strictly by username (keyword term); results are tenant-isolated per user</li><li>Embedding call offloaded to a thread pool via run_in_executor to avoid blocking the async event loop</li><li>Results merged with <strong>Reciprocal Rank Fusion</strong> (RRF, k=60); top-10 documents fetched via mget and returned</li></ul><h4>Streaming summarization</h4><ul><li>Accepts prompt, model_id, and context (retrieved chunks) from the UI</li><li>Exchanges a watsonx API key for a short-lived IAM Bearer token before each request</li><li>Proxies the watsonx generation_stream endpoint as a text/event-stream StreamingResponse; tokens reach the browser as they are generated</li><li>LLaMA 3.3 70B prompt formatted with Llama 3 chat tokens (&lt;|begin_of_text|&gt;, headers, &lt;|eot_id|&gt;)</li></ul><h4>Task lifecycle</h4><ul><li>store_task: writes task metadata to a Redis hash keyed {username}:{task_id} and a reverse-lookup hash {task_id}-username</li><li>task_status: reads {id}-status and {id}-error keys set by the job worker; returns running, ready, or failed</li><li>get_tasks: scans Redis with KEYS {username}:* and returns all task hashes for the user</li><li>delete_task : removes Redis keys and issues an Elasticsearch delete_by_query on source == task_id to purge all indexed chunks</li></ul><h4>PDF streaming</h4><ul><li>Fetches the raw file body from MinIO and returns it as a chunked application/pdf StreamingResponse</li><li>Used by the in-browser PDF viewer to render source documents alongside search results</li></ul><h3>Job worker</h3><h4>Queue consumer loop</h4><ul><li>Blocks on BLPOP job_queue 0: zero CPU usage between jobs; wakes instantly when a key arrives</li><li>Single-threaded by design: one document processed at a time, preventing GPU/memory contention on constrained hardware</li><li>Crash-resistant: outer try/except logs the full traceback, marks the job failed in Redis, and resumes listening; the worker never exits</li></ul><h4>Document parsing</h4><ul><li>Fetches the raw bytes from MinIO and wraps them in a DocumentStream for in-memory processing (no temp files)</li><li>Uses <strong>Docling</strong> DocumentConverter with PdfPipelineOptions: table structure extraction enabled, OCR disabled (native text PDFs)</li><li>Supports PDF, DOCX, PPTX, HTML, image, AsciiDoc, and Markdown inputs through a single converter</li><li>HierarchicalChunker splits the converted document into heading-aware chunks preserving document structure</li></ul><h4>Contextual enrichment</h4><ul><li>Adjacent chunks sharing the same heading are <strong>merged</strong> before embedding, which reduces over-segmentation of tightly related content</li><li>For each merged chunk, a <strong>contextualization prompt</strong> is sent to llama-3-3-70b-instruct: the full document markdown + the chunk text → a short retrieval-optimized context sentence</li><li>IAM token cached in-process with a 50-minute refresh interval to avoid redundant token-exchange calls</li><li>LLM errors are caught silently: chunks are indexed with an empty context rather than failing the entire job</li></ul><h4>Embedding</h4><ul><li>Four 1024-dimensional vectors generated per chunk via intfloat/multilingual-e5-large hosted on watsonx.ai:<br>content_vector: raw chunk text<br>content_with_context_vector: LLM context prepended to chunk text<br>headings_vector: section heading text<br>combined_text_vector: source key + heading + context + content (broadest semantic signal)</li><li>Input truncated to 512 tokens server-side; input_text not returned to minimize payload size</li></ul><h4>Elasticsearch indexing</h4><ul><li>Index auto-created on first job with explicit mappings: dense_vector (dims=1024, cosine similarity) for all vector fields; text for full-text fields; keyword for source and username</li><li>Existing chunks for the same source file are deleted via delete_by_query before re-indexing — supports clean re-processing of updated documents</li><li>Chunks written in batches of 100 using the Elasticsearch helpers.bulk API for throughput</li><li>Job status written to Redis as {key}-status = ready on success; UI polls /task_status to surface completion</li></ul><h3>Cloud architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZvoiII7snAFAWEA6saXhuA.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=62901a4925a7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building an On-Premises Speech-to-Text & AI Summary Platform]]></title>
            <link>https://medium.com/@jameschen_78678/building-an-on-premises-speech-to-text-ai-summary-platform-8568b631ae5b?source=rss-c16867ccea73------2</link>
            <guid isPermaLink="false">https://medium.com/p/8568b631ae5b</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[James Chen]]></dc:creator>
            <pubDate>Sun, 02 Aug 2026 14:10:58 GMT</pubDate>
            <atom:updated>2026-08-02T14:10:58.553Z</atom:updated>
            <content:encoded><![CDATA[<h4>A deep dive into architecture, API design, structured LLM output, and frontend patterns</h4><p>This post walks through designing and building a self-hosted platform <strong>prototype </strong>(in 4 weeks), that takes an audio file, transcribes it with <strong>Whisper</strong>, and uses a locally-deployed large language model to produce a structured, timestamp-anchored meeting summary, and streamed in real time to a React frontend. Every component runs in a container; no data leaves your infrastructure.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F6Dt09tJuJBU%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D6Dt09tJuJBU&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F6Dt09tJuJBU%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/61de4a5ad1bc6089f46f138db047f3ff/href">https://medium.com/media/61de4a5ad1bc6089f46f138db047f3ff/href</a></iframe><h3>1 · Overall architecture &amp; data flow</h3><p>The stack is 5 containers wired together on a private bridge network, orchestrated with podman compose (or docker compose).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G6VP8GDY2p3y5sQ4EibzFQ.png" /><figcaption>Overall architecture generated with mermaid</figcaption></figure><ul><li><strong>stt-ui (</strong>Node 20 + React): Single-page app with upload, playback, transcript view, and streaming summary funcions</li><li><strong>stt-api</strong> (Python 3.11 + FastAPI): REST API, JWT auth, MinIO orchestration, LLM gateway</li><li><strong>stt-worker </strong>(python 3.11 + Celery): Async transcription via faster-whisper (CPU, int8). mem_limit: 16g prevents runaway jobs from OOMing the host.</li><li><strong>redis: </strong>Celery broker + result backend + user credentials + transcript cache (also seeding the first user into Redis on first boot via redis-cli HSET)</li><li><strong>minio</strong>: S3-compatible object store with raw uploads and processed MP3s in separate buckets</li></ul><h4>Audio upload &amp; transcription: two-phase flow</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jovvRpY49kVeDM-OmxFaXA.png" /><figcaption>Screenshot of the audio upload process</figcaption></figure><p><strong>Phase 1 : File storage &amp; job dispatch<br></strong>User selects an audio file in the browser. Before sending it to the server, the browser creates a temporary Audio object from the file to read its duration without server round-trip needed for that metadata. The file is then uploaded to the API, which saves it to MinIO’s raw bucket under a randomly generated UUID key and immediately queues a background transcription task. The API responds in milliseconds; no transcription has happened yet.</p><p><strong>Phase 2 : Metadata persistence<br></strong>Only after MinIO confirms the file was stored successfully does the client make a second call to record the task metadata in Redis, including filename, file size, duration, and the UUID . This two-step sequence is intentional: if the file storage fails, no orphan metadata record is ever created. The two operations are kept independent so each can fail or be retried without affecting the other.</p><p><strong>Background transcription<br></strong>Meanwhile the Celery worker picks up the queued task. It fetches the raw file from MinIO, converts it to MP3, writes the MP3 back into a separate processed bucket, then runs faster-whisper to produce timestamped segments. When transcription completes, the results are written to Redis and the task status flips from running to ready. The UI polls the status endpoint every 15 seconds and fetches the transcript once it sees ready.</p><h4>On-prem LLM inference</h4><p>The /summarize flow calls any OpenAI-compatible endpoint via LiteLLM. To run fully on-prem, point the env vars at an <a href="https://ollama.com/">Ollama</a> instance and set the model prefix to ollama/.</p><h4>Whisper model</h4><p>An optimized <a href="https://github.com/SYSTRAN/faster-whisper">faster-whisper</a> version of the original Whisper released by OpenAI. Note that depending on your hardware specifications, you may need to select suitable model size and precision accordingly. For example, if you are running on a MacBook Pro with 16gb of RAM (same as me), then you need to go with a <strong>medium</strong> model instead of <strong>large-V2 </strong>and running on <strong>CPU. </strong>Note that having a dedicated GPU significantly speeds up the transcription process.</p><h3>2 · API design</h3><p>FastAPI auto-generates OpenAPI docs at http://localhost:8082/docs (Swagger UI) and /redoc. All routes except POST /token require a Bearer JWT.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VQoFOxvzEZjbyrbnQfESsQ.png" /><figcaption>Screenshot of the Swagger UI</figcaption></figure><h4>Authentication</h4><p>The API uses FastAPI’s built-in OAuth2PasswordBearer. POST /token fetches the user hash from Redis, verifies the password, and signs a JWT. Every protected route injects current_user via Depends(get_current_user).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N8f-5vaat099X-PmIWy9aw.png" /><figcaption>Screenshot of the login page to get authentication</figcaption></figure><h3>3 · Structured output with LiteLLM + Instructor</h3><p>Rather than asking the LLM to produce free-form markdown, Instructor enforces a typed Pydantic schema and streams partial results as they arrive.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Pc3Aza4X8342-_b_FN0HGQ.png" /><figcaption>Forcing LLM to generate a list of JSON object with <strong>content</strong> and <strong>reference_timestamp</strong> as keys</figcaption></figure><h4>The schema</h4><p>Field descriptions are serialized into the JSON schema sent to the model, giving the LLM explicit format guidance without extensive prompt engineering:</p><pre>class BulletPoint(BaseModel):<br>    content: str<br>    reference_timestamp: str = Field(<br>        description=&quot;Timestamp in HH:MM:SS format (e.g. &#39;00:12:34&#39;)&quot;<br>    )<br><br>class SummaryResponse(BaseModel):<br>    bullet_points: list[BulletPoint] = Field(<br>        description=&quot;Key points, each with the timestamp where it appears&quot;<br>    )</pre><pre>SUMMARIZE_SYSTEM_PROMPT = &quot;&quot;&quot;You are an AI assistant specializing in writing detailed meeting summaries.<br>Your task is to provide a detailed and relevant summary based on the transcript provided.<br>For each key point, identify the timestamp from the transcript (in HH:MM:SS format) where it was mentioned.<br>Do not copy sentences verbatim from the transcript — synthesize and rephrase.<br>&quot;&quot;&quot;</pre><h4>Streaming partial results</h4><p>create_partial yields progressively more complete SummaryResponse instances as tokens arrive. The bullet_points list grows one item at a time:</p><pre>async def run_summary_stream(messages, model):<br>    async for partial in instructor_client.chat.completions.create_partial(<br>        model=model, # openai/gpt-oss-120b<br>        messages=messages,<br>        api_key=WATSONX_API_KEY,<br>        api_base=WATSONX_URL,<br>        project_id=WATSONX_PROJECT_ID,<br>        response_model=SummaryResponse,<br>        max_tokens=2000,<br>        stream=True<br>    ):<br>        yield partial</pre><h4>Filtering incomplete partials</h4><p>During streaming, a timestamp arrives character-by-character (&quot;00&quot; → &quot;00:0&quot; → &quot;00:01:07&quot;). A regex gate holds each bullet back until its timestamp is fully formed, and an event is only emitted when the list actually grows, suppressing the flood of empty partials:</p><pre>TS_RE = re.compile(r&quot;^\d{2}:\d{2}:\d{2}$&quot;)<br><br>last_count = 0<br>async for partial in run_summary_stream(messages, model):<br>    points = [<br>        {&quot;content&quot;: bp.content, &quot;reference_timestamp&quot;: bp.reference_timestamp}<br>        for bp in (partial.bullet_points or [])<br>        if bp.content and TS_RE.match(bp.reference_timestamp or &quot;&quot;)<br>    ]<br>    if len(points) &gt; last_count:   # only emit when a new bullet completes<br>        last_count = len(points)<br>        yield f&quot;data: {json.dumps({&#39;type&#39;:&#39;partial&#39;,&#39;bullet_points&#39;:points})}\n\n&quot;<br><br>yield f&quot;data: {json.dumps({&#39;type&#39;: &#39;complete&#39;})}\n\n&quot;</pre><h3>4· Frontend (audio sync)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RqcZrI-0uH55zsS-NyJlEg.png" /><figcaption>Screenshot of the original mockup design in Figma</figcaption></figure><p>The transcript view synchronizes a live audio player with both the raw transcript and the AI-generated summary. Three interactions are wired together:</p><h4>Real-time transcript highlighting</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vlKMCXJ5HqMbfjeirI41Vg.png" /><figcaption>Highlight in bold in sync with audio play time</figcaption></figure><p>An HTML5 Audio object is created from a blob URL fetched from the API. A timeupdate listener continuously writes the current playback position to state. Each transcript row compares that value against its [start, end] window — the matching row turns bold and auto-scrolls into view:</p><pre>a.addEventListener(&quot;timeupdate&quot;, () =&gt; setCurrentAudioTime(a.currentTime));<br><br>// in JSX — each transcript row:<br>const isActive = currentAudioTime &gt;= item.start &amp;&amp; currentAudioTime &lt; item.end;<br>if (isActive) element.scrollIntoView({ behavior: &quot;instant&quot; });</pre><h4>Click-to-seek from transcript and summary</h4><p>Clicking any transcript row seeks the audio to that segment’s start time. Clicking the clock icon on a summary bullet point does the same, but anchored to the LLM-produced timestamp, parsed from HH:MM:SS to seconds:</p><pre>// transcript row click<br>const handleTranscriptClick = (seconds) =&gt; {<br>    a.currentTime = seconds;<br>    a.play();<br>    setButtonName(&quot;Pause&quot;);<br>};<br><br>// summary bullet — parse HH:MM:SS<br>const [h, m, s] = bp.reference_timestamp.split(&quot;:&quot;).map(Number);<br>const seconds = h * 3600 + m * 60 + s;</pre><h4>Client-side transcript export</h4><p>The download button formats each segment as [HH:MM:SS-HH:MM:SS]: text, creates a Blob, triggers a synthetic anchor click, then immediately calls revokeObjectURL to release the memory:</p><pre>const text = transcription<br>    .map(({ start, end, text }) =&gt;<br>        `[${toTime(start)}-${toTime(end)}]: ${text}`)<br>    .join(&quot;\n&quot;);<br><br>const blob = new Blob([text], { type: &quot;text/plain&quot; });<br>const link = document.createElement(&quot;a&quot;);<br>link.href = URL.createObjectURL(blob);<br>link.download = `transcription_${taskId}.txt`;<br>link.click();<br>URL.revokeObjectURL(link.href);  // clean up blob memory</pre><h3>Key takeaways</h3><ul><li><strong>Decoupled transcription</strong>: Celery + Redis keeps the API non-blocking; Whisper loads once per worker process via worker_process_init.</li><li><strong>Two-phase upload:</strong> object storage write and metadata persistence are separate calls, keeping them atomic and independently retryable.</li><li><strong>Structured streaming:</strong> instructor’s create_partial + regex gate delivers clean, complete bullet points one at a time, never a half-formed timestamp.</li><li><strong>Provider-agnostic LLM: </strong>LiteLLM prefix routing targets Ollama, watsonx.ai, Azure OpenAI, or OpenAI with a single env var.</li><li><strong>Frontend without a state library: </strong>React Hooks + Context handles auth, language, sort/filter/pagination, and real-time streaming cleanly at this scale.</li><li><strong>Audio synchronization: </strong>timeupdate + segment windows provide real-time transcript highlighting; both the transcript and AI summary seek the audio on click.</li><li><strong>Zero external dependencies: </strong>MinIO, Redis, and Ollama all run in containers on the same host. No data leaves the machine.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8568b631ae5b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building an End-to-End RAG System for the Enterprise — part I]]></title>
            <link>https://medium.com/@jameschen_78678/building-an-end-to-end-rag-system-for-the-enterprise-b821c88527f6?source=rss-c16867ccea73------2</link>
            <guid isPermaLink="false">https://medium.com/p/b821c88527f6</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[retrieval-augmented-gen]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[James Chen]]></dc:creator>
            <pubDate>Sat, 25 Jul 2026 05:01:02 GMT</pubDate>
            <atom:updated>2026-08-15T15:01:54.672Z</atom:updated>
            <content:encoded><![CDATA[<p>If you’ve ever asked a chatbot a question and gotten a confident but completely wrong answer, you’ve hit the core problem that RAG solves. Retrieval-Augmented Generation (RAG) connects an AI model to your own documents and data, so it answers from what you actually know.</p><p>Getting a RAG demo working takes an afternoon. Getting it to work reliably at enterprise scale is a different challenge entirely. This post walks through the four components that make up a production RAG system, the tradeoffs that matter in each one, and the tools we’ve found most useful along the way.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YM2RSdAR8ioPY4gVlWunow.png" /><figcaption>Different configuration options identified for each core component</figcaption></figure><p>The four components are:</p><ol><li><strong>Ingestion &amp; Extraction: </strong>reading your documents and turning them into searchable pieces</li><li><strong>Retrieval: </strong>finding the most relevant pieces for any given question</li><li><strong>Generation: </strong>using an AI model to craft an answer from those pieces</li><li><strong>Evaluation: </strong>measuring quality so you can keep improving</li></ol><p>Let’s go through each one.</p><h3><strong>1. Ingestion and Extraction</strong></h3><p>Before an AI can answer questions from your documents, those documents need to be read, understood, and organized. This sounds straightforward until you’re dealing with a mix of scanned PDFs, Word files, PowerPoint decks, spreadsheets, and web pages. All with different layouts, tables, and embedded images.</p><h4><strong>Parsing documents with Docling</strong></h4><p><a href="https://github.com/DS4SD/docling">Docling</a> is an open-source library from IBM Research that handles this diversity well. You point it at a document and it figures out the structure and hands you back clean, organized content.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ns-XJJDm79ATSiDqeJLmMA.png" /><figcaption>Illustration of Docling architecture</figcaption></figure><p>Under the hood, Docling routes each file type to the right parser: PDFs go through a specialized backend that understands layouts, while Word and Excel files go through OpenXML. For tricky content like scanned pages or charts, it applies OCR and vision models. The end result is a structured document you can export to Markdown, JSON, or HTML.</p><p>For most RAG pipelines, Markdown export is the sweet spot. It keeps headings, lists, and tables intact without the overhead of more complex formats. Docling also integrates directly with LangChain and LlamaIndex, so it fits cleanly into most existing pipelines.</p><h4><strong>Breaking documents into chunks</strong></h4><p>A language model can only read so much text at once. After parsing, you need to break documents into smaller pieces called <strong>chunks, </strong>that can be stored, searched, and passed to the model efficiently.</p><p>There’s no one-size-fits-all approach here. The right strategy depends on your documents and your questions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G93bV35k91Wmy8aw7OOJSQ.png" /><figcaption>3 different chunking strategies</figcaption></figure><p><strong>Fixed-size chunking</strong> is the simplest approach: split every document into pieces of a set length (say, 512 tokens), with a small overlap between adjacent chunks so context isn’t lost at the edges. It’s quick to set up and works well as a starting point. The downside is that finding the right size takes trial and error. A chunk that’s too small loses context; one that’s too large buries the relevant sentence. Tools like <strong>IBM watsonx AutoRAG</strong> can automate this search, which saves a lot of manual tuning.</p><p><strong>Hierarchical chunking</strong> is smarter: instead of counting characters, it respects the document’s own structure. Section breaks become chunk breaks. This produces chunks that align with how the document was actually written, which tends to mean better search results. Docling’s <strong>HybridChunker</strong> does this out of the box and can also merge small adjacent chunks so nothing ends up too short.</p><p><strong>Custom chunking</strong> is for when you know exactly what your documents look like. Legal contracts, product data sheets, and internal knowledge-base articles often follow a predictable structure. Writing a custom parser for that structure gives you the most precise and well-labeled chunks, at the cost of needing to maintain it as document formats evolve.</p><h4><strong>Making chunks easier to find</strong></h4><p>Two enrichment steps make a big difference in retrieval quality:</p><ul><li><strong>Contextual summaries </strong>add a brief summary of the surrounding section to each chunk. A chunk on its own might be too narrow to match a broad question; the summary gives it extra surface area for search.</li><li><strong>Metadata </strong>store the page number, document title, section heading, and source alongside each chunk. This lets you filter results at query time. For example, restricting a search to documents from the latest version, a specific department or date range.</li></ul><h3>2. Retrieval</h3><p>With documents ingested and indexed, the retrieval layer’s job is to find the handful of chunks most relevant to each question. Getting this right matters more than almost anything else; even a perfect AI model can’t give a good answer if it’s given the wrong context.</p><h4><strong>Why hybrid search wins</strong></h4><p>There are two main ways to search a document index:</p><ul><li><strong>Keyword search</strong> (BM25) looks for documents that contain the exact words from your query. It’s fast, reliable for precise terms like product codes or proper nouns, and has been the backbone of enterprise search for decades. Its weakness is that it doesn’t understand meaning so it can miss relevant documents that use different words to say the same thing.</li><li><strong>Semantic search</strong> uses AI embeddings to compare the <em>meaning</em> of a query to the meaning of each chunk. It handles synonyms and paraphrasing well, but can be thrown off when exact terminology matters. Searching for “Section 4.2(b)” semantically is unlikely to find exactly what you need.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Gl7Rf2odn3_tG6_OSoRxcQ.png" /><figcaption>Keyword, semantic, and hybrid search compared</figcaption></figure><p><strong>Hybrid search</strong> runs both in parallel and combines the scores. The result set captures both precise matches and conceptually similar content, and it consistently outperforms either approach used alone. There are two common ways to merge the scores: a tunable weighted average, or the simpler <strong>Reciprocal Rank Fusion (RRF)</strong> algorithm, which requires no tuning and is a solid default.</p><h4><strong>OpenSearch as the retrieval engine</strong></h4><p><a href="https://opensearch.org/">OpenSearch</a> is a well-proven open-source search engine that supports both BM25 and vector search natively, making it a natural fit for hybrid RAG.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gO3YTnWKuFI_avkziXOGWg.png" /><figcaption>Illustration of how OpenSearch handles a hybrid search query</figcaption></figure><p>When a query arrives, a coordinating node fans it out to all data shards simultaneously to run keyword and semantic searches in parallel. Results come back from each shard, are normalized and merged in the coordinating node, and then the top matches are fetched and returned. The parallel architecture keeps response times low even over large document collections.</p><h4><strong>Reranking: a second pass for precision</strong></h4><p>Retrieval often casts a wide net. Before passing 50 results to the model, a <strong>reranker</strong> does a more careful job of sorting them. A cross-encoder model reads the query and each chunk together (rather than independently) and scores how well they match. You then pass only the top 3–5 results to the model, which keeps context windows small and answers focused.</p><h3>3. Generation</h3><p>Once you have the right context, the generation step is where the AI model actually writes the answer. This stage has two key decisions: which model to use, and how to control what it gives back.</p><h4><strong>Choosing a model</strong></h4><ul><li>Proprietary APIs such as <strong>ChatGPT</strong> or <strong>Gemini</strong> has the highest capability, and can be chosen when data can leave your environment.</li><li>Open-weight models like <strong>Granite</strong> or <strong>Llama</strong> are self-hosted, and particularly good for use cases with data-residency requirements.</li><li>Fine-tuned models shine in specialized domains with unique terminology or tone.</li></ul><p>For most enterprise teams, open-weight models deployed on their own infrastructure strike the right balance between capability and control.</p><h4><strong>Getting structured output</strong></h4><p>Free-form prose is useful for some use cases, but many enterprise workflows need the AI to return data in a predictable, machine-readable format. Examples include extracting fields from a contract, classifying a support ticket, or filling a form from an email.</p><p>The <a href="https://python.useinstructor.com/">Instructor</a> library makes this straightforward. You define the shape of the output you want using a Pydantic model, and Instructor ensures the AI returns exactly that, with automatically retrying if the output doesn’t validate.</p><p>The <a href="https://python.useinstructor.com/examples/watsonx">example</a> below shows how to pull structured information from a sentence:</p><pre>import os<br><br>import litellm<br>from litellm import completion<br>from pydantic import BaseModel, Field<br><br>import instructor<br>from instructor import Mode<br><br>litellm.drop_params = True  # watsonx.ai doesn&#39;t support `json_mode`<br><br>os.environ[&quot;WATSONX_URL&quot;] = &quot;https://us-south.ml.cloud.ibm.com&quot;<br>os.environ[&quot;WATSONX_API_KEY&quot;] = &quot;&quot;<br>os.environ[&quot;WATSONX_PROJECT_ID&quot;] = &quot;&quot;<br># Additional options: https://docs.litellm.ai/docs/providers/watsonx<br><br><br>class Company(BaseModel):<br>    name: str = Field(description=&quot;name of the company&quot;)<br>    year_founded: int = Field(description=&quot;year the company was founded&quot;)<br><br><br>client = instructor.from_litellm(completion, mode=Mode.JSON)<br><br>resp = client.create(<br>    model=&quot;watsonx/meta-llama/llama-3-8b-instruct&quot;,<br>    max_tokens=1024,<br>    messages=[<br>        {<br>            &quot;role&quot;: &quot;user&quot;,<br>            &quot;content&quot;: &quot;&quot;&quot;\<br>Given the following text, create a Company object:<br><br>IBM was founded in 1911 as the Computing-Tabulating-Recording Company (CTR), a holding company of manufacturers of record-keeping and measuring systems.<br>&quot;&quot;&quot;,<br>        }<br>    ],<br>    project_id=os.environ[&quot;WATSONX_PROJECT_ID&quot;],<br>    response_model=Company,<br>)<br><br>print(resp.model_dump_json(indent=2))<br>&quot;&quot;&quot;<br>{<br>  &quot;name&quot;: &quot;IBM&quot;,<br>  &quot;year_founded&quot;: 1911<br>}<br>&quot;&quot;&quot;</pre><p>This pattern works especially well with IBM watsonx, which adds enterprise governance, audit logging, and data-residency controls on top of the model inference layer.</p><p>A few prompt engineering tips that consistently help:</p><ul><li><strong>Be explicit in the system prompt</strong> about the model’s role and the format you expect.</li><li><strong>Keep temperature low</strong> (0.0–0.2) for factual questions where you want consistent, reliable answers.</li><li><strong>Set a max token limit</strong> so responses stay concise and costs stay predictable.</li></ul><h4>Agentic approach</h4><p>Another interesting way is to build the retrieval system into a tool and connect it to agentic platforms like <strong>IBM watsonx Orchestrate</strong>, either via OpenAPI or MCP server, and have the AI agent reason and decide the query term to retrieve knowledge based on chat history. The advantage of the agentic approach is that if retrieved result is not relevant, the AI agent can reason and fix the query term to retrieve better results in a loop.</p><h3>4. Evaluation</h3><p>Building a RAG system without measuring it is like driving without a speedometer. You need to know whether changes are actually improvements. And if something breaks, you need to find out before your users do.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_8v5esqLW2Ahoa1cWWmaOw.png" /><figcaption>Two approaches to RAG evaluation</figcaption></figure><p>There are two complementary ways to evaluate a RAG pipeline:</p><h4><strong>Algorithm-based checks: fast and cheap</strong></h4><p>These metrics compare tokens between the generated answer and a reference answer, with no AI calls required. That makes them fast enough to run on every deployment.</p><p>These are best used as automated gates in a CI/CD pipeline — if the score drops below a threshold, flag it for review before it reaches production.</p><h4><strong>LLM-as-a-judge: deeper quality checks</strong></h4><p>For a more nuanced view, you can use a second AI model to evaluate the outputs. This captures things that token counts miss — like whether the answer actually makes sense given the question.</p><p>LLM-as-a-judge evaluation typically covers two areas:</p><p><strong>Did we retrieve the right context?</strong></p><ul><li>Did the relevant document appear in the results at all? <em>(hit rate)</em></li><li>Was it near the top? <em>(reciprocal rank, NDCG)</em></li><li>Was most of what we retrieved actually useful? <em>(precision, context relevance)</em></li></ul><p><strong>Did we generate a good answer?</strong></p><ul><li>Is every claim backed by the retrieved context? <em>(faithfulness)</em></li><li>Does the answer actually address what was asked? <em>(answer relevance)</em></li><li>How close is it to the ideal answer? <em>(answer similarity)</em></li></ul><p>LLM-as-a-judge takes longer and costs more, so a practical approach is to run algorithm-based checks on every release and LLM-as-a-judge periodically against a curated benchmark set.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n-qoDX0adgpvhuzFS84h9A.png" /><figcaption>Registry and monitoring are key in an RAG governance flow</figcaption></figure><h3>Closing thoughts</h3><p>If you’re starting from scratch, this is the order of impact we’d recommend focusing on:</p><ol><li><strong>Get parsing right first:</strong> downstream quality is limited by the quality of your extracted text. Spend time configuring Docling for your document types before touching anything else.</li><li><strong>Pick a chunking strategy that fits your content:</strong> hierarchical chunking works well for structured documents; custom chunking gives the best results when you control the format.</li><li><strong>Use hybrid search:</strong> it consistently outperforms either keyword or semantic search alone and takes only moderate extra effort to set up.</li><li><strong>Add structured output:</strong> if your use case needs machine-readable responses, Instructor + Pydantic removes a lot of fragile prompt engineering.</li><li><strong>Automate evaluation from day one:</strong> it’s much easier to maintain quality when you’re catching regressions automatically rather than reactively.</li></ol><p>The tools covered above are a solid starting point for building something that scales.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gWbYOXFnbGXiBw1zkGfjrA.png" /><figcaption>Illustration of an end-to-end RAG system for enterprise</figcaption></figure><p><a href="https://medium.com/@jameschen_78678/building-an-end-to-end-rag-system-for-the-enterprise-part-ii-62901a4925a7?source=friends_link&amp;sk=347a88a5574c7d65ea926f9b56561d16">Part II</a> of this post covers the design aspect of such enterprise RAG system.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b821c88527f6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Getting Started with the IBM watsonx Orchestrate API]]></title>
            <link>https://medium.com/@jameschen_78678/getting-started-with-the-ibm-watsonx-orchestrate-api-eabb8e4b27e1?source=rss-c16867ccea73------2</link>
            <guid isPermaLink="false">https://medium.com/p/eabb8e4b27e1</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[James Chen]]></dc:creator>
            <pubDate>Fri, 24 Jul 2026 06:00:15 GMT</pubDate>
            <atom:updated>2026-07-24T06:00:15.923Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7MGa3ppE-a98PS4PoD8f5g.jpeg" /></figure><p><strong>IBM watsonx Orchestrate</strong> exposes a REST API that lets you integrate AI agents directly into your applications and workflows. Whether you are building an internal automation pipeline or a customer-facing product, the API gives you programmatic access to agent orchestration, streaming responses, and an OpenAI-compatible chat completions endpoint.</p><p>This guide walks through the four essential operations you need to get up and running: obtaining an authentication token, listing your agents, sending a message with streaming, and calling the chat completions endpoint. All examples are shown in both <strong>cURL</strong> and <strong>Python</strong>.</p><h3><strong>Prerequisites</strong></h3><p>Set these environment variables once and every snippet below will work without modification.</p><pre>export IBM_API_KEY=&quot;your_api_key_here&quot;<br>export WXO_REGION=&quot;your_region&quot;<br>export WXO_INSTANCE_ID=&quot;your_instance_id&quot;</pre><h3><strong>Step 1 : Obtain an IAM Access Token</strong></h3><p><strong>POST</strong> <a href="https://iam.cloud.ibm.com/identity/token">https://iam.cloud.ibm.com/identity/token</a></p><p>All API calls are authenticated with a short-lived IBM IAM bearer token. Exchange your API key for a token before making any other call. Tokens are valid for <strong>3600 seconds (1hr) </strong>and must be refreshed periodically.</p><pre>curl --location &#39;https://iam.cloud.ibm.com/identity/token&#39; \<br>  --header &#39;Content-Type: application/x-www-form-urlencoded&#39; \<br>  --header &#39;Accept: application/json&#39; \<br>  --data-urlencode &#39;grant_type=urn:ibm:params:oauth:grant-type:apikey&#39; \<br>  --data-urlencode &#39;apikey=IBM_API_KEY&#39;</pre><pre>import requests, os<br><br>response = requests.post(<br>    &quot;https://iam.cloud.ibm.com/identity/token&quot;,<br>    headers={&quot;Content-Type&quot;: &quot;application/x-www-form-urlencoded&quot;},<br>    data={<br>        &quot;grant_type&quot;: &quot;urn:ibm:params:oauth:grant-type:apikey&quot;,<br>        &quot;apikey&quot;: os.getenv(&quot;IBM_API_KEY&quot;)<br>    }<br>)<br><br>print(response.json())</pre><pre>{<br>  &quot;access_token&quot;: &quot;eyJhbGciOiJIUz......&quot;,<br>  &quot;refresh_token&quot;: &quot;not_supported&quot;,<br>    &quot;ims_user_id&quot;: 15031254,<br>    &quot;token_type&quot;: &quot;Bearer&quot;,<br>    &quot;expires_in&quot;: 3600,<br>    &quot;expiration&quot;: 1774596742,<br>    &quot;scope&quot;: &quot;ibm openid&quot;<br>}</pre><p><strong>Note:</strong> The response includes <strong>access_token</strong>, <strong>expires_in</strong> (3600), and <strong>token_type: Bearer</strong>. Cache and reuse the token across calls until it expires.</p><h3><strong>Step 2 : List Available Agents</strong></h3><p><strong>GET</strong> /instances/<strong>{instance_id}</strong>/v1/orchestrate/agents</p><p>Retrieve all agents provisioned in your instance. The <strong>id</strong> field from this response is required for all subsequent calls.</p><pre>curl &#39;https://api.$WXO_REGION.watson-orchestrate.cloud.ibm.com/instances/$WXO_INSTANCE_ID/v1/orchestrate/agents&#39; \<br>  --header &#39;Accept: application/json&#39; \<br>  --header &#39;Authorization: Bearer YOUR_ACCESS_TOKEN&#39;</pre><pre>region = os.getenv(&quot;WXO_REGION&quot;)<br>instance_id = os.getenv(&quot;WXO_INSTANCE_ID&quot;)<br><br>response = requests.get(<br>    f&quot;https://api.{region}.watson-orchestrate.cloud.ibm.com&quot;<br>    f&quot;/instances/{instance_id}/v1/orchestrate/agents&quot;,<br>    headers={&quot;Authorization&quot;: f&quot;Bearer {token}&quot;}<br>)<br><br>print(response.json())</pre><pre>[<br>    {<br>        &quot;id&quot;: &quot;4ba981cb...&quot;,<br>        &quot;tenant_id&quot;: &quot;b319196991014918aeee542a3527dbd7...&quot;,<br>        &quot;name&quot;: &quot;VoiceAgentTest_95874q&quot;,<br>        &quot;description&quot;: &quot;testing voice agent&quot;,<br>        &quot;instructions&quot;: &quot;&quot;,<br>        &quot;tools&quot;: [],<br>        &quot;collaborators&quot;: [],<br>        &quot;knowledge_base&quot;: [],<br>        &quot;llm&quot;: &quot;groq/openai/gpt-oss-120b&quot;,<br>        &quot;style&quot;: &quot;default&quot;,<br>        &quot;supported_apps&quot;: [],<br>        &quot;created_by&quot;: &quot;IBMid-696000C5O6&quot;,<br>        &quot;created_on&quot;: &quot;2026-03-20T06:22:49.593367Z&quot;,<br>        &quot;updated_by&quot;: &quot;IBMid-696000C5O6&quot;,<br>        &quot;updated_at&quot;: &quot;2026-03-25T08:33:10.818168Z&quot;,<br>        &quot;deleted_by&quot;: null,<br>        &quot;deleted_at&quot;: null,<br>        &quot;environments&quot;: [],<br>        ...<br>    }<br>]</pre><p><strong>Note:</strong> Each agent object includes its <strong>id</strong>, <strong>name</strong>, <strong>llm</strong> model binding, tools, collaborators, and audit timestamps. Store the <strong>id</strong> of the agent you want to interact with.</p><h3><strong>Step 3 : Send a Message with Streaming</strong></h3><p><strong>POST</strong> /instances/<strong>{instance_id}</strong>/v1/orchestrate/runs?stream=true</p><p>The <strong>/runs</strong> endpoint starts an agent run and streams server-sent events back to the client — ideal for real-time UIs and long-running tasks.</p><pre>curl &#39;https://api.$WXO_REGION.watson-orchestrate.cloud.ibm.com/instances/$WXO_INSTANCE_ID/v1/orchestrate/runs?stream=true&amp;stream_timeout=120000&amp;multiple_content=true&#39; \<br>  --header &#39;IAM-API-KEY: IBM_API_KEY&#39; \<br>  --header &#39;Authorization: Bearer YOUR_ACCESS_TOKEN&#39; \<br>  --header &#39;Content-Type: application/json&#39; \<br>  --data &#39;{<br>    &quot;message&quot;: { &quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hello, how can you help me?&quot; },<br>    &quot;agent_id&quot;: &quot;YOUR_AGENT_ID&quot;<br>  }&#39;</pre><pre>response = requests.post(<br>    f&quot;https://api.{region}.watson-orchestrate.cloud.ibm.com&quot;<br>    f&quot;/instances/{instance_id}/v1/orchestrate/runs&quot;,<br>    params={&quot;stream&quot;: &quot;true&quot;, &quot;stream_timeout&quot;: &quot;120000&quot;, &quot;multiple_content&quot;: &quot;true&quot;},<br>    headers={<br>        &quot;IAM-API-KEY&quot;:   os.getenv(&quot;IBM_API_KEY&quot;),<br>        &quot;Authorization&quot;: f&quot;Bearer {token}&quot;,<br>        &quot;Content-Type&quot;:  &quot;application/json&quot;<br>    },<br>    json={<br>        &quot;message&quot;: {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hello, how can you help me?&quot;},<br>        &quot;agent_id&quot;: agent_id<br>    },<br>    stream=True<br>)<br>response.raise_for_status()<br><br>for line in response.iter_lines():<br>    if line:<br>        print(line)   # process each SSE event</pre><pre>Raw: b&#39;{&quot;id&quot;: &quot;177459...&quot;, &quot;event&quot;: &quot;run.started&quot;, &quot;data&quot;: {&quot;run_id&quot;: &quot;6001dfb7...&quot;, &quot;agent_id&quot;: &quot;4ba981cb...&quot;, &quot;thread_id&quot;: &quot;f05f080a...&quot;}}&#39;<br>Raw: b&#39;{&quot;id&quot;: &quot;177459...&quot;, &quot;event&quot;: &quot;message.started&quot;, &quot;data&quot;: {&quot;thread_id&quot;: &quot;f05f080a...&quot;, &quot;run_id&quot;: &quot;6001dfb7...&quot;}}&#39;</pre><p><strong>Event sequence:</strong> run.started → message.started → <em>_…chunks…_</em> → run.completed. Each event carries a <strong>run_id</strong> and <strong>thread_id</strong> for correlating messages across a conversation.</p><h3><strong>Step 4 : Chat Completions (optional)</strong></h3><p><strong>POST</strong> /instances/<strong>{instance_id}</strong>/v1/orchestrate/<strong>{agent_id}</strong>/chat/completions</p><p>For applications already built on the OpenAI Chat Completions interface, watsonx Orchestrate provides a drop-in compatible endpoint per agent — no client library changes required.</p><pre>curl &#39;https://api.$WXO_REGION.watson-orchestrate.cloud.ibm.com/instances/$WXO_INSTANCE_ID/v1/orchestrate/$WXO_AGENT_ID/chat/completions&#39; \<br>  --header &#39;Authorization: Bearer YOUR_ACCESS_TOKEN&#39; \<br>  --header &#39;Content-Type: application/json&#39; \<br>  --data &#39;{<br>    &quot;messages&quot;: [{ &quot;role&quot;: &quot;human&quot;, &quot;content&quot;: &quot;What services do you offer?&quot; }],<br>    &quot;model&quot;: &quot;open-ai/gpt-oss-120b&quot;,<br>    &quot;stream&quot;: false<br>  }&#39;</pre><pre>response = requests.post(<br>    f&quot;https://api.{region}.watson-orchestrate.cloud.ibm.com&quot;<br>    f&quot;/instances/{instance_id}/v1/orchestrate/{agent_id}/chat/completions&quot;,<br>    headers={<br>        &quot;Authorization&quot;: f&quot;Bearer {token}&quot;,<br>        &quot;Content-Type&quot;:  &quot;application/json&quot;<br>    },<br>    json={<br>        &quot;messages&quot;: [{&quot;role&quot;: &quot;human&quot;, &quot;content&quot;: &quot;What services do you offer?&quot;}],<br>        &quot;model&quot;:    &quot;open-ai/gpt-oss-120b&quot;,<br>        &quot;stream&quot;:   False<br>    }<br>)<br><br>print(response.json())</pre><pre>{<br>  &quot;id&quot;: &quot;chatcmpl-abc123&quot;,<br>  &quot;object&quot;: &quot;chat.completion&quot;,<br>  &quot;created&quot;: 1234567890,<br>  &quot;model&quot;: &quot;open-ai/gpt-oss-120b&quot;,<br>  &quot;choices&quot;: [<br>    {<br>      &quot;index&quot;: 0,<br>      &quot;message&quot;: {<br>        &quot;role&quot;: &quot;assistant&quot;,<br>        &quot;content&quot;: &quot;We offer a wide range of services including...&quot;<br>      },<br>      &quot;finish_reason&quot;: &quot;stop&quot;<br>    }<br>  ],<br>  ...<br>}</pre><p><strong>Note:</strong> The response mirrors the OpenAI <strong>ChatCompletion</strong> schema, <strong>choices[0].message.content</strong> holds the reply and <strong>finish_reason</strong> indicates why generation stopped.</p><h3>What’s Next</h3><p>With authentication, agent discovery, and two interaction patterns in hand, you have everything required to build production-grade integrations on top of watsonx Orchestrate. From here, explore tool-calling to connect agents to external APIs, multi-agent collaboration, and context management to maintain conversation state across turns.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eabb8e4b27e1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Upgrading Your Existing Knowledge Base into an AI Chatbot That Answers with Images]]></title>
            <link>https://medium.com/@jameschen_78678/upgrading-your-existing-knowledge-base-into-an-ai-chatbot-that-answers-with-images-cc052c13acef?source=rss-c16867ccea73------2</link>
            <guid isPermaLink="false">https://medium.com/p/cc052c13acef</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[knowledge-management]]></category>
            <dc:creator><![CDATA[James Chen]]></dc:creator>
            <pubDate>Thu, 23 Jul 2026 14:12:12 GMT</pubDate>
            <atom:updated>2026-07-23T14:12:12.051Z</atom:updated>
            <content:encoded><![CDATA[<h3>The problem</h3><p>Your team already has a knowledge management tool — and it’s well-used. Years of how-to articles, troubleshooting guides, and step-by-step procedures live inside it, many of them rich with visual aids, including annotated screenshots showing where to click, diagrams of network topology, photos of physical printer setup steps.</p><p>Today, that means opening a portal, typing a search query, hoping the right article surfaces, and then reading through it. That workflow was fine before AI became mainstream. Now users expect more: they want to type a question in plain English: <strong><em>How do I connect the office printer on Windows? </em></strong>and get back a direct, conversational answer that includes the <strong>relevant screenshots</strong> from the original article, not just a link to go read it themselves.</p><p>The gap isn’t the content. The gap is the <strong>interface</strong> and the <strong>richness of the response</strong>. Text-only chatbot answers fail when the source material is inherently visual. A step-by-step printer setup guide without its annotated images is significantly less useful.</p><p>This writeup solves that gap by keeping your existing knowledge base exactly as it is, and adding an AI layer on top that:</p><ol><li>Understands natural language questions</li><li>Retrieves the right article from your existing system</li><li>Extracts and surfaces the embedded images alongside the text answer, so users see the visual aids, not just words describing them</li></ol><h3>Solution overview</h3><p>The system adds an AI layer on top of an existing knowledge base without modifying it. <strong>IBM watsonx Orchestrate </strong>is an agentic platform that<strong> </strong>acts as the AI brain — the LLM receives the user’s natural language question, decides to call an external search tool, and synthesises a rich response from the result. Critically, that response includes <strong>images extracted from the source articles</strong>, served to the chat widget with correct CORS headers so they render inline.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KgItrIWLl7Oc-Q_A6m3x3w.png" /><figcaption>Response from AI with images from relevant source articles rendered inline</figcaption></figure><p>The <strong>tool-km </strong>(km, short for knowledge management)<strong> </strong>is a FastAPI micro service we control. It handles authenticating against the knowledge base API, fetching articles, parsing HTML bodies, extracting both text and embedded images, and resolving file attachments (PDFs, DOCX) based on a single <strong>keyword</strong> argument that the AI agent decides based on user query. Nothing in the existing knowledge base changes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N4e7XiJ8coVXI2Nv-lZg5g.png" /><figcaption>Supporting context provided to the LLM agent from <strong>tool-km</strong></figcaption></figure><p>Everything runs as containers on IBM Cloud Code Engine and communicates over HTTPS.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SOSXj0Pqqnxfk9qaL2DuMQ.png" /><figcaption>High-level architecture</figcaption></figure><h3>Key design decisions</h3><ul><li><strong>Why not modify the existing knowledge base?<br></strong>The entire point of this architecture is non-invasive augmentation. The existing knowledge management system keeps running unchanged. The AI layer reads from it as a consumer. No schema changes, no data migration, no disruption to existing workflows or users.</li><li><strong>Why extract images rather than just link to the article?<br></strong> A link sends the user back to the original portal, which is the exact experience we’re replacing. Extracting images from the HTML and surfacing them inline in the chat widget means the user gets a complete, self-contained answer without context-switching. For visually-guided tasks like hardware setup, this is the difference between a useful response and a frustrating one.</li><li><strong>Why a dedicated image proxy service?<br></strong> Browser security (CORS, mixed-content) makes it impossible to render private COS images directly in a chat widget. A dedicated proxy sidecar solves this cleanly without coupling image-serving concerns into <strong>tool-km</strong>. It also scales independently — and to zero when idle.</li></ul><h3>Closing thoughts</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7AmipWTVoSzgC1eG_ACICQ.png" /><figcaption>Illustration of different knowledge sources ingested as tools into the AI system</figcaption></figure><p>Multiple knowledge sources could also be added alongside <strong>tool-km</strong>, such as SharePoint, ServiceNow, or Confluence. The LLM routes queries to the right tool automatically and each tool can<strong> </strong>follow the similar<strong> </strong>image extraction strategy via batch jobs.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cc052c13acef" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Enhancing Image Text Extraction with LLM and OCR]]></title>
            <link>https://medium.com/@jameschen_78678/enhancing-image-text-extraction-with-llm-and-ocr-8221cb555cc5?source=rss-c16867ccea73------2</link>
            <guid isPermaLink="false">https://medium.com/p/8221cb555cc5</guid>
            <category><![CDATA[ocr]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[llama-3]]></category>
            <dc:creator><![CDATA[James Chen]]></dc:creator>
            <pubDate>Sat, 17 May 2025 01:28:01 GMT</pubDate>
            <atom:updated>2025-05-17T01:28:01.568Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OdICm0hw9NkL88bvLq7FrA.jpeg" /><figcaption>Image source: <a href="https://en.wikipedia.org/wiki/Optical_character_recognition">https://en.wikipedia.org/wiki/Optical_character_recognition</a></figcaption></figure><h3>Introduction</h3><p>The rapid advancement of multimodal Large Language Models (LLMs) has opened new possibilities for AI to process both textual and visual information. These models can now understand images, extract text, and even perform more complex tasks like reasoning about the content of images in a way that was previously only possible with separate Optical Character Recognition (OCR) tools. But what if we could combine the power of LLM and OCR to create a more robust solution for image text extraction and understanding?</p><p>In this blog post, we explore how multimodal LLM, specifically Meta’s Llama 3.2, can complement OCR systems like Tesseract to enhance text extraction and interpretation from images.</p><h3>Llama 3.2: Meta’s Multimodal Marvel</h3><p>Llama 3.2 is Meta’s open-source LLM that can process both text and images. This marks a significant step forward in multimodal AI systems, where the model is capable of understanding high-resolution images, extracting textual content, and even relating those visual inputs to textual knowledge.</p><p><strong>Key Features of Llama 3.2:</strong></p><ul><li><strong>Multimodal capabilities:</strong> Llama 3.2 can handle both text and image inputs</li><li><strong>Scale:</strong> model comes in 11B and 90B parameter sizes, with robust vision models</li><li><strong>Resolution support:</strong> Llama 3.2 can process high-resolution images (up to 1120x1120 pixels)</li><li><strong>Adapter weights:</strong> to integrate image understanding into the pre-existing language model, Llama 3.2 uses adapter weights, which consist of cross-attention layers that feed image encoder representations into the language model</li></ul><p>This integration allows the model to process images in conjunction with text while preserving the language-only capabilities of the Llama model. Developers can leverage Llama 3.2 as a drop-in replacement for earlier versions, such as Llama 3.1, while gaining the benefit of image understanding.</p><h3>OCR with Tesseract: A Tried-and-Tested Solution</h3><p>While Llama 3.2 provides a powerful, multimodal solution, the integration of traditional OCR tools like Tesseract can still be highly valuable, especially when extracting text from images with varying qualities or formats.</p><p><strong>What is Tesseract OCR?</strong></p><p>Tesseract is an open-source OCR engine developed by Google, capable of recognizing text from images. It supports a wide range of image types (e.g., JPEG, PNG, TIFF) and is used in a variety of applications, from scanning documents to text extraction from screenshots.</p><p>For Python developers, the <strong>python-tesseract</strong> library is a popular wrapper around the Tesseract engine, enabling easy integration into Python-based workflows. With Tesseract, you can:</p><ul><li>Extract text from images</li><li>Retrieve the text along with bounding box coordinates (for localization)</li><li>Handle a variety of image formats via the Pillow and Leptonica libraries</li></ul><p>This makes Tesseract a robust tool for text extraction tasks, but it is not without its limitations — particularly in cases where the image quality is poor, or the text is embedded in complex visual contexts.</p><h3>How LLM and OCR Can Complement Each Other</h3><p>By combining the strengths of OCR (text extraction) and multimodal LLMs (image/text understanding), we can create more advanced solutions for image-text interactions. Here are a few ways in which these technologies can work together:</p><p><strong>1. Improved LLM Responses with OCR</strong></p><p><strong>Approach:</strong></p><p>One of the simplest yet most powerful ways to leverage OCR with LLMs is to use OCR-extracted text as part of the prompt when interacting with the model. The extracted text can provide context for the LLM, allowing it to refer back to the OCR data when necessary.</p><p><strong>Use Case:</strong></p><p>For example, if you’re working with an image containing text in different sections, you can pass the OCR result into the prompt and ask the model to interpret or analyze it accordingly. The LLM can use this extracted text to generate more accurate and context-aware responses.</p><p><strong>Result:</strong></p><ul><li><strong>OCR-only:</strong> Extracted text from the image</li><li><strong>LLM-only:</strong> Model interpretation without OCR input</li><li><strong>OCR + LLM:</strong> Text extraction combined with context from LLM analysis</li></ul><pre># example prompt for OCR + LLM<br>prompt = f&quot;&quot;&quot;<br>You are an AI assistant specialized in understanding images<br> <br>user question:<br>{query}<br> <br>use following text extracted from object character recognition:<br>{ocr_text}<br>&quot;&quot;&quot;</pre><p>This approach provides a way for the LLM to reference OCR results directly, improving response quality and relevance.</p><p><strong>2. Correcting and Enhancing OCR Results with LLM</strong></p><p><strong>Approach:</strong></p><p>OCR engines like Tesseract may occasionally misinterpret or miss certain details in text extraction, particularly when images are noisy or contain unusual fonts. By passing the OCR-extracted text (along with localization data, such as bounding box coordinates) into an LLM, you can ask the model to correct or enhance the extracted text.</p><p><strong>Use Case:</strong></p><p>You could pass the OCR results to the LLM and ask it to make corrections based on the surrounding context or additional information provided by the image. For instance, if the OCR engine fails to recognize certain words or misinterprets characters, the LLM can make intelligent corrections based on the context of the image or the surrounding text.</p><p><strong>Result:</strong></p><p>By comparing the results from:</p><ul><li><strong>OCR-only:</strong> The raw, potentially flawed OCR output</li><li><strong>OCR + LLM:</strong> The improved version of the text, as corrected by the LLM</li></ul><pre># example prompt for OCR + LLM<br>prompt = f&quot;&quot;&quot;<br>You are an AI assistant specialized in understanding images.<br>Your task is to review text extracted from OCR.<br> <br>OCR extracted text with location:<br>{ocr_text}<br> <br>Correct any errors from OCR and return text<br>&quot;&quot;&quot;<br>   <br>Copy code<br>      # example ocr_text json<br># left/top/width/height are boundries/locations for text boxes<br>orc_text = [<br>    {<br>        &quot;index&quot;: 13,<br>        &quot;text&quot;: &quot;Tax Invoice&quot;,<br>        &quot;left&quot;: 177,<br>        &quot;top&quot;: 198,<br>        &quot;width&quot;: 225,<br>        &quot;height&quot;: 22<br>    },<br>    {<br>        &quot;index&quot;: 16,<br>        &quot;text&quot;: &quot;Ahmedabad Shopping Mall&quot;,<br>        &quot;left&quot;: 170,<br>        &quot;top&quot;: 220,<br>        &quot;width&quot;: 178,<br>        &quot;height&quot;: 17<br>    },<br>    ...<br>  ]</pre><p>The LLM’s ability to correct and improve OCR results can lead to more accurate text extraction, especially in challenging image conditions.</p><p><strong>3. Highlighting Specific Text in Images</strong></p><p><strong>Approach:</strong></p><p>When working with an image that contains multiple blocks of text, it may be useful to extract and highlight specific portions of the image. By passing both OCR-extracted text and its bounding box coordinates into an LLM, you can request the model to return the relevant areas of the image or highlight the text corresponding to a user query.</p><p><strong>Use Case:</strong></p><p>For example, if you’re processing a document image and need to find the portion of the image that contains a specific keyword or phrase, you can use OCR to identify and localize the text, then prompt the LLM to identify the relevant text and return its bounding box coordinates.</p><p>The role of the LLM here is to understand user query and relate them to text extracted from OCR, without LLM, we may need to use other techniques such as <strong>fuzzy match</strong> or <strong>sematic search</strong> (via embeddings)</p><p><strong>Result:</strong></p><ul><li><strong>Box area:</strong> exact location of the highlighted text</li><li><strong>Image with highlight:</strong> section of the image that corresponds to the user query, visually highlighted</li></ul><pre>prompt = f&quot;&quot;&quot;<br>You are an AI assistant specialized in analyzing images.<br>Here&#39;s the result from OCR: {ocr_text}<br> <br>Return the following JSON object with 2 keys:<br>- answer: contains the answer to find {query}, note that you should correct mistakes made by OCR<br>- reference: a single numeric index used from OCR as references that are relevant<br> <br>Return only valid JSON. Nothing else.<br>&quot;&quot;&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*R_WhtJ-pPToVhtMkUzKKaQ.png" /><figcaption>Example: If we are looking for <strong>DATE</strong> of this invoice (image source: <a href="https://github.com/KMKnation/Four-Point-Invoice-Transform-with-OpenCV/blob/master/README.md">https://github.com/KMKnation/Four-Point-Invoice-Transform-with-OpenCV/blob/master/README.md</a>)</figcaption></figure><pre># example JSON output from LLM with query: date<br>{<br>    &quot;text&quot;: &quot;The date is 25/8/17. The reference index is 43.\n\n```json\n{\n    \&quot;answer\&quot;: \&quot;25/8/17\&quot;,\n    \&quot;reference\&quot;: 43\n}\n```&quot;,<br>    &quot;json&quot;: {<br>        &quot;answer&quot;: &quot;25/8/17&quot;,<br>        &quot;box&quot;: {<br>            &quot;index&quot;: 43,<br>            &quot;text&quot;: &quot;:25/8/17&quot;,<br>            &quot;left&quot;: 358,<br>            &quot;top&quot;: 303,<br>            &quot;width&quot;: 68,<br>            &quot;height&quot;: 15<br>        }<br>    }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*f7oxRDpR73RP4lm5AvGnFA.png" /><figcaption>Returning image with yellow highlight using Python OpenCV</figcaption></figure><h3>Conclusion</h3><p>The integration of multimodal LLMs like Llama 3.2 and OCR technologies like Tesseract opens up a new world of possibilities for text extraction and understanding from images. By combining the strengths of both approaches, you can enhance the quality of OCR results, generate more context-aware responses, and even extract and highlight specific parts of an image based on textual content.</p><p>As AI models continue to evolve, we can expect even more sophisticated solutions that seamlessly integrate vision and language, driving forward the capabilities of image-text interactions in various applications, from document processing to interactive AI assistants.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8221cb555cc5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What can we do to help convert leads?]]></title>
            <link>https://medium.com/data-science/what-can-we-do-to-help-convert-leads-8cbbcaee3e61?source=rss-c16867ccea73------2</link>
            <guid isPermaLink="false">https://medium.com/p/8cbbcaee3e61</guid>
            <category><![CDATA[visualization]]></category>
            <category><![CDATA[statistics]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[James Chen]]></dc:creator>
            <pubDate>Mon, 28 Sep 2020 17:57:17 GMT</pubDate>
            <atom:updated>2020-09-28T18:34:01.751Z</atom:updated>
            <content:encoded><![CDATA[<h4>A brief introduction to visualizing results from machine learning</h4><blockquote><strong>Overview</strong></blockquote><p>This blog post is to demonstrate the studying and interpretation of the relationships between machine learning model output and various input variables — as a starting point to optimize lead conversion.</p><p>Business context-wise, we will be learning about an online education company called <em>X Education</em><strong> </strong>that<strong> </strong>sells online courses to industry professionals. With limited resources, it is important for the company to identify which users are more likely to be converted into leads.</p><p>We will mostly be using R to perform the analysis and visualization.</p><p><strong>Live demo: </strong><a href="https://jameschen4.shinyapps.io/leads/">https://jameschen4.shinyapps.io/leads/</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7AZ0VghVOWBRu4vIkue_Yw.png" /><figcaption>Image by Author: Screenshot of the final visualization built with reactable and R shiny</figcaption></figure><p>library(data.table)<br>library(caret)<br>library(xgboost)<br>library(pROC)<br>library(xgboostExplainer)<br>library(dplyr)<br>library(tidyr)<br>library(reactable)<br>library(htmltools)</p><blockquote><strong>Data</strong></blockquote><p>The data is available on <a href="https://www.kaggle.com/ashydv/leads-dataset">Kaggle</a>. One good thing about this data set is that it comes with a data dictionary, which explains what key columns mean. There are 37 variables and around 10k records.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/992/1*2XH4Xh4pHFPuR6zuZhQnyA.png" /><figcaption>Image by Author: taken from the data dictionary file</figcaption></figure><blockquote><strong>Preprocessing</strong></blockquote><p>There is already a well-written EDA by <a href="https://www.kaggle.com/ashydv/lead-scoring-logistic-regression/output">Ashish</a> with Python. We will be taking the cleaned dataset directly from him, and focus on the simulation and visualization part.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rXF4ze9oBENuiXQg8NpgNw.png" /><figcaption>Image by Author: Raw data before cleaning</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hh33EGVBMbkwcTAQ-z3SpA.png" /><figcaption>Image by Author: Clean and preprocessed data</figcaption></figure><p>We will also be dropping these columns, which are mainly related to leads:</p><ul><li>Lead Quality (Low in Relevance, Might be, Not Sure, Worst)</li><li>Lead Origin (Landing Page Submission, Lead Add Form, Lead Import)</li><li>Lead Source (Facebook, Google, Olark Chat, Organic Search..etc)</li></ul><blockquote><strong>Training</strong></blockquote><p>Here we will train an XGBoost model with 10-fold cross-validation, where the target variable is if the user is converted into a lead (0- No, 1-Yes). The code below are taken directly from <a href="https://medium.com/applied-data-science/new-r-package-the-xgboost-explainer-51dd7d1aa211">David Foster</a>.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/80011173791c5192e477d95825190e0b/href">https://medium.com/media/80011173791c5192e477d95825190e0b/href</a></iframe><p>Turns out the baseline model already gives good result: AUC 0.9650633.</p><p>Using the <strong>xgboostExplainer</strong> package, we are able to identify few important variables that could help lead conversion.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/dc41398b44503a39ee7441adb25d3482/href">https://medium.com/media/dc41398b44503a39ee7441adb25d3482/href</a></iframe><p>The package creates a waterfall chart which can help us identify the most important contributing factors when it comes to lead conversion.</p><p>For the record below (taken from the test set):</p><p>Prediction: 0.9630512</p><p>Weight: 3.260574</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/811/1*I9_9qQRh-vcwXBiwBTKHKQ.png" /><figcaption>Image by Author: Sample output from<strong> </strong>xgboostExplainer<strong> </strong>on a single prediction</figcaption></figure><p>After reviewing some more cases, we have identified a list of 6 variables that may help convert a lead:</p><ul><li>Total.Time.Spent.on.Website</li><li>Last.Activity_SMS.Sent/Last.Notable.Activity_SMS.Sent</li><li>Tags_Will.revert.after.reading.the.email</li><li>Specialization_Other_Specialization</li><li>Tags_Lost.to.EINS</li><li>TotalVisits</li></ul><blockquote><strong>Simulation</strong></blockquote><p>The core idea here is that if the variables are independent, we could play around with different inputs to see how the predictions will change.</p><p>We already know from Ashish’s EDA that there isn’t any serious collinearity (usually when VIF &gt; 5):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/816/1*9jXxIr-Tkvx_th8li_WSJA.png" /><figcaption>Source: <a href="https://www.kaggle.com/ashydv/lead-scoring-logistic-regression/notebook">https://www.kaggle.com/ashydv/lead-scoring-logistic-regression/notebook</a></figcaption></figure><p><strong>Numerical Variables</strong></p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6a4d81ccfebb579e9446b9c16aa3488b/href">https://medium.com/media/6a4d81ccfebb579e9446b9c16aa3488b/href</a></iframe><p>First we will create a list of percentages and a list of seeds to iterate though. The percentage here refers to the percentage of user base that could be affected. For example, if perc = 0.2, that means we will randomly select 20% of the records (again, from the test data), and add 600 seconds to the Total.Time.Spent.on.Website variable, while other variables remain unchanged for those 20% records.</p><p>The diversified seeding here is just to make sure the simulation is more robust. For every iteration, we will take the median of the predictions. Note that here baseline of median predictions before we simulate anything is around 0.152.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1004/1*gGxCvgarqzhT9hBo0vkUKw.png" /><figcaption>Image by Author: Simulation on each variable with different % user base and seeding combinations</figcaption></figure><p>A side note is that, despite there are no serious collinearity present, when TotalVisits = 0, Total.Time.Spent.on.Website must also be 0 as well. For simplicity, we are ignoring this for now.</p><p>Another note is that here we are only demonstrating a fixed boost on the continuous variables (for example, add 600 seconds), and we can always try different combinations as well.</p><p><strong>One-hot Variables</strong></p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1b7c2b48c430a36132a08cd07102da66/href">https://medium.com/media/1b7c2b48c430a36132a08cd07102da66/href</a></iframe><p>For one-hot variables, one way is to split up the 0s and 1s first, and then randomly take a percentage on the 0’s (target) and convert it into 1’s — which means that the SMS Sent becomes the last activity for those selected records.</p><p>We then repeat the process for the remaining 4 variables, and the aggregated conversion rates are as below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lw_IHKRMtBbE2lelNb-BRA.png" /><figcaption>Image by Author: Aggregated lead conversion rate simulations in wide format</figcaption></figure><blockquote><strong>Visualization</strong></blockquote><p>Before visualization, let’s also add more details to the final data:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i5LFSpMGgFpJoVQnXS-aDA.png" /><figcaption>Image by Author: Additional information to help visualization</figcaption></figure><p>For the actual visualization, we will construct a heatmap in a table format with the <a href="https://glin.github.io/reactable/articles/examples.html"><em>reactable</em></a><em> </em>package — best part? It is compatible with R Shiny.</p><p><strong>Live demo: </strong><a href="https://jameschen4.shinyapps.io/leads/">https://jameschen4.shinyapps.io/leads/</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7AZ0VghVOWBRu4vIkue_Yw.png" /><figcaption>Image by Author: Screenshot of the final visualization built with reactable and R shiny</figcaption></figure><p>Some thoughts behind this visualization:</p><ul><li>Upward and download arrow icons are to illustrate increase/decrease objectives — which is more eye-catching</li><li>Baseline categorical and numerical are just reference points on current test data (before any simulation) — and will be used to evaluate how feasible it is to set up said objective. For example, collecting 50% more users from EINS Education (a competitor company) may not be realistic as current baseline is only 2%</li><li>Numbers often are aligned right, and texts are aligned left. However, since we only have a few handful of numbers, aligning center looks better with % and #</li><li>Emphasize <strong>X=10%</strong> on both the first column of the lead simulations as well as overhead description with <strong>X% of user…</strong></li></ul><p>High priority questions to ask next:</p><ul><li>What can we do to attract users from EINS Education?</li><li>What strategies EINS education are using?</li><li>Why do users buy from us and EINS both?</li><li>Are our email and SMS campaigns successful — where can we get more data on this?</li><li>Why users without specialization are less likely to convert? Is it because they actually don’t have one, or that they don’t want to tell us this information?</li></ul><blockquote><strong>Final Thoughts</strong></blockquote><p>We focus on objectives here (what we want to achieve), as opposed to the actual approach we are taking, because it may require deeper analysis and domain knowledge of Online Education to come up with solid optimization plan (for example, to study why certain users are spending 2000+ seconds on the website — is it because of certain content?).</p><p>Also note that in proper setting, there should be A/B testing to validate correlations revealed by the XGBoost model. But again, the analysis in this blog should only be served as the starting point of lead conversion optimization, and it is an iterative process to refine the analysis with different stakeholders.</p><p>Machine learning model is often perceived as a blackbox, and the community is well aware of this (hence we see packages like xgboostexplainer and shap). Sometimes even the simplest linear regression is hard for others to understand — and this is where simulation or sensitivity-based analysis can add value. There are also <a href="https://datascience.stackexchange.com/questions/38743/sensitivity-analysis-of-a-machine-learning-model">other way</a>s to perform such analysis:</p><p><em>From variables </em><em>A, </em><em>B, </em><em>C and </em><em>D; which combination of values of </em><em>A, </em><em>B and </em><em>C (without touching </em><em>D) increases the target </em><em>y value by 10, minimizing the sum of </em><em>A, </em><em>B and </em><em>C.</em></p><p>Questions?</p><p>jchen6912@gmail.com</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8cbbcaee3e61" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/what-can-we-do-to-help-convert-leads-8cbbcaee3e61">What can we do to help convert leads?</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Weak Supervision (Part I)]]></title>
            <link>https://medium.com/@jameschen_78678/weak-supervision-part-i-aca5b810af72?source=rss-c16867ccea73------2</link>
            <guid isPermaLink="false">https://medium.com/p/aca5b810af72</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[visualization]]></category>
            <category><![CDATA[data-visualization]]></category>
            <dc:creator><![CDATA[James Chen]]></dc:creator>
            <pubDate>Sun, 22 Dec 2019 16:08:05 GMT</pubDate>
            <atom:updated>2019-12-22T16:08:05.041Z</atom:updated>
            <content:encoded><![CDATA[<p>This is a short introduction on Snorkel, a Stanford-built system to explore new labeling processes with Weak Supervision.</p><blockquote><strong>Overview</strong></blockquote><p>The original article with links to technical papers can be found <a href="http://ai.stanford.edu/blog/weak-supervision/">here</a>. Part I of this blog will cover overview and challenges of machine learning, particularly within the realm of labeling training data. Part II will then focus more on the technical implementations of Snorkel, based on technical papers published.</p><blockquote><strong>Audience</strong></blockquote><p>The audience of this blog post are data scientists and data analysts. We will create new visualizations on key concepts covered within the original article published by Stanford in Part I. Advanced analytics background is not required to understand this blog post, but is nice to have.</p><blockquote><strong>Objective</strong></blockquote><p>The objective of Part I blog post is to communicate the background and potential challenges data science practitioners might face in supervised machine learning — training data labeling. In real world data science, training data sets might not always be properly labeled.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*39NqCEXUN3gUqc0PYYkHLg.png" /><figcaption>Exhibit 1: A comparison between common types of AI</figcaption></figure><p>The problem statement here is clear: both Classical and Modern ML require large amount of training data sets to be manually labeled, in order to obtain promising predictive results. However, common labeling process often takes domain experts months (if not years) to do so, in a consistent way. Not to mention that when analytic objectives change, labels require re-tagging too.</p><p><strong><em>Exhibit 1 is based on below section from the original article, along with additional information for new perspectives.</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6adOuTn-diKcH9hNb_sxfQ.png" /></figure><p>In Traditional Supervision, it is common to identify data points that lie closer to the decision boundary, and prioritize domain experts’ time on those data first, hoping that learning can be based on more valuable information.</p><p>There are also ways to prioritize labeling data points over outliers in the cyber security setting, where attack signals only account for a tiny fraction compared to normal web traffics. The <a href="https://people.csail.mit.edu/kalyan/AI2/">paper</a> can be found here.</p><p>Snorkel, however, utilizes Weak Supervision, which not only includes such prioritization method, but also with other label generation approaches.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ie16QP2-t9J1_mXTSEB0eA.png" /><figcaption>Exhibit 2: Structural thinking on ways to collect more training labels</figcaption></figure><p>Accordingly to the article, Weak Supervision is capable of leverage both manual and automatic labeling methods to get more labeled data, in a less painful way, while maintaining promising results.</p><p>Weak Supervision can take inputs from both domain experts (high-level abstracts similar to Expert Systems) and non-experts, as there are internal mechanisms to deal with conflicts and correlations (we will cover in Part II). Varies input formats are also supported, including task-specific heuristics, regular express patterns (mostly for NLP), and generic rules of thumb.</p><p><strong><em>Exhibit 2 is based on below section from the original article.</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l0LWq_VyRNJjut4twpysMw.png" /></figure><p>This sums up for the Part I. We will cover the technical aspects of Weak Supervision in Part II. Stay tuned!</p><p>Questions?</p><p>jchen6912@gmail.com</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=aca5b810af72" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How long do popular songs last on Spotify, across different places?]]></title>
            <link>https://medium.com/@jameschen_78678/how-long-do-popular-songs-last-on-spotify-across-different-places-74d612b28eb8?source=rss-c16867ccea73------2</link>
            <guid isPermaLink="false">https://medium.com/p/74d612b28eb8</guid>
            <category><![CDATA[data-visualization]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[design]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[James Chen]]></dc:creator>
            <pubDate>Sun, 27 Oct 2019 09:51:29 GMT</pubDate>
            <atom:updated>2019-10-27T09:51:29.986Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Introduction to heatmap visualization with D3.</em></p><p>This blog post is a quick introduction to interactive data visualization with D3. The focus of this post is to walk to basic ideas/steps of visualizing data, as opposed to actual technical implementation.</p><p><a href="https://jchen6912-spotify-song-heatmap.netlify.com/"><strong>Live Demo</strong></a>(Chrome on PC for best experience).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cTvzr4YGyt8kpffw110kBw.png" /></figure><blockquote>Data</blockquote><p><a href="https://www.kaggle.com/edumucelli/spotifys-worldwide-daily-song-ranking/download">Available here</a>.</p><p>It is consisted of the daily top 200 most listened songs in 50+ countries from 2017 and 2018 by Spotify users. It contains more than 2 million rows, which includes more than 6,600 artists, and close to 20,000 songs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zxCqdW3gwt9PeTFzBFBMMA.png" /></figure><blockquote>Problem</blockquote><p>While there could be a number of different ways to frame the problem, here wewould like to know that what is the lifespan of popular songs. As a result, we will use first day’s data (top 200 songs listened on 2017–01–01) as the baseline and see changes on the percentage of songs remaining on the top 200 list over time. No surprise here that the percentage will drop as people are getting tired of listening to the same songs, and new songs will appear.</p><blockquote>Processing</blockquote><p>Since this is rather large amount of data, we will import the CSV file to SQLite3 for easier manipulation. Also for data visualization, usually we don’t want to bombard audience with the full data; and hence, instead of showing daily results, we will go for weekly (so roughly 53 data points for each region).</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/74c4404b70f81791b3e1b34c33e8b471/href">https://medium.com/media/74c4404b70f81791b3e1b34c33e8b471/href</a></iframe><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4VFjijvyXAYI_L-7yWOvNg.png" /><figcaption>Sample result from SQL query</figcaption></figure><blockquote>Visuals</blockquote><p>Highly recommend looking for visual inspirations from online sources, including Pinterest, McKinsey Insight, and Wall Street Journey.</p><p>This visualization is inspired from <a href="http://graphics.wsj.com/infectious-diseases-and-vaccines/">here</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*weDKbw_533OYC-P2vORVKg.png" /></figure><blockquote>Development</blockquote><p>The actual development is through D3.js, where the <a href="https://www.d3-graph-gallery.com/graph/heatmap_style.html">official gallery/guide</a> provides excellent demo and instructions on how to implement heat map visualization, along with interactive tool tips.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UwAKyYAjozfPqn8cjAZMOg.png" /></figure><blockquote>Deployment</blockquote><p>Highly recommend <a href="https://www.netlify.com/">Netlify</a>, a free hosting service platform. Once you have created in the index.html file, you can drag and drop the entire folder onto the platform, and it would take less than 3 seconds for your site to be up and running; alternatively, you can also connect Git to it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YDYH6vkCXs5q779L4rZ3rQ.png" /></figure><p>Questions?</p><p>jchen6912@gmail.com</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=74d612b28eb8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Time-series Forecasting Flow]]></title>
            <link>https://medium.com/data-science/time-series-forecasting-flow-2e49740664de?source=rss-c16867ccea73------2</link>
            <guid isPermaLink="false">https://medium.com/p/2e49740664de</guid>
            <category><![CDATA[analytics]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[forecasting]]></category>
            <category><![CDATA[data-visualization]]></category>
            <dc:creator><![CDATA[James Chen]]></dc:creator>
            <pubDate>Sat, 15 Dec 2018 13:35:56 GMT</pubDate>
            <atom:updated>2018-12-16T16:14:35.428Z</atom:updated>
            <content:encoded><![CDATA[<p>A brief introduction on critical steps in demand forecasting</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OWUCs5dvoMXQJXQLKUqQRQ.png" /></figure><blockquote><strong>Collecting</strong></blockquote><p>The key here is the format of data storage.</p><p>Intuitively we think of time-series data in the format below, also known as the <strong>wide</strong> format.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rxq3sLHPiHZeFVThMPzZAg.png" /></figure><p>However, the wide format is bad for SQL-based storage, as when we add new dates, we need to add another column to the table, and thus, best practice is storing the data in <strong>long</strong> format, as shown below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Z2MAATE8uigIvNyIW6JT4Q.png" /></figure><p>Another thing worth mentioning here is that since it is highly likely we are forecasting multiple products here, a convenient way is to use Python dictionary, where key is product ID, and value is the long data format (basically a dataframe). So we end up with a dictionary of dataframes.</p><blockquote><strong>Cleaning</strong></blockquote><p>Cleaning here is pretty standard. Basically we look for outliers. Common practice is that if we have weird values such as a fixed negative number for demand (which it usually is greater than or equal to 0), we drop them. Another thing we need to look for here is missing values. We could use pandas fillna function, in which we specify if we want to replace them with 0’s or with a method such as the last demand (backfill).</p><blockquote><strong>Testing</strong></blockquote><p>Also known as back-testing. Basically two most common ways of windowing, one is sliding (training and forecast time are fixed), and the other one is expanding (training data length increased by x at a time). <a href="https://eng.uber.com/forecasting-introduction/">Uber</a> does a great job visualizing the two methods as below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_fn80EUpyTVCL6Q9Dkp3NA.png" /></figure><p>Common evaluation metrics include MAPE, wMAPE, sMAPE, as well as RMSE and RMLSE (if the data distribution is skewed).</p><blockquote><strong>Modeling</strong></blockquote><p>The most important take-away here is that often simple methods such as mean of all data points outperform more sophisticated ARIMA. Also FB’s prophet package (in both R and Python) could be a go-to algorithm as well.</p><blockquote><strong>Evaluating</strong></blockquote><p>When evaluating performance, we could try different training and forecasting length to better understand our data, as well as the algorithms used. For example, Naive method performs extremely well when training data is 1 week and forecasting data is the next week; and ARIMA shines when we have longer data length, in terms of both training and forecasting. In other words, we need to determine our business use case first, whether it is short-term forecast or long-term forecast, so that we can better evaluate the results from different algorithms.</p><p>Questions?</p><p>jchen6912@gmail.com</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2e49740664de" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/time-series-forecasting-flow-2e49740664de">Time-series Forecasting Flow</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>