Build a RAG Code Translator API

Build a self-improving RAG API that translates Infobasic code to Java locally.

Introduction

30 Second Summary

Legacy codebases in niche languages are a real challenge for organizations trying to modernize. Translating thousands of lines by hand is slow, error-prone, and expensive.

In this project, you will build a self-hosted REST API that automatically translates Infobasic code to Java using a local LLM grounded in real documentation. The system improves over time by learning from your approved translations.

What You'll Build

A FastAPI service that accepts Infobasic code and returns the Java equivalent, powered by a RAG pipeline that retrieves relevant documentation from a ChromaDB vector store and feeds it as context to a locally-hosted Qwen2.5-Coder model via Ollama.

By the end of this project, you'll have:

  • A web scraper that automatically extracts jBASE documentation into a searchable knowledge base.
  • A RAG pipeline that retrieves relevant documentation and generates accurate code translations using a local LLM.
  • A feedback loop that stores approved translations back into the knowledge base, making the system smarter over time.
  • A REST API with translate, chat, feedback, and health endpoints that any IDE, bot, or platform can integrate with.
  • Secret Mission: Add streaming responses and conversation memory so the API remembers previous translations in a session.

Are there any prerequisites?

You should have basic Python knowledge (functions, classes, pip, virtual environments). You also need access to a Windows server with an NVIDIA GPU that has 24GB VRAM.

Familiarity with Infobasic/jBC syntax is helpful but not required. The project teaches you what you need to know as you go.

Set Up Ollama and Python Environment

Before you can build the RAG pipeline, you need the foundational tools in place on your GPU server. Ollama serves your LLM and embedding model locally, while Python packages like LangChain and ChromaDB wire the RAG pipeline together.

In this step, you will install Ollama on your Windows GPU server, pull the two AI models you need, and set up a Python project with all the dependencies and configuration ready to go.

In this step, get ready to:
  • Install Ollama and pull the code generation and embedding models.
  • Create a Python virtual environment and install all required packages.
  • Set up the project folder structure and configuration file.
Install Ollama and pull models

Ollama is a tool that runs large language models locally on your machine. It exposes an API at http://localhost:11434 that your Python code will call to generate code translations and create vector embeddings.

  • On your Windows GPU server, open PowerShell as Administrator.
  • Check if Ollama is already installed by running:
ollama --version

✔️ I see a version number

Ollama is already installed. You can skip ahead to pulling the models below.

ⓧ Command not found

Ollama is not installed yet. Install it by running this command in PowerShell:

irm https://ollama.com/install.ps1 | iex

What does this command do?

This downloads and runs the official Ollama installer script from ollama.com. It installs Ollama as a Windows service that starts automatically in the background.

Seeing a permissions error?

Make sure you opened PowerShell as Administrator (right-click the PowerShell icon and select Run as administrator). Ollama also requires Windows 10 22H2 or newer and NVIDIA 452.39+ drivers.

help me fix Ollama installation errors on Windows

Now pull the two models your project needs. The first is the code generation model. The second is the embedding model that converts text into vectors for search.

  • Pull the Qwen2.5-Coder model (14B parameters, optimized for code) by running:
ollama pull qwen2.5-coder:14b

Why this model?

Qwen2.5-Coder:14b is one of the strongest open-source code models at the 14B parameter tier. It supports 40+ programming languages and has a 32K token context window. The 14B size fits comfortably in 24GB of VRAM.

This download takes time

The 14B model is approximately 8-9 GB. Expect the download to take several minutes depending on your server's internet speed.

  • Once that completes, pull the embedding model by running:
ollama pull nomic-embed-text

What is an embedding model?

An embedding model converts text into numerical vectors (lists of numbers) that capture meaning. When two pieces of text are about similar topics, their vectors are close together. This is how your RAG pipeline will find relevant documentation for each translation request.

  • Verify both models are downloaded by running:
ollama list

You should see both qwen2.5-coder:14b and nomic-embed-text in the output table with their sizes listed.

  • Verify the Ollama API is responding by running:
Invoke-WebRequest -Uri http://localhost:11434

You should see a response containing "Ollama is running" in the output content.

Not getting a response from the API?

If the request fails, Ollama may not be running as a service. Try starting it manually with ollama serve in a separate PowerShell window, then retry the request.

help me get Ollama running on Windows

Create Python virtual environment and install packages

With Ollama ready, you need a Python project with all the libraries that connect to it. A virtual environment keeps this project's packages isolated from anything else on your server.

  • Create your project directory on the Desktop and move into it by running these commands:
cd ~\Desktop
mkdir infobasic-translator
cd infobasic-translator
  • Create a Python virtual environment by running:
python -m venv venv
  • Activate the virtual environment by running:
.\venv\Scripts\Activate

You should see (venv) appear at the beginning of your PowerShell prompt. This confirms the virtual environment is active.

Seeing an execution policy error?

Windows may block the activation script. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned first, then try the activation command again.

help me fix PowerShell execution policy errors

  • Install all required packages by running:
pip install "fastapi[standard]" langchain langchain-community langchain-ollama langchain-chroma chromadb beautifulsoup4 requests

What are these packages?

  • fastapi[standard] is the web framework for your API. It includes uvicorn (the server) and pydantic (data validation).
  • langchain, langchain-community, langchain-ollama, and langchain-chroma connect your LLM, embeddings, and vector store into a chain.
  • chromadb is the vector database that stores your document embeddings.
  • beautifulsoup4 parses HTML from the documentation pages you will scrape.
  • requests handles HTTP requests to fetch web pages.

The installation may take a minute or two. You should see a success message for each package at the end.

Installation failing?

Make sure your virtual environment is active (you see (venv) in your prompt). If a specific package fails, try installing it individually to see the full error.

help me fix pip install errors in my virtual environment

Create the project structure and config file

Your project needs a set of folders to organize scraped documentation, local files, verified translations, and the vector database. You will also create a central configuration file so all settings live in one place.

  • Create the project folders by running these commands:
mkdir scraped_docs
mkdir local_docs
mkdir verified_translations
mkdir chroma_db

What are these folders for?

  • scraped_docs/ stores text files extracted from the jBASE documentation website.
  • local_docs/ is where you place your own Infobasic code samples and Java reference docs.
  • verified_translations/ collects approved translation pairs that the system learns from over time.
  • chroma_db/ is where ChromaDB persists the vector database to disk.
  • Confirm the folders exist by running:
ls

You should see scraped_docs, local_docs, verified_translations, and chroma_db listed in the output.

  • Create the configuration file by running:
New-Item config.py
  • Open config.py in your text editor and paste the following code:
OLLAMA_BASE_URL = "http://localhost:11434"
LLM_MODEL = "qwen2.5-coder:14b"
EMBEDDING_MODEL = "nomic-embed-text"
CHROMA_PERSIST_DIR = "./chroma_db"
CHROMA_COLLECTION_NAME = "infobasic_knowledge"
SCRAPED_DOCS_DIR = "./scraped_docs"
LOCAL_DOCS_DIR = "./local_docs"
VERIFIED_TRANSLATIONS_DIR = "./verified_translations"
CHUNK_SIZE = 1000
CHUNK_OVERLAP = 150

What does this config do?

This file centralizes every setting your project needs. Instead of hardcoding values in multiple files, every script imports from config.py.

  • OLLAMA_BASE_URL tells your code where to find the Ollama API.
  • LLM_MODEL and EMBEDDING_MODEL specify which Ollama models to use.
  • CHUNK_SIZE and CHUNK_OVERLAP control how documents are split before embedding. Overlap ensures context is not lost at chunk boundaries.
  • Save config.py.

✔️ Awesome, I've got everything!

Great. Make sure you have saved config.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete config.py file:

OLLAMA_BASE_URL = "http://localhost:11434"
LLM_MODEL = "qwen2.5-coder:14b"
EMBEDDING_MODEL = "nomic-embed-text"
CHROMA_PERSIST_DIR = "./chroma_db"
CHROMA_COLLECTION_NAME = "infobasic_knowledge"
SCRAPED_DOCS_DIR = "./scraped_docs"
LOCAL_DOCS_DIR = "./local_docs"
VERIFIED_TRANSLATIONS_DIR = "./verified_translations"
CHUNK_SIZE = 1000
CHUNK_OVERLAP = 150

Your environment is locked and loaded. Next up, you will build a web scraper that extracts Infobasic documentation from the jBASE reference site and saves it as text files ready for ingestion.

Build the Web Scraper

Your environment is ready with Ollama serving models, Python packages installed, and a clean project structure. Now you need data to populate the knowledge base.

The RAG pipeline is only as good as the documentation it can reference. In this step, you will build a web scraper that automatically extracts jBASE/Infobasic documentation from a public reference site and saves it as structured text files.

In this step, get ready to:
  • Inspect the jBASE documentation site to understand its HTML structure.
  • Write a Python scraper that crawls all function pages and saves their content locally.
  • Run the scraper and verify that your knowledge base folder is populated with documentation files.
Understand the target page structure

Before writing any scraping code, you need to understand how the data you want is organized in the HTML. The jBASE documentation index page lists 200+ functions and statements, each linking to its own detail page.

  • Open your browser and navigate to the jBASE documentation index page:
https://static.zumasys.com/jbase/r99/knowledgebase/manuals/3.0/30manpages/man/JBC1.htm
  • Right-click the page and select View Page Source (or press Ctrl+U).
  • Look at the HTML structure. Each function or statement is a row in a table, with an <a> tag linking to a .htm page.

What are we looking for?

The index page contains a table of anchor tags. Each anchor's href points to a detail page (like READ.htm or WRITE.htm) that contains the syntax, parameters, and examples for that function.

Your scraper will fetch this index, extract all the links ending in .htm, then visit each one and save its text content.

Write the scraper

Since these are simple static HTML pages, you do not need a headless browser. The requests library fetches the HTML, and BeautifulSoup parses it.

  • Create a new file called scraper.py in your project folder. Start with the imports, base URL constants, and the function that fetches all links from the index page:
import os
import time
import requests
from bs4 import BeautifulSoup
from config import SCRAPED_DOCS_DIR

BASE_URL = "https://static.zumasys.com/jbase/r99/knowledgebase/manuals/3.0/30manpages/man/"
INDEX_URL = BASE_URL + "JBC1.htm"


def get_function_links():
    """Fetch the index page and extract links to individual function pages."""
    response = requests.get(INDEX_URL)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")

    links = []
    for anchor in soup.find_all("a", href=True):
        href = anchor["href"]
        if href.endswith(".htm") and href != "JBC1.htm":
            full_url = BASE_URL + href if not href.startswith("http") else href
            links.append({"name": anchor.get_text(strip=True), "url": full_url})

    return links

What does this code do?

  • BASE_URL and INDEX_URL point to the jBASE documentation root and the index page (JBC1.htm) respectively.
  • get_function_links() fetches the index page, finds all anchor tags with .htm links (excluding the index page itself), and returns a list of dictionaries with the function name and full URL.
  • SCRAPED_DOCS_DIR is imported from your config.py so folder paths are managed in one place.

You will not see output yet. The next function uses these links to scrape individual pages.

  • Below get_function_links(), add the function that scrapes a single documentation page:
def scrape_page(url):
    """Fetch a single documentation page and extract its text content."""
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        soup = BeautifulSoup(response.text, "html.parser")

        for element in soup(["script", "style", "nav", "footer"]):
            element.decompose()

        text = soup.get_text(separator="\n", strip=True)
        return text
    except requests.RequestException as e:
        print(f"  Error scraping {url}: {e}")
        return None

What does this code do?

  • scrape_page() fetches a single URL with a 10-second timeout to prevent hanging on unresponsive pages.
  • It removes non-content elements (scripts, styles, navigation, footers) with decompose() so only the actual documentation text remains.
  • get_text(separator="\n", strip=True) extracts clean, readable text with line breaks between sections.
  • If a request fails, it prints the error and returns None instead of crashing the entire scraper.

You will not see output yet. The next function orchestrates the full crawl.

  • Below scrape_page(), add the main orchestrator function and the entry point that ties everything together:
def scrape_all_docs():
    """Scrape all jBASE/Infobasic documentation pages."""
    os.makedirs(SCRAPED_DOCS_DIR, exist_ok=True)

    print("Fetching function index...")
    links = get_function_links()
    print(f"Found {len(links)} function/statement pages to scrape.")

    scraped_count = 0
    for i, link in enumerate(links):
        filename = link["name"].replace("/", "_").replace(" ", "_") + ".txt"
        filepath = os.path.join(SCRAPED_DOCS_DIR, filename)

        if os.path.exists(filepath):
            print(f"  [{i+1}/{len(links)}] Skipping {link['name']} (already scraped)")
            continue

        print(f"  [{i+1}/{len(links)}] Scraping {link['name']}...")
        content = scrape_page(link["url"])

        if content:
            with open(filepath, "w", encoding="utf-8") as f:
                f.write(f"# {link['name']}\n\n")
                f.write(f"Source: {link['url']}\n\n")
                f.write(content)
            scraped_count += 1

        time.sleep(0.5)

    print(f"\nDone! Scraped {scraped_count} new pages. Total docs in: {SCRAPED_DOCS_DIR}")


if __name__ == "__main__":
    scrape_all_docs()

What does this code do?

  • scrape_all_docs() creates the output directory, fetches all links from the index, then loops through each one.
  • The skip-if-exists check (os.path.exists(filepath)) makes the scraper resumable. If it crashes halfway, you can re-run without re-downloading pages you already have.
  • time.sleep(0.5) adds a half-second delay between requests to avoid overwhelming the server.
  • Each saved file includes a header with the function name and source URL for traceability in the RAG pipeline later.
  • Save scraper.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved scraper.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete scraper.py file. Compare yours against it:

import os
import time
import requests
from bs4 import BeautifulSoup
from config import SCRAPED_DOCS_DIR

BASE_URL = "https://static.zumasys.com/jbase/r99/knowledgebase/manuals/3.0/30manpages/man/"
INDEX_URL = BASE_URL + "JBC1.htm"


def get_function_links():
    """Fetch the index page and extract links to individual function pages."""
    response = requests.get(INDEX_URL)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")

    links = []
    for anchor in soup.find_all("a", href=True):
        href = anchor["href"]
        if href.endswith(".htm") and href != "JBC1.htm":
            full_url = BASE_URL + href if not href.startswith("http") else href
            links.append({"name": anchor.get_text(strip=True), "url": full_url})

    return links


def scrape_page(url):
    """Fetch a single documentation page and extract its text content."""
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        soup = BeautifulSoup(response.text, "html.parser")

        for element in soup(["script", "style", "nav", "footer"]):
            element.decompose()

        text = soup.get_text(separator="\n", strip=True)
        return text
    except requests.RequestException as e:
        print(f"  Error scraping {url}: {e}")
        return None


def scrape_all_docs():
    """Scrape all jBASE/Infobasic documentation pages."""
    os.makedirs(SCRAPED_DOCS_DIR, exist_ok=True)

    print("Fetching function index...")
    links = get_function_links()
    print(f"Found {len(links)} function/statement pages to scrape.")

    scraped_count = 0
    for i, link in enumerate(links):
        filename = link["name"].replace("/", "_").replace(" ", "_") + ".txt"
        filepath = os.path.join(SCRAPED_DOCS_DIR, filename)

        if os.path.exists(filepath):
            print(f"  [{i+1}/{len(links)}] Skipping {link['name']} (already scraped)")
            continue

        print(f"  [{i+1}/{len(links)}] Scraping {link['name']}...")
        content = scrape_page(link["url"])

        if content:
            with open(filepath, "w", encoding="utf-8") as f:
                f.write(f"# {link['name']}\n\n")
                f.write(f"Source: {link['url']}\n\n")
                f.write(content)
            scraped_count += 1

        time.sleep(0.5)

    print(f"\nDone! Scraped {scraped_count} new pages. Total docs in: {SCRAPED_DOCS_DIR}")


if __name__ == "__main__":
    scrape_all_docs()
Run the scraper and verify output

With the scraper written, it is time to run it against the live documentation site and confirm that files are being saved correctly.

  • Make sure your virtual environment is activated, then run the scraper by executing this command:
python scraper.py

What should I see?

You should see progress output like:

Fetching function index... followed by Found <number> function/statement pages to scrape. and then individual lines for each page being scraped. The scraper takes a few minutes because of the 0.5-second delay between requests.

  • Once the scraper finishes, verify that files were created by listing the contents of your scraped_docs folder:
dir scraped_docs

You should see many .txt files, one per jBASE function or statement (e.g. READ.txt, WRITE.txt, OPEN.txt).

  • Open one of the files to confirm the content is readable documentation. Check one by running:
Get-Content scraped_docs\READ.txt | Select-Object -First 20

You should see the file header with the function name and source URL, followed by the documentation text describing the READ statement.

Scraper failed or no files appeared?

  • If you see a connection error, check that your server has internet access. Try pinging the documentation host from PowerShell.
  • If you see ModuleNotFoundError, make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
  • If only some pages failed, that is fine. The scraper skips errors gracefully. Re-run it and it will pick up where it left off thanks to the skip-if-exists logic.

Why is my scraper failing to download pages?

Next, add your own local reference material to the knowledge base. This gives the RAG pipeline additional context beyond the scraped web docs.

  • Copy any Infobasic code samples you have (e.g. .txt files with Infobasic source code, design documents, or Java L3 reference docs) into the local_docs/ folder.
  • If you do not have local samples yet, create at least one example file to test the pipeline. Create a sample by running:
Set-Content -Path local_docs\sample_infobasic.txt -Value @"
# Sample Infobasic Code

SUBROUTINE CUSTOMER.READ(CUST.ID, CUST.REC, ERR.MSG)
    OPEN 'CUSTOMER' TO F.CUSTOMER ELSE
        ERR.MSG = 'Cannot open CUSTOMER file'
        RETURN
    END
    READ CUST.REC FROM F.CUSTOMER, CUST.ID ELSE
        ERR.MSG = 'Customer not found: ' : CUST.ID
        RETURN
    END
    ERR.MSG = ''
    RETURN
END
"@

Why add local docs?

The scraped documentation teaches the LLM about jBASE syntax and semantics. Your local code samples teach it about the specific patterns and conventions used in your codebase.

The more representative examples you provide, the better the translations will match your project's style.

  • Confirm both folders have content by running:
dir scraped_docs | Measure-Object
dir local_docs | Measure-Object

The first command should show dozens or hundreds of files. The second should show at least one file.

Your knowledge base now has raw documentation and code samples. Next up, you will ingest these documents into a vector store so the RAG pipeline can search them by meaning, not just keywords.

Ingest Documents into the Knowledge Base

Your scraper has done its job and your scraped_docs/ folder is full of jBASE documentation.

Raw text files are not searchable by an LLM. To make them useful, you need to transform them into vector embeddings and store them in a database that supports similarity search. In this step, you will build an ingestion pipeline that loads, chunks, embeds, and stores your documents in ChromaDB.

In this step, get ready to:
  • Load and split all your documentation into overlapping text chunks.
  • Embed those chunks using the nomic-embed-text model and store them in a ChromaDB vector collection.
  • Verify the knowledge base works by running a similarity search query.
Load and chunk documents

An LLM has a limited context window. You cannot feed 200+ full documentation pages into a single prompt. Instead, you break them into smaller overlapping pieces called chunks. The overlap ensures that no important context is lost at chunk boundaries.

  • Create a new file called ingest.py in your project folder.
  • Add the imports and the load_documents() function by pasting the following code:
import os
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
from config import (
    OLLAMA_BASE_URL,
    EMBEDDING_MODEL,
    CHROMA_PERSIST_DIR,
    CHROMA_COLLECTION_NAME,
    SCRAPED_DOCS_DIR,
    LOCAL_DOCS_DIR,
    VERIFIED_TRANSLATIONS_DIR,
    CHUNK_SIZE,
    CHUNK_OVERLAP,
)


def load_documents():
    """Load documents from scraped, local, and verified translation directories."""
    documents = []

    for doc_dir in [SCRAPED_DOCS_DIR, LOCAL_DOCS_DIR, VERIFIED_TRANSLATIONS_DIR]:
        if os.path.exists(doc_dir):
            print(f"Loading docs from {doc_dir}...")
            loader = DirectoryLoader(
                doc_dir,
                glob="**/*.txt",
                loader_cls=TextLoader,
                loader_kwargs={"encoding": "utf-8"},
            )
            documents.extend(loader.load())

    print(f"Loaded {len(documents)} documents total.")
    return documents

What does this code do?

The imports pull in everything you need for loading files, splitting text, generating embeddings, and storing vectors. All configuration values come from your config.py file so nothing is hard-coded here.

The load_documents() function iterates over three directories: scraped_docs/, local_docs/, and verified_translations/. It uses DirectoryLoader with TextLoader to read every .txt file from each directory into LangChain Document objects.

  • Now add the chunk_documents() function below load_documents() in the same file:

def chunk_documents(documents):
    """Split documents into overlapping chunks for embedding."""
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=CHUNK_SIZE,
        chunk_overlap=CHUNK_OVERLAP,
        separators=["\n\n", "\n", ". ", " ", ""],
    )
    chunks = splitter.split_documents(documents)
    print(f"Split into {len(chunks)} chunks.")
    return chunks

What does this code do?

RecursiveCharacterTextSplitter breaks each document into pieces of at most 1000 characters, with 150 characters of overlap between consecutive chunks. The overlap means that context near chunk boundaries appears in both adjacent chunks so nothing important falls through the cracks.

The separators list tells the splitter to try double newlines first (paragraph breaks), then single newlines, then sentences, then words. This keeps semantically related text together as much as possible.

  • Save ingest.py.

Seeing an import error?

If Python cannot find langchain_community or langchain_text_splitters, confirm your virtual environment is activated. You should see (venv) in your terminal prompt.

help me debug a missing LangChain module error

Embed and store in ChromaDB

Vector embeddings are numerical representations of text that capture semantic meaning. Once your chunks are embedded, you can find the most relevant documentation for any question by comparing embedding vectors.

  • Add the create_vector_store() function below chunk_documents() in ingest.py:

def create_vector_store(chunks):
    """Embed chunks and store in ChromaDB."""
    print("Creating embeddings and storing in ChromaDB...")
    print("(This may take a few minutes depending on document count)")

    embeddings = OllamaEmbeddings(
        model=EMBEDDING_MODEL,
        base_url=OLLAMA_BASE_URL,
    )

    vector_store = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=CHROMA_PERSIST_DIR,
        collection_name=CHROMA_COLLECTION_NAME,
    )

    print(f"Vector store created at: {CHROMA_PERSIST_DIR}")
    print(f"Total vectors stored: {vector_store._collection.count()}")
    return vector_store

What does this code do?

OllamaEmbeddings connects to your locally running Ollama server and uses the nomic-embed-text model to generate a 768-dimension vector for each chunk.

Chroma.from_documents() takes your chunks, embeds them in one batch, and stores the results in a persistent ChromaDB collection on disk at ./chroma_db/. The data survives restarts because it is persisted to that directory.

  • Add the ingest() orchestrator function and the if __name__ block at the bottom of ingest.py:

def ingest():
    """Run the full ingestion pipeline."""
    documents = load_documents()
    if not documents:
        print("No documents found. Run scraper.py first or add files to local_docs/")
        return

    chunks = chunk_documents(documents)
    create_vector_store(chunks)
    print("\nIngestion complete!")


if __name__ == "__main__":
    ingest()

What does this code do?

The ingest() function ties the full pipeline together: load, chunk, embed, and store. If no documents are found, it exits early with a helpful message.

The if __name__ == "__main__" guard lets you run the script directly from the terminal.

  • Save ingest.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved ingest.py with all four functions: load_documents(), chunk_documents(), create_vector_store(), and ingest().

ⓧ I'd like to double check the full code

import os
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
from config import (
    OLLAMA_BASE_URL,
    EMBEDDING_MODEL,
    CHROMA_PERSIST_DIR,
    CHROMA_COLLECTION_NAME,
    SCRAPED_DOCS_DIR,
    LOCAL_DOCS_DIR,
    VERIFIED_TRANSLATIONS_DIR,
    CHUNK_SIZE,
    CHUNK_OVERLAP,
)


def load_documents():
    """Load documents from scraped, local, and verified translation directories."""
    documents = []

    for doc_dir in [SCRAPED_DOCS_DIR, LOCAL_DOCS_DIR, VERIFIED_TRANSLATIONS_DIR]:
        if os.path.exists(doc_dir):
            print(f"Loading docs from {doc_dir}...")
            loader = DirectoryLoader(
                doc_dir,
                glob="**/*.txt",
                loader_cls=TextLoader,
                loader_kwargs={"encoding": "utf-8"},
            )
            documents.extend(loader.load())

    print(f"Loaded {len(documents)} documents total.")
    return documents


def chunk_documents(documents):
    """Split documents into overlapping chunks for embedding."""
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=CHUNK_SIZE,
        chunk_overlap=CHUNK_OVERLAP,
        separators=["\n\n", "\n", ". ", " ", ""],
    )
    chunks = splitter.split_documents(documents)
    print(f"Split into {len(chunks)} chunks.")
    return chunks


def create_vector_store(chunks):
    """Embed chunks and store in ChromaDB."""
    print("Creating embeddings and storing in ChromaDB...")
    print("(This may take a few minutes depending on document count)")

    embeddings = OllamaEmbeddings(
        model=EMBEDDING_MODEL,
        base_url=OLLAMA_BASE_URL,
    )

    vector_store = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=CHROMA_PERSIST_DIR,
        collection_name=CHROMA_COLLECTION_NAME,
    )

    print(f"Vector store created at: {CHROMA_PERSIST_DIR}")
    print(f"Total vectors stored: {vector_store._collection.count()}")
    return vector_store


def ingest():
    """Run the full ingestion pipeline."""
    documents = load_documents()
    if not documents:
        print("No documents found. Run scraper.py first or add files to local_docs/")
        return

    chunks = chunk_documents(documents)
    create_vector_store(chunks)
    print("\nIngestion complete!")


if __name__ == "__main__":
    ingest()
Run ingestion and verify

With the script complete, it is time to populate your knowledge base. The ingestion process will load every document, split it into chunks, generate embeddings through Ollama, and write the vectors to ChromaDB.

  • Run the ingestion script by executing this command:
python ingest.py

What should I see?

The script prints progress as it loads each directory, reports the total document count, splits into chunks, and then creates the vector store. This may take a few minutes if you have 200+ scraped pages, because each chunk needs to be sent to the embedding model.

At the end, you should see a line like Total vectors stored: <number> followed by Ingestion complete!. The exact number depends on how many docs you scraped and placed in local_docs/.

  • Confirm the chroma_db/ directory was created by running:
ls chroma_db

You should see files and subdirectories that ChromaDB uses for persistent storage.

  • Test a sample similarity search to confirm retrieval works by running this verification script:
python -c "
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
from config import OLLAMA_BASE_URL, EMBEDDING_MODEL, CHROMA_PERSIST_DIR, CHROMA_COLLECTION_NAME

embeddings = OllamaEmbeddings(model=EMBEDDING_MODEL, base_url=OLLAMA_BASE_URL)
vector_store = Chroma(
    persist_directory=CHROMA_PERSIST_DIR,
    collection_name=CHROMA_COLLECTION_NAME,
    embedding_function=embeddings,
)
print(f'Total vectors in store: {vector_store._collection.count()}')
results = vector_store.similarity_search('READ statement file operations', k=3)
for i, doc in enumerate(results):
    print(f'\n--- Result {i+1} ---')
    print(doc.page_content[:200])
"

What should I see?

The script prints the total vector count (should match what ingest.py reported) and then shows the top 3 most relevant chunks for the query "READ statement file operations". You should see snippets of jBASE documentation about READ operations.

This proves your vector store is working. The RAG pipeline will use this same retrieval mechanism to find context for translation requests.

Seeing zero vectors or empty results?

If the total vector count is 0, check that scraped_docs/ actually contains .txt files. Run ls scraped_docs to verify.

If you see a connection error mentioning port 11434, Ollama may not be running. Start it with ollama serve in a separate terminal window.

help me debug why my ChromaDB vector store is empty after running ingest.py

Your knowledge base is live. Every document you scraped and every file in local_docs/ is now searchable by semantic meaning. Next up, you will build the RAG pipeline that retrieves relevant chunks and feeds them to the LLM for accurate code translation.

Build the RAG Pipeline

Your knowledge base is loaded and searchable. Now it is time to connect it to the LLM so it can actually generate code translations.

In this step, you will build a RAG pipeline that retrieves relevant documentation from ChromaDB and feeds it as context to the Qwen2.5-Coder model. The model uses that context to produce accurate Infobasic-to-Java translations grounded in real documentation.

In this step, get ready to:
  • Design prompt templates that instruct the LLM on how to translate Infobasic code to Java.
  • Wire up a LangChain LCEL chain connecting the retriever, prompts, LLM, and output parser.
  • Test the translation chain with a sample Infobasic snippet and verify the Java output.
Create the translation prompt template

The prompt template is the instruction set that tells the LLM exactly how to translate Infobasic constructs into Java equivalents. It also defines a placeholder for retrieved documentation to be injected at runtime.

  • Create a new file called rag_chain.py in your project folder.
  • Add the imports and the translation prompt template by pasting this code:
from langchain_ollama import OllamaEmbeddings, ChatOllama
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from config import (
    OLLAMA_BASE_URL,
    LLM_MODEL,
    EMBEDDING_MODEL,
    CHROMA_PERSIST_DIR,
    CHROMA_COLLECTION_NAME,
)

# Prompt template for Infobasic-to-Java translation
TRANSLATE_PROMPT = ChatPromptTemplate.from_messages([
    ("system", """You are an expert code translator specializing in converting Infobasic (jBC/jBASE BASIC) code to Java.

Use the following reference documentation and verified translation examples to ensure accurate translation:

{context}

Translation rules:
- Convert dynamic arrays to Java ArrayList or HashMap as appropriate
- Convert SUBROUTINE to Java methods
- Convert PROGRAM to Java main class with public static void main
- Convert FM/VM/SM delimiters to proper Java data structures
- Convert file operations (READ/WRITE/OPEN) to appropriate Java I/O or JDBC
- Convert CRT/PRINT to System.out.println
- Convert IF/CASE/FOR/LOOP to Java equivalents
- Add proper Java type declarations for all variables
- Include necessary Java imports
- Preserve the original logic and flow

If the context includes verified translation examples for similar patterns, follow those conventions closely.
If you are unsure about a specific Infobasic construct, state your uncertainty clearly."""),
    ("human", """Convert the following Infobasic code to Java:

```infobasic
{code}
```

Provide the complete Java translation with comments explaining key conversion decisions."""),
])

What does this code do?

The imports bring in everything needed for the RAG pipeline: the Ollama LLM and embedding wrappers, the ChromaDB vector store, prompt templates, the chain-composition tool (RunnablePassthrough), and an output parser.

TRANSLATE_PROMPT is a ChatPromptTemplate with two messages. The system message gives the LLM detailed translation rules (dynamic arrays to ArrayList, SUBROUTINE to methods, FM/VM/SM to Java structures). The {context} placeholder is where retrieved documentation gets injected at runtime. The human message wraps the Infobasic code the user submits.

Now add the chat prompt template below the translation prompt. This one handles general Q&A questions about Infobasic or Java.

  • Add the following code below TRANSLATE_PROMPT in rag_chain.py:
# Prompt template for general Q&A about Infobasic/Java
CHAT_PROMPT = ChatPromptTemplate.from_messages([
    ("system", """You are an expert assistant for Infobasic (jBC/jBASE BASIC) and Java programming.

Use the following reference documentation to answer questions accurately:

{context}

If the documentation does not contain enough information to answer, say so clearly."""),
    ("human", "{question}"),
])

What does this code do?

CHAT_PROMPT follows the same pattern but is simpler. It instructs the LLM to answer questions using retrieved docs as context. The {question} placeholder receives the user's free-text question instead of code.

  • Save rag_chain.py.

Seeing a syntax error on the triple quotes?

Make sure each """ triple-quote pair is balanced. The system message opens with """You are... and closes with ...clearly.""". A missing or extra quote breaks the string.

help me fix a syntax error in my ChatPromptTemplate triple-quoted strings

Wire up the RAG chain

With the prompts defined, you need helper functions that load the vector store and format retrieved documents. Then you will compose these into full LangChain LCEL chains.

  • Add the following helper functions below CHAT_PROMPT in rag_chain.py:

def get_vector_store():
    """Load the existing ChromaDB vector store."""
    embeddings = OllamaEmbeddings(model=EMBEDDING_MODEL, base_url=OLLAMA_BASE_URL)
    vector_store = Chroma(
        persist_directory=CHROMA_PERSIST_DIR,
        collection_name=CHROMA_COLLECTION_NAME,
        embedding_function=embeddings,
    )
    return vector_store


def format_docs(docs):
    """Format retrieved documents into a context string."""
    return "\n\n---\n\n".join(
        f"[Source: {d.metadata.get('source', 'unknown')}]\n{d.page_content}"
        for d in docs
    )

What does this code do?

get_vector_store() connects to your existing ChromaDB collection using the same embedding model (nomic-embed-text) you used during ingestion. This ensures query embeddings match the stored document embeddings.

format_docs() takes the list of retrieved documents and joins them into a single string separated by --- dividers. Each document includes its source path so the LLM knows where the information came from.

Now add the two chain-builder functions that compose everything into working LCEL chains.

  • Add the following functions below format_docs in rag_chain.py:

def get_translate_chain():
    """Build the translation RAG chain."""
    vector_store = get_vector_store()
    # Retrieve top 6 relevant docs for translation context
    retriever = vector_store.as_retriever(search_kwargs={"k": 6})
    # Low temperature for deterministic code output
    llm = ChatOllama(model=LLM_MODEL, base_url=OLLAMA_BASE_URL, temperature=0.1)
    chain = (
        {"context": retriever | format_docs, "code": RunnablePassthrough()}
        | TRANSLATE_PROMPT
        | llm
        | StrOutputParser()
    )
    return chain


def get_chat_chain():
    """Build the general Q&A RAG chain."""
    vector_store = get_vector_store()
    # Retrieve top 5 relevant docs for Q&A context
    retriever = vector_store.as_retriever(search_kwargs={"k": 5})
    # Slightly higher temperature for conversational responses
    llm = ChatOllama(model=LLM_MODEL, base_url=OLLAMA_BASE_URL, temperature=0.3)
    chain = (
        {"context": retriever | format_docs, "question": RunnablePassthrough()}
        | CHAT_PROMPT
        | llm
        | StrOutputParser()
    )
    return chain

What does this code do?

Each chain builder follows the same LCEL (LangChain Expression Language) pattern. The chain is a pipeline that flows left to right:

  • The input (code or question) passes through RunnablePassthrough() unchanged, while the retriever simultaneously fetches relevant documents.
  • The prompt template receives both the formatted context and the user input.
  • ChatOllama sends the completed prompt to the local Qwen model via Ollama.
  • StrOutputParser() extracts the text from the LLM response object.

The translate chain uses temperature=0.1 for deterministic code output and retrieves 6 documents. The chat chain uses temperature=0.3 for slightly more varied conversational responses and retrieves 5 documents.

  • Save rag_chain.py.

Seeing an import error for langchain_core?

Make sure your virtual environment is activated. The langchain-core package is installed as a dependency of langchain-ollama and langchain-chroma. If you still see errors, run:

pip install langchain-core

help me fix import errors in my rag_chain.py file

✔️ Awesome, I've got everything!

Great. Make sure you have saved rag_chain.py with all the code above.

ⓧ I'd like to double check the full code

from langchain_ollama import OllamaEmbeddings, ChatOllama
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from config import (
    OLLAMA_BASE_URL,
    LLM_MODEL,
    EMBEDDING_MODEL,
    CHROMA_PERSIST_DIR,
    CHROMA_COLLECTION_NAME,
)

TRANSLATE_PROMPT = ChatPromptTemplate.from_messages([
    ("system", """You are an expert code translator specializing in converting Infobasic (jBC/jBASE BASIC) code to Java.

Use the following reference documentation and verified translation examples to ensure accurate translation:

{context}

Translation rules:
- Convert dynamic arrays to Java ArrayList or HashMap as appropriate
- Convert SUBROUTINE to Java methods
- Convert PROGRAM to Java main class with public static void main
- Convert FM/VM/SM delimiters to proper Java data structures
- Convert file operations (READ/WRITE/OPEN) to appropriate Java I/O or JDBC
- Convert CRT/PRINT to System.out.println
- Convert IF/CASE/FOR/LOOP to Java equivalents
- Add proper Java type declarations for all variables
- Include necessary Java imports
- Preserve the original logic and flow

If the context includes verified translation examples for similar patterns, follow those conventions closely.
If you are unsure about a specific Infobasic construct, state your uncertainty clearly."""),
    ("human", """Convert the following Infobasic code to Java:

```infobasic
{code}
```

Provide the complete Java translation with comments explaining key conversion decisions."""),
])

CHAT_PROMPT = ChatPromptTemplate.from_messages([
    ("system", """You are an expert assistant for Infobasic (jBC/jBASE BASIC) and Java programming.

Use the following reference documentation to answer questions accurately:

{context}

If the documentation does not contain enough information to answer, say so clearly."""),
    ("human", "{question}"),
])


def get_vector_store():
    embeddings = OllamaEmbeddings(model=EMBEDDING_MODEL, base_url=OLLAMA_BASE_URL)
    vector_store = Chroma(
        persist_directory=CHROMA_PERSIST_DIR,
        collection_name=CHROMA_COLLECTION_NAME,
        embedding_function=embeddings,
    )
    return vector_store


def format_docs(docs):
    return "\n\n---\n\n".join(
        f"[Source: {d.metadata.get('source', 'unknown')}]\n{d.page_content}"
        for d in docs
    )


def get_translate_chain():
    vector_store = get_vector_store()
    retriever = vector_store.as_retriever(search_kwargs={"k": 6})
    llm = ChatOllama(model=LLM_MODEL, base_url=OLLAMA_BASE_URL, temperature=0.1)
    chain = (
        {"context": retriever | format_docs, "code": RunnablePassthrough()}
        | TRANSLATE_PROMPT
        | llm
        | StrOutputParser()
    )
    return chain


def get_chat_chain():
    vector_store = get_vector_store()
    retriever = vector_store.as_retriever(search_kwargs={"k": 5})
    llm = ChatOllama(model=LLM_MODEL, base_url=OLLAMA_BASE_URL, temperature=0.3)
    chain = (
        {"context": retriever | format_docs, "question": RunnablePassthrough()}
        | CHAT_PROMPT
        | llm
        | StrOutputParser()
    )
    return chain
Test the chain directly

Before building the full API, you should verify that the translation chain actually works. You will invoke it with a simple Infobasic snippet and check that the Java output makes sense.

  • Test the translation chain by running this command in your terminal:
python -c "
from rag_chain import get_translate_chain
chain = get_translate_chain()
result = chain.invoke('SUBROUTINE GET.CUSTOMER(CUST.ID, CUST.REC)\nOPEN \"CUSTOMER\" TO CUST.FILE ELSE STOP\nREAD CUST.REC FROM CUST.FILE, CUST.ID ELSE CUST.REC = \"\"\nRETURN')
print(result)
"

What does this test do?

This sends a small Infobasic SUBROUTINE to the translation chain. The snippet opens a file, reads a record by ID, and returns it. The chain will retrieve relevant jBASE documentation from ChromaDB, inject it into the prompt, and ask the Qwen model to translate it to Java.

The first run may take 30-60 seconds as the model loads into GPU memory. Subsequent calls will be faster.

  • Check the terminal output. You should see a Java class or method with comments explaining the conversion decisions. The output will include Java imports, type declarations, and the equivalent logic using Java I/O or JDBC patterns.

Seeing a connection error to Ollama?

Make sure Ollama is running. Open a separate terminal and run:

ollama list

You should see qwen2.5-coder:14b and nomic-embed-text in the list. If Ollama is not running, start it with ollama serve in a separate terminal window.

help me fix a connection error between my RAG chain and Ollama

Your RAG pipeline is working. The model is retrieving real documentation and using it to produce grounded translations. Next up, you will wrap this pipeline in a FastAPI REST API so any tool or IDE can call it over HTTP.

Create the FastAPI REST API with Feedback Learning

Your RAG pipeline can translate Infobasic code and answer questions. The problem is that it only works as a Python script you run locally.

In this step, you will wrap the pipeline in a REST API using FastAPI. This turns your translator into a service that any tool, IDE plugin, or bot can call over HTTP.

In this step, get ready to:
  • Build core API endpoints for translation, chat, and health checks.
  • Create a feedback endpoint that stores approved translations back into the knowledge base.
  • Configure CORS, startup logic, and launch the server with auto-generated docs.
Build the core API endpoints

FastAPI lets you define HTTP endpoints as Python functions with type annotations. It automatically generates request validation and interactive documentation from your code.

  • Create a new file called main.py in your project folder.
  • Add the imports, app initialization, global chain variables, and Pydantic request/response models by pasting the following code:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from rag_chain import get_translate_chain, get_chat_chain

# Create the FastAPI application
app = FastAPI(
    title="Infobasic to Java Translator",
    description="RAG-powered code translation from Infobasic (jBC) to Java with self-improving knowledge base",
    version="1.0.0",
)

# Global references to the RAG chains (loaded at startup)
translate_chain = None
chat_chain = None


# Request models define what the API expects from clients
class TranslateRequest(BaseModel):
    code: str


class ChatRequest(BaseModel):
    message: str


# Response models define what the API sends back
class TranslateResponse(BaseModel):
    java_code: str
    model: str


class ChatResponse(BaseModel):
    response: str
    model: str

What does this code do?

The FastAPI() constructor creates your application with metadata that appears in the auto-generated docs.

The Pydantic models (TranslateRequest, ChatRequest, etc.) define the exact shape of incoming and outgoing JSON. FastAPI uses them to validate requests automatically and reject anything that does not match.

  • Below the Pydantic models in main.py, add the /translate endpoint:
@app.post("/translate", response_model=TranslateResponse)
async def translate_code(request: TranslateRequest):
    """Translate Infobasic code to Java using RAG."""
    if not request.code.strip():
        raise HTTPException(status_code=400, detail="Code cannot be empty")

    try:
        result = translate_chain.invoke(request.code)
        return TranslateResponse(java_code=result, model="qwen2.5-coder:14b")
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

What does this endpoint do?

The @app.post("/translate") decorator registers this function to handle POST requests at that path. It validates the incoming JSON against TranslateRequest, calls the RAG translation chain, and returns the Java output wrapped in TranslateResponse.

Empty input is caught early with a 400 error. Any chain failures return a 500 error with the exception message for debugging.

  • Below the /translate endpoint, add the /chat and /health endpoints:
@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
    """Ask questions about Infobasic or Java."""
    if not request.message.strip():
        raise HTTPException(status_code=400, detail="Message cannot be empty")

    try:
        result = chat_chain.invoke(request.message)
        return ChatResponse(response=result, model="qwen2.5-coder:14b")
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/health")
async def health_check():
    """Check if the service is running."""
    return {"status": "healthy", "model": "qwen2.5-coder:14b"}

What do these endpoints do?

The /chat endpoint works like /translate but uses the Q&A chain. Clients can ask general questions about Infobasic or Java.

The /health endpoint is a simple GET that returns the service status. Load balancers and monitoring tools use this to confirm the API is alive.

  • Save main.py.

You will not be able to run this file yet. The chains are None until you add startup logic in substep 3.

Build the feedback and learning endpoint

The feedback system is what makes this translator self-improving. When you approve a translation, it gets saved as a verified example and immediately embedded into ChromaDB. Future translations will retrieve these examples as context.

  • Create a new file called feedback.py in your project folder and paste the following code:
import os
import hashlib
from datetime import datetime
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
from langchain_core.documents import Document
from config import (
    OLLAMA_BASE_URL,
    EMBEDDING_MODEL,
    CHROMA_PERSIST_DIR,
    CHROMA_COLLECTION_NAME,
    VERIFIED_TRANSLATIONS_DIR,
)


def store_approved_translation(infobasic_code: str, java_code: str):
    """Store an approved translation as a new document in the knowledge base."""
    os.makedirs(VERIFIED_TRANSLATIONS_DIR, exist_ok=True)

    # Create a unique filename based on content hash
    content_hash = hashlib.md5(infobasic_code.encode()).hexdigest()[:8]
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"translation_{timestamp}_{content_hash}.txt"
    filepath = os.path.join(VERIFIED_TRANSLATIONS_DIR, filename)

    # Format the translation pair as a document
    doc_content = f"""# Verified Translation Example

## Infobasic (Original)
```infobasic
{infobasic_code}
```

## Java (Verified Translation)
```java
{java_code}
```

## Metadata
- Verified: {datetime.now().isoformat()}
- Type: approved_translation
"""

    # Save to disk for persistence
    with open(filepath, "w", encoding="utf-8") as f:
        f.write(doc_content)

    # Also embed and add directly to ChromaDB for immediate availability
    embeddings = OllamaEmbeddings(
        model=EMBEDDING_MODEL,
        base_url=OLLAMA_BASE_URL,
    )

    vector_store = Chroma(
        persist_directory=CHROMA_PERSIST_DIR,
        collection_name=CHROMA_COLLECTION_NAME,
        embedding_function=embeddings,
    )

    doc = Document(
        page_content=doc_content,
        metadata={
            "source": filepath,
            "type": "verified_translation",
            "timestamp": timestamp,
        },
    )

    vector_store.add_documents([doc])

    return filepath

What does this code do?

The store_approved_translation function does two things when a translation is approved.

  • It saves the Infobasic/Java pair as a formatted .txt file in verified_translations/. This means the next full ingestion run will pick it up automatically.
  • It immediately embeds the document into ChromaDB so the RAG chain can retrieve it right away without waiting for a re-ingestion.

The filename uses an MD5 hash of the input code to avoid duplicates and a timestamp for ordering.

  • Save feedback.py.

Now you need to wire this function into your API. You will add the feedback models and endpoint to main.py.

  • In main.py, find this import line at the top:
from rag_chain import get_translate_chain, get_chat_chain
  • Add the feedback import directly below it, so the two lines look like this:
from rag_chain import get_translate_chain, get_chat_chain
from feedback import store_approved_translation
  • Next, find the ChatResponse model. Add the FeedbackRequest and FeedbackResponse models below it:
class FeedbackRequest(BaseModel):
    infobasic_code: str
    java_code: str
    rating: str  # "approved" or "rejected"


class FeedbackResponse(BaseModel):
    status: str
    message: str
  • Below the /health endpoint, add the /feedback endpoint:
@app.post("/feedback", response_model=FeedbackResponse)
async def submit_feedback(request: FeedbackRequest):
    """Submit feedback on a translation. Approved translations are learned."""
    if request.rating not in ("approved", "rejected"):
        raise HTTPException(
            status_code=400,
            detail="Rating must be 'approved' or 'rejected'"
        )

    if request.rating == "approved":
        filepath = store_approved_translation(
            request.infobasic_code, request.java_code
        )
        return FeedbackResponse(
            status="learned",
            message=f"Translation stored and embedded. The system will use this as a reference for future translations. Saved to: {filepath}",
        )

    return FeedbackResponse(
        status="noted",
        message="Feedback recorded. Rejected translations are not added to the knowledge base.",
    )

How does the feedback loop work?

When a client sends a rating of "approved", the endpoint calls store_approved_translation which saves the pair to disk AND embeds it into ChromaDB immediately.

The next time the RAG chain handles a similar translation, the retriever will find this verified example and include it as context. Over time, the system builds a library of proven translations and produces better output.

  • Save main.py.

Seeing an import error?

Make sure feedback.py is in the same folder as main.py. Python resolves from feedback import ... by looking in the current directory.

help me debug an import error with feedback.py in my FastAPI project

Add CORS, startup logic, and start the server

CORS middleware allows requests from any origin, which means browser-based tools and external clients can reach your API. The startup event pre-loads both RAG chains so the first request does not have to wait for initialization.

  • In main.py, find this import line at the top:
from fastapi import FastAPI, HTTPException
  • Replace it with these two lines to add the CORS import and uvicorn:
import uvicorn
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
  • Next, find the line that says translate_chain = None. Add the CORS middleware block between the app creation and the global variables. The section should look like this:
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

translate_chain = None
chat_chain = None


@app.on_event("startup")
async def startup():
    global translate_chain, chat_chain
    print("Loading RAG chains...")
    translate_chain = get_translate_chain()
    chat_chain = get_chat_chain()
    print("RAG chains ready!")

What does this code do?

The CORSMiddleware with allow_origins=["*"] lets any domain make requests to your API. This is necessary for browser-based clients or tools running on different ports.

The @app.on_event("startup") function runs once when the server starts. It loads both RAG chains into memory so they are ready to handle the first request immediately.

  • Finally, add the server entry point at the very bottom of main.py:
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

Why 0.0.0.0?

Binding to 0.0.0.0 makes the server accessible from any network interface, not just localhost. This is important if you are connecting to the server from your local machine over the network.

  • Save main.py.
  • Start the FastAPI server by running this command:
python main.py
  • You should see output that includes Loading RAG chains... followed by RAG chains ready! and a message that uvicorn is running on http://0.0.0.0:8000.
  • Open your browser and navigate to http://localhost:8000/docs to see the auto-generated Swagger UI with all four endpoints listed.

What is the /docs page?

FastAPI automatically generates interactive API documentation at /docs. You can test every endpoint directly from the browser by clicking **Try it out**, filling in the request body, and hitting **Execute**.

Server won't start?

Make sure your virtual environment is activated. Check that Ollama is running (the startup event connects to it to load the RAG chains).

If you see a port conflict, another process may be using port 8000. Stop it or change the port number in the uvicorn.run call.

help me debug why my FastAPI server won't start

✔️ Awesome, I've got everything!

Great. Double check you have saved both feedback.py and main.py.

ⓧ I'd like to double check the full code

Here is the complete feedback.py:

import os
import hashlib
from datetime import datetime
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
from langchain_core.documents import Document
from config import (
    OLLAMA_BASE_URL,
    EMBEDDING_MODEL,
    CHROMA_PERSIST_DIR,
    CHROMA_COLLECTION_NAME,
    VERIFIED_TRANSLATIONS_DIR,
)


def store_approved_translation(infobasic_code: str, java_code: str):
    """Store an approved translation as a new document in the knowledge base."""
    os.makedirs(VERIFIED_TRANSLATIONS_DIR, exist_ok=True)

    # Create a unique filename based on content hash
    content_hash = hashlib.md5(infobasic_code.encode()).hexdigest()[:8]
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"translation_{timestamp}_{content_hash}.txt"
    filepath = os.path.join(VERIFIED_TRANSLATIONS_DIR, filename)

    # Format the translation pair as a document
    doc_content = f"""# Verified Translation Example

## Infobasic (Original)
```infobasic
{infobasic_code}
```

## Java (Verified Translation)
```java
{java_code}
```

## Metadata
- Verified: {datetime.now().isoformat()}
- Type: approved_translation
"""

    # Save to disk for persistence
    with open(filepath, "w", encoding="utf-8") as f:
        f.write(doc_content)

    # Also embed and add directly to ChromaDB for immediate availability
    embeddings = OllamaEmbeddings(
        model=EMBEDDING_MODEL,
        base_url=OLLAMA_BASE_URL,
    )

    vector_store = Chroma(
        persist_directory=CHROMA_PERSIST_DIR,
        collection_name=CHROMA_COLLECTION_NAME,
        embedding_function=embeddings,
    )

    doc = Document(
        page_content=doc_content,
        metadata={
            "source": filepath,
            "type": "verified_translation",
            "timestamp": timestamp,
        },
    )

    vector_store.add_documents([doc])

    return filepath

Here is the complete main.py:

import uvicorn
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from rag_chain import get_translate_chain, get_chat_chain
from feedback import store_approved_translation

app = FastAPI(
    title="Infobasic to Java Translator",
    description="RAG-powered code translation from Infobasic (jBC) to Java with self-improving knowledge base",
    version="1.0.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

translate_chain = None
chat_chain = None


@app.on_event("startup")
async def startup():
    global translate_chain, chat_chain
    print("Loading RAG chains...")
    translate_chain = get_translate_chain()
    chat_chain = get_chat_chain()
    print("RAG chains ready!")


class TranslateRequest(BaseModel):
    code: str


class ChatRequest(BaseModel):
    message: str


class FeedbackRequest(BaseModel):
    infobasic_code: str
    java_code: str
    rating: str  # "approved" or "rejected"


class TranslateResponse(BaseModel):
    java_code: str
    model: str


class ChatResponse(BaseModel):
    response: str
    model: str


class FeedbackResponse(BaseModel):
    status: str
    message: str


@app.post("/translate", response_model=TranslateResponse)
async def translate_code(request: TranslateRequest):
    """Translate Infobasic code to Java using RAG."""
    if not request.code.strip():
        raise HTTPException(status_code=400, detail="Code cannot be empty")
    try:
        result = translate_chain.invoke(request.code)
        return TranslateResponse(java_code=result, model="qwen2.5-coder:14b")
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
    """Ask questions about Infobasic or Java."""
    if not request.message.strip():
        raise HTTPException(status_code=400, detail="Message cannot be empty")
    try:
        result = chat_chain.invoke(request.message)
        return ChatResponse(response=result, model="qwen2.5-coder:14b")
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/feedback", response_model=FeedbackResponse)
async def submit_feedback(request: FeedbackRequest):
    """Submit feedback on a translation. Approved translations are learned."""
    if request.rating not in ("approved", "rejected"):
        raise HTTPException(
            status_code=400,
            detail="Rating must be 'approved' or 'rejected'"
        )
    if request.rating == "approved":
        filepath = store_approved_translation(
            request.infobasic_code, request.java_code
        )
        return FeedbackResponse(
            status="learned",
            message=f"Translation stored and embedded. The system will use this as a reference for future translations. Saved to: {filepath}",
        )
    return FeedbackResponse(
        status="noted",
        message="Feedback recorded. Rejected translations are not added to the knowledge base.",
    )


@app.get("/health")
async def health_check():
    """Check if the service is running."""
    return {"status": "healthy", "model": "qwen2.5-coder:14b"}


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

Your API is live and serving requests. Next up, you will test it end-to-end with real Infobasic code and train the knowledge base with approved translations.

Test End-to-End and Train the Knowledge Base

Your API is running and ready to accept requests. Now it's time to put it through its paces from the outside, exactly the way a real client (an IDE plugin, a Slack bot, or a CI pipeline) would call it.

In this step, you will send requests to each endpoint using PowerShell, approve a translation to teach the system, and then verify that the knowledge base actually improves.

In this step, get ready to:
  • Test all API endpoints from PowerShell to confirm they respond correctly.
  • Approve a translation to store it as a verified example in the knowledge base.
  • Verify the self-improving loop by observing better results after training.
Test with PowerShell

Any HTTP client can consume your API. PowerShell's Invoke-WebRequest cmdlet simulates exactly how an external tool would integrate with your translation service.

  • Open a new PowerShell window (keep the one running the server open separately).
  • Confirm the API is reachable by testing the health endpoint:
Invoke-WebRequest -Uri "http://localhost:8000/health" -Method GET | Select-Object -ExpandProperty Content

What does this command do?

Invoke-WebRequest sends an HTTP request to your FastAPI server. Piping to Select-Object -ExpandProperty Content extracts just the JSON response body so you can read it clearly.

You should see a JSON response: {"status":"healthy","model":"qwen2.5-coder:14b"}.

  • Test the /translate endpoint by sending a simple Infobasic subroutine:
$body = '{"code": "SUBROUTINE HELLO(MSG)\n  CRT MSG\nRETURN"}'
$response = Invoke-WebRequest -Uri "http://localhost:8000/translate" -Method POST -Body $body -ContentType "application/json"
$response.Content

What does this command do?

The $body variable holds a JSON string containing your Infobasic code. The \n sequences represent newlines in the code so the API can parse multi-line snippets.

The -ContentType "application/json" flag tells the server to expect JSON in the request body.

Expect a wait on first request

The LLM generates tokens sequentially, so the first response may take 15-60 seconds depending on your GPU. This is normal for a 14B parameter model running locally.

You should see a JSON response containing a java_code field with a Java method translation of the subroutine.

  • Test the /chat endpoint by asking a question about Infobasic:
$body = '{"message": "What is the difference between READ and MATREAD in Infobasic?"}'
$response = Invoke-WebRequest -Uri "http://localhost:8000/chat" -Method POST -Body $body -ContentType "application/json"
$response.Content

You should see a JSON response with a response field containing an explanation grounded in the documentation you scraped earlier.

Getting a 500 error?

Check the PowerShell window running your server for the full traceback. Common causes:

  • Ollama is not running. Open a separate terminal and run ollama list to confirm it responds.
  • ChromaDB collection is empty. Re-run python ingest.py to repopulate the vector store.
  • Help me debug this error
Translate and approve real samples

Testing with toy snippets proves the plumbing works. Now you will feed a real Infobasic pattern through the translator and teach the system by approving the output.

  • Send a more realistic Infobasic snippet through /translate by running this command:
$body = '{"code": "SUBROUTINE READ.CUSTOMER(ID, RECORD, ERR)\n  OPEN \"CUSTOMER\" TO F.CUST ELSE\n    ERR = \"CANNOT OPEN FILE\"\n    RETURN\n  END\n  READ RECORD FROM F.CUST, ID ELSE\n    ERR = \"RECORD NOT FOUND\"\n    RETURN\n  END\n  ERR = \"\"\nRETURN"}'
$response = Invoke-WebRequest -Uri "http://localhost:8000/translate" -Method POST -Body $body -ContentType "application/json"
$response.Content

Why this snippet?

This snippet covers multiple Infobasic-to-Java translation challenges at once: file OPEN, READ with an ELSE clause, SUBROUTINE parameter passing, and error handling. The quality of this translation tells you how well the RAG pipeline is performing.

  • Review the Java output in the response. Look for correct use of Java I/O patterns, proper exception handling, and accurate parameter mapping.
  • If the translation looks reasonable, approve it by sending it to the /feedback endpoint. Copy the java_code value from the response above and paste it into this command:
$feedback = @{
    infobasic_code = "SUBROUTINE READ.CUSTOMER(ID, RECORD, ERR)\n  OPEN \"CUSTOMER\" TO F.CUST ELSE\n    ERR = \"CANNOT OPEN FILE\"\n    RETURN\n  END\n  READ RECORD FROM F.CUST, ID ELSE\n    ERR = \"RECORD NOT FOUND\"\n    RETURN\n  END\n  ERR = \"\"\nRETURN"
    java_code = "PASTE YOUR JAVA OUTPUT HERE"
    rating = "approved"
} | ConvertTo-Json
$response = Invoke-WebRequest -Uri "http://localhost:8000/feedback" -Method POST -Body $feedback -ContentType "application/json"
$response.Content

What does this command do?

This sends the original Infobasic code and its Java translation to the /feedback endpoint with a rating of approved. The server saves the translation pair as a .txt file in verified_translations/ and immediately embeds it into ChromaDB.

Future queries that match this pattern will retrieve your verified example as context, guiding the LLM toward the conventions you approved.

You should see a JSON response with "status": "learned" confirming the translation was stored and embedded.

  • Confirm the file was created by listing the verified_translations/ folder:
Get-ChildItem .\verified_translations\

You should see a file named something like translation_20240101_120000_abc12345.txt (the timestamp and hash will differ on your machine).

Feedback returned but no file appeared?

Check that your server process has write permissions to the project directory. Also confirm you're running Get-ChildItem from the same folder where your project lives (the one containing config.py).

Help me troubleshoot missing feedback files

Verify the learning loop

The real proof of a self-improving system is observable improvement. You will now translate a similar Infobasic pattern and check whether the RAG retrieves your verified example as context.

  • Send a variation of the same pattern (a different SUBROUTINE that also uses OPEN and READ) through /translate:
$body = '{"code": "SUBROUTINE READ.ACCOUNT(ACCT.ID, ACCT.REC, ERROR.MSG)\n  OPEN \"ACCOUNTS\" TO F.ACCT ELSE\n    ERROR.MSG = \"FILE OPEN FAILED\"\n    RETURN\n  END\n  READ ACCT.REC FROM F.ACCT, ACCT.ID ELSE\n    ERROR.MSG = \"NOT FOUND\"\n    RETURN\n  END\n  ERROR.MSG = \"\"\nRETURN"}'
$response = Invoke-WebRequest -Uri "http://localhost:8000/translate" -Method POST -Body $body -ContentType "application/json"
$response.Content

What should I look for?

Compare this translation to the first one you ran before approving any feedback. The new output should follow the same Java conventions as your approved example (similar class structure, exception handling style, and variable naming patterns).

This happens because the RAG retriever now pulls your verified translation as context alongside the scraped documentation. The LLM sees a concrete example of how you want similar patterns translated and follows those conventions.

  • Optionally, check the server logs in your other PowerShell window. You will see the retrieval step pulling documents from verified_translations/ alongside the scraped docs.

Why does this matter?

Most translation tools are static. They never learn from corrections. Your system stores every approved translation directly into the same vector store the RAG pipeline queries. Over time, as you approve more translations, the knowledge base accumulates proven patterns that guide future output.

This is the self-improving loop: translate, review, approve, and the next translation of a similar pattern gets better automatically.

Output looks the same as before?

The improvement is most visible when the approved example closely matches the new query. If your test snippet is too different from what you approved, the retriever may not rank your verified translation highly enough to include it.

  • Try approving 2-3 more translation pairs covering different Infobasic patterns. More verified examples give the retriever more to work with.
  • Help me verify my learning loop is working

You now have a fully operational, self-improving code translation service. Every time you approve a translation, the system gets a little smarter for next time.

Up next: the Secret Mission challenges you to add real-time streaming and conversation memory, turning this into a production-grade assistant.

Secret mission

Add Streaming Responses and Conversation Memory

Your API works, but clients have to wait for the entire response before seeing any output. In production, streaming tokens as they generate feels instant. And without conversation memory, every request is a blank slate. Can you upgrade both?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses a rented GPU server that costs approximately $0.50-1.50/hr while running, so remember to shut it down when you are not using it.

Cost warning

Your GPU server rental is billed by the hour. If you leave it running overnight or over a weekend, the costs add up quickly. Shut down or terminate the server when you are not actively working on this project.

Resources you used:

  • GPU server rental (Windows, NVIDIA 24GB VRAM).
  • Ollama installation with pulled models (qwen2.5-coder:14b, nomic-embed-text).
  • Python virtual environment and installed packages.
  • Project directory with all Python files, scraped docs, and ChromaDB data.

Keep everything running

No action needed. Choose this if you are still actively building and testing translations.

  • Your GPU server continues to run and bill by the hour.
  • Ollama stays installed with both models ready.
  • Your ChromaDB knowledge base retains all vectors and verified translations.
  • The FastAPI server is ready to restart at any time.

Pause - I'll come back to this later

Shut down your GPU server to stop billing. Your project files will persist on disk if your provider supports stopped instances.

  • Stop the FastAPI server by pressing Ctrl+C in the terminal where it is running.
  • Stop Ollama by closing the Ollama application from the system tray.
  • Shut down (do not terminate) your GPU server instance through your cloud provider's dashboard.
  • When you return, start the server instance, launch Ollama, activate your virtual environment, and run the FastAPI server again.

Delete - I don't want to use this again

Remove all project resources completely. This frees up all storage and stops all billing.

  • Stop the FastAPI server by pressing Ctrl+C in the terminal.
  • Deactivate your Python virtual environment by running:
deactivate
  • Delete the entire project directory (including the virtual environment, scraped docs, ChromaDB data, and all Python files) by running:
cd ~\Desktop
Remove-Item -Recurse -Force .\infobasic-translator
  • Uninstall Ollama and remove its models. Open Windows Settings, then select Apps, then Installed apps. Find Ollama in the list and click Uninstall. The uninstaller will ask if you want to remove your downloaded models. Select yes to free up disk space.
  • If any model files remain, delete the Ollama data folder by running:
Remove-Item -Recurse -Force $env:HOMEPATH\.ollama
  • Terminate (not just stop) your GPU server instance through your cloud provider's dashboard. This permanently destroys the instance and stops all billing.

That's a Wrap!

That's a Wrap!

Nice work. You just built a fully local, self-improving AI-powered code translation service from scratch.

You've learned how to:

  • Build a RAG pipeline that scrapes documentation, chunks and embeds it into ChromaDB, and grounds LLM responses in real reference material.
  • Deploy a FastAPI REST API with /translate, /chat, /feedback, and /health endpoints that any tool, IDE, or platform can integrate with.
  • Implement a feedback loop that stores approved translations back into the knowledge base, making the system smarter over time with zero external API costs.
  • Added real-time streaming and conversation memory so the API remembers previous translations in a session.

Ready to quiz yourself?