# Project export: Lentil

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: AI-powered compiler feedback assistant 🫘
- Devpost: https://devpost.com/software/lentil
- GitHub: https://github.com/plamorg/lentil
- Video: https://www.youtube.com/embed/31NFUm3NtMM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — smjleo (30 commits), Lucas Sta Maria (28 commits), Edward Wibowo (19 commits), Astoria-ni (2 commits)

## Devpost submission (written by the team)

### Overview

Terminal output messages often vary significantly in clarity and usefulness. Many developers know the frustration of compiling a program only to be overwhelmed by hundreds of lines of cryptic or irrelevant messages. Lentil is a tool that enhances command-line output by filtering unnecessary noise and emphasising the most relevant information. It goes a step further by suggesting potential fixes based on the context of your project files. Lentil empowers developers to navigate complex compiler outputs more efficiently, reducing time spent on debugging. Usage Just type lentil before your usual compilation command! Instead of sifting through a sea of cryptic messages, view it through Lentil. Lentil captures both stdout and stderr, then uses retrieval-augmented generation (RAG) techniques to contextualise the output. By analysing your project files recursively, it converts compiler messages into a standardised intermediate representation (IR), which is then presented in a clear and structured format on the webapp. Key Features Live updates: Continuously reflects changes as you code. Language agnostic: Works with any programming language and integrates seamlessly with various editors. Flexible integration: Can be used as a standalone command or integrated into existing tools via its IR. Architecture Overview Core application The core is built with OCaml and is responsible for: Capturing stdout, stderr, and the context of the current working directory Producing a standardised IR that powers the frontend Starting the web server Web server Lentil also includes a web server written in Python, serving three main functions: RAG facilitation: Using a Chroma vector database for contextual analysis LLM inference: Generating AI-driven explanations and recommendations Web interface: Offering a modern, browser-based view for enhanced user experience To start the web server: Get Started Explore the code and contribute to the project on GitHub.

## README (from the GitHub repository)

# Lentil 🫘

AI-powered compiler feedback assistant

## Getting started

### Prerequisites

* [OCaml and `opam`](https://ocaml.org/install)
* [Dune](https://dune.build/install)
* [uv](https://github.com/astral-sh/uv)

After cloning the repository, you need to install the webserver by adding a symlink:
```sh
mkdir ~/.lentil
ln -s /full/path/to/lentil/server ~/.lentil/server
```

### Usage

First launch the webserver:

```sh
cd cli
lentil -- --serve
```

...and run your compilation command through Lentil.

```sh
lentil -- gcc main.c
```


## Detected evidence (automated analysis)

Indexed codebase: 22 recognized source files, 22 KB.
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Ollama (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (55 of 55)

```
.gitignore
.ocamlformat
cli/.gitignore
cli/.ocamlformat
cli/bin/dune
cli/bin/lentil.ml
cli/dune-project
cli/flake.lock
cli/flake.nix
cli/lentil.opam
cli/lib/api.ml
cli/lib/api.mli
cli/lib/context.ml
cli/lib/context.mli
cli/lib/dune
cli/lib/file.ml
cli/lib/file.mli
cli/lib/process.ml
cli/lib/process.mli
cli/lib/server.ml
cli/lib/server.mli
cli/test/dune
cli/test/test_lentil.ml
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/README.md
frontend/src/Annotation.tsx
frontend/src/Annotations.tsx
frontend/src/App.tsx
frontend/src/ErrorMessage.tsx
frontend/src/Header.tsx
frontend/src/Loading.tsx
frontend/src/main.css
frontend/src/main.tsx
frontend/src/socket.ts
frontend/src/SuggestedFix.tsx
frontend/src/vite-env.d.ts
frontend/tsconfig.app.json
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/vite.config.ts
idea/idea.typ
README.md
server/.gitignore
server/.python-version
server/database.py
server/llm.py
server/main.py
server/pyproject.toml
server/README.md
server/test_app.py
server/test_database.py
server/uv.lock
```

### Dependencies

- frontend/package.json: @eslint/js@^9.19.0, @heroicons/react@^2.2.0, @tailwindcss/vite@^4.0.6, @types/react@^19.0.8, @types/react-dom@^19.0.3, @types/react-syntax-highlighter@^15.5.13, @vitejs/plugin-react@^4.3.4, autoprefixer@^10.4.20, eslint@^9.19.0, eslint-plugin-react-hooks@^5.0.0, eslint-plugin-react-refresh@^0.4.18, globals@^15.14.0, postcss@^8.5.2, react@^19.0.0, react-dom@^19.0.0, react-syntax-highlighter@^15.6.1, socket.io-client@^4.8.1, tailwindcss@^4.0.6, typescript@~5.7.2, typescript-eslint@^8.22.0, vite@^6.1.0
- server/pyproject.toml: chromadb@>=0.6.3, flask@>=3.1.0, flask-cors@>=5.0.0, flask-socketio@>=5.5.1, ollama@>=0.4.7, openai@>=1.63.0, pydantic@>=2.10.6, pytest@>=8.3.4

### Recent commits (newest first)

- feat(server): switch to o3-mini
- fix: shut down properly
- feat(server): introduce cmd
- feat: add command to context
- feat: add command to context
- feat(cli): use sys argv
- fix: add `Loading` and `LoadingBar` component
- fix: more prompt
- fix: update loading page
- fix: prompt
- style: minor margin changes
- feat(frontend): add loading indicator
- chore(nix): add python packages to flake
- docs: readme fix
- Merge branch 'main' of https://github.com/plamorg/lentil
- docs: readme fix
- chore(nix): add missing ocaml packages
- fix: unused variable warning
- feat: integrate frontend with server
- docs: update README

## Key source files (fetched from GitHub, selected and truncated for size)

### server/pyproject.toml

```
[project]
name = "server"
version = "0.1.0"
description = "Lentil server"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "chromadb>=0.6.3",
    "flask>=3.1.0",
    "flask-cors>=5.0.0",
    "flask-socketio>=5.5.1",
    "ollama>=0.4.7",
    "openai>=1.63.0",
    "pydantic>=2.10.6",
    "pytest>=8.3.4",
]

```

### frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@heroicons/react": "^2.2.0",
    "@tailwindcss/vite": "^4.0.6",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.5.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-syntax-highlighter": "^15.6.1",
    "socket.io-client": "^4.8.1",
    "tailwindcss": "^4.0.6"
  },
  "devDependencies": {
    "@eslint/js": "^9.19.0",
    "@types/react": "^19.0.8",
    "@types/react-dom": "^19.0.3",
    "@types/react-syntax-highlighter": "^15.5.13",
    "@vitejs/plugin-react": "^4.3.4",
    "eslint": "^9.19.0",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.18",
    "globals": "^15.14.0",
    "typescript": "~5.7.2",
    "typescript-eslint": "^8.22.0",
    "vite": "^6.1.0"
  }
}

```

### server/main.py

```python
from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS
from flask_socketio import SocketIO

import math

from database import VectorDatabase
from llm import Llm, Backend


app = Flask(__name__, static_folder="../frontend/dist", static_url_path="/")
CORS(app)
socketio = SocketIO(app, cors_allowed_origins="*")

vdb = VectorDatabase()
llm = Llm(backend=Backend.OPENAI)


def error_response(message: str, status_code: int = 400):
    return jsonify({"error": message}), status_code


@app.route("/")
def index():
    if app.static_folder:
        return send_from_directory(app.static_folder, "index.html")
    return error_response("Static folder not set")


@app.route("/hello")
def hello():
    return "Hello, World!"


@app.route("/generate", methods=["POST"])
def generate():
    """
    Take in

    {
        "stdout": "...",
        "stderr": "...",
        "cmd": "...",
        "files": [
            {"path": "...", "content": "..."},
            {"path": "...", "content": "..."},
            ...
        ]
    }

    and send back IR.
    """

    context = request.get_json()
    if not context:
        return error_response("No context provided")

    if "stdout" not in context or "stderr" not in context or "files" not in context:
        return error_response("Invalid context provided")

    stdout = context.get("stdout")
    stderr = context.get("stderr")
    cmd = context.get("cmd")
    files = [(f.get("path"), f.get("content")) for f in context.get("files", [])]

    socketio.emit("loading", {"message": "Generating response..."})

    vdb.clear()
    vdb.add_files(files)

    terminal_output = f"""
    The following is the terminal output:
    # Stdout:
    {stdout}
    # Stderr:
    {stderr}
    """

    # Get the top 20% most pertinent files based on terminal output
    num_files = len(files) if len(files) < 5 else math.ceil(0.2 * len(files))
    vdb_result = vdb.query(f"{terminal_output}", num_files)
    file_context = ""
    for path, content in zip(vdb_result["ids"], vdb_result["documents"]):
        file_context += f"""
        {path}:
        {content}
        """

    prompt = f"""
    The following terminal command was run: {cmd}
    Identify whether the following terminal output in response to the terminal command indicates an error:
    {terminal_output}

    If debugging needs to happen, indicate the most important lines of the terminal output that relate to the error and annotate it with a concise explanation. Each unique error should be a different annotation.
    In addition, provide a very quick one-sentence summary of the issue. Keep the quick description extremely concise - for example: "You have a missing source file.", or "You are using an unknown function.", or "The function 'myfunc' has wrong argument types.". In addition, provide a more detailed description.
    Also provide a diff of a suggested change, if possible. For the diff DO NOT use markdown formatting, just plain text. Each addition should start with the prefix "+ ", and each removal should start with the prefix "- ". You may add surrounding text if it helps with clarity (prefix with "  " to make sure indentation is consistent with the changes), but do not add additional diff formatting such as filenames.
    Do not use markdown formatting, just plain text.

    If no debugging needs to happen, no need to annotate anything and no description or diff is needed.

    The following is the file context:
    {file_context}
    """

    llm_result = llm.query(prompt)

    if not llm_result:
        return error_response("Response could not be created")

    socketio.emit("response", {"message": llm_result.model_dump_json()})
    return llm_result.model_dump_json()


if __name__ == "__main__":
    import sys

    port = int(sys.argv[1]) if len(sys.argv) > 1 else 5000
    socketio.run(app)

```

### frontend/src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './main.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### frontend/src/App.tsx

```typescript
import { useEffect, useState } from "react";
import ErrorMessage from "./ErrorMessage.tsx";
import SuggestedFix from "./SuggestedFix.tsx";
import { Annotations, Annotation } from "./Annotations.tsx";
import Loading from "./Loading.tsx";
import { socket } from "./socket.ts";

interface Response {
  summary: string;
  description: string;
  diff: string;
  annotations: Array<Annotation>;
}

export default function App() {
  const [isLoading, setIsLoading] = useState(true);
  const [response, setResponse] = useState<Response | null>(null);

  useEffect(() => {
    socket.on("loading", (_: any) => {
      setIsLoading(true);
    });
    socket.on("response", (data: any) => {
      data = JSON.parse(data["message"]);
      setResponse(data as Response);
      setIsLoading(false);
    });
  }, []);

  console.log(response);

  return (
    <>
      <div className="h-screen p-8 max-w-4xl m-auto mt-8 flex flex-col gap-4">
        {/* <Header /> */}
        {!isLoading && response ? (
          <div>
            <ErrorMessage errMsg={response.summary} />
            <SuggestedFix diff={response.diff} />
            <Annotations annotations={response.annotations} />
            <p className="font-serif mt-4 mb-8">{response.description}</p>
          </div>
        ) : (
          <Loading />
        )}
      </div>
    </>
  );
}

```

### frontend/vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
})

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Lentil</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Ovo&display=swap" rel="stylesheet">
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### frontend/eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
    },
  },
)

```

### server/test_app.py

```python
import pytest
from main import app


@pytest.fixture()
def setup_app():
    app.config.update(
        {
            "TESTING": True,
        }
    )

    yield app


@pytest.fixture()
def client(setup_app):
    return setup_app.test_client()


@pytest.fixture()
def runner(setup_app):
    return setup_app.test_cli_runner()


def test_hello(client):
    response = client.get("/hello")
    assert response.status_code == 200
    assert response.data == b"Hello, World!"


def test_generate_success(client):
    """
    Test that a valid POST request to /generate returns a 200 status and the expected JSON structure.
    """
    data = {
        "stdout": "Compilation successful",
        "stderr": "",
        "files": [
            {"path": "file1.py", "content": "print('Hello World')"},
            {"path": "file2.txt", "content": "Just some text."},
        ],
    }
    response = client.post("/generate", json=data)
    assert response.status_code == 200
    json_data = response.get_json()
    assert json_data is not None


def test_generate_failure_no_context(client):
    """
    Test that sending an empty JSON (no context) fails with a 400 status code.
    """
    response = client.post("/generate", json={})
    assert response.status_code == 400
    json_data = response.get_json()
    assert json_data is not None
    assert "error" in json_data
    assert json_data["error"] == "No context provided"

```

### server/llm.py

```python
from enum import Enum
from pydantic import BaseModel
from typing import List, Optional


class Annotation(BaseModel):
    command_output: List[str]
    comment: str


class Response(BaseModel):
    summary: str
    description: str
    diff: str
    annotations: List[Annotation]


class Backend(Enum):
    OLLAMA = "ollama"
    OPENAI = "openai"


class Llm:
    backend: Backend

    def __init__(self, backend: Backend):
        self.backend = backend

    def query(self, prompt: str) -> Optional[Response]:
        match self.backend:
            case Backend.OLLAMA:
                return self._ollama_query(prompt)
            case Backend.OPENAI:
                return self._openai_query(prompt)

    def _ollama_query(self, prompt: str) -> Optional[Response]:
        from ollama import chat

        response = chat(
            messages=[
                {
                    "role": "user",
                    "content": prompt,
                }
            ],
            model="llama3.2",
            format=Response.model_json_schema(),
        )

        if response.message.content:
            response = Response.model_validate_json(response.message.content)
            return response
        return None

    def _openai_query(self, prompt: str) -> Optional[Response]:
        from openai import OpenAI

        client = OpenAI()

        response = client.beta.chat.completions.parse(
            model="o3-mini-2025-01-31",
            messages=[{"role": "user", "content": prompt}],
            response_format=Response,
        )

        return response.choices[0].message.parsed

```

[11 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]