PPhablo Vilas Boas

//

← All entries
May 28, 2026β€’7 min read

Lab Reviewer API

This technical lab outlines the creation of a blog reviewing application using FastAPI and Langchain. The application transforms raw Markdown notes into structured blog posts, extracts and reviews code examples, translates content into Brazilian Portuguese, and generates PDFs of the output.

Lab Reviewer API

Overview

This technical lab outlines the creation of a lab reviewing application using FastAPI and Langchain. The application transforms raw Markdown notes into structured blog posts, extracts and reviews code examples, translates content into Brazilian Portuguese, and generates PDFs of the output.

Introduction

As a software engineer, I aim to enhance my skills and share my experiences through a lab website. Recognizing the potential of AI technologies such as FastAPI and Langchain, I developed an AI-assisted tool to refine my content and translate it into Portuguese. This application organizes raw notes into structured Markdown posts, generates translations, and provides detailed content revisions.

Diagram of the AI lab pipeline from raw notes to published labs

Project Setup

Dependencies

To set up the project, ensure the following dependencies are installed:

  • FastAPI: fastapi==0.110.0
  • Uvicorn: uvicorn[standard]==0.29.0
  • Environment Variables: python-dotenv==1.0.1
  • Data Validation: pydantic==2.6.4
  • Multipart Form Handling: python-multipart==0.0.9

LLM Stack

  • OpenAI: openai==1.14.3
  • Langchain: langchain==0.1.16
  • Langchain-OpenAI: langchain-openai==0.1.3
  • Langchain-Groq: langchain-groq

Additional Tools

  • Async HTTP Client: httpx==0.27.0
  • Markdown Processing: markdown==3.6
  • PDF Generation: weasyprint==62.3, pydyf==0.10.0

Environment Setup

  1. Create a virtual environment and activate it:

    python -m venv venv
    source venv/bin/activate
    
  2. Install the required packages:

    pip install -r requirements.txt
    
  3. Copy the example environment file and configure your environment variables:

    cp .env.example .env
    

    Edit .env with your keys and runtime values.

Core Functionality

The application consists of five main agents, each with specific responsibilities:

1. Lab Post Writer Agent

This agent organizes raw notes into coherent and structured lab posts, ensuring clarity and coherence.

class LabPostWriterAgent:
    """Agent responsible for turning sketch notes into structured blog posts."""

    def __init__(self, llm: BaseChatModel | None = None) -> None:
        """Initialize the chat model used by the agent."""
        self.logger = logging.getLogger(__name__)
        self.agent_name = AgentRole.POST_WRITER
        self.llm = llm or LLMConfig.build_chat_model_for_agent(AgentRole.POST_WRITER)
        self.blog_reviewer = LabReviewerAgent()
        self.code_example_agent = LabCodeExampleAgent()

    def organize_notes(self, request: LabPostWriterRequest) -> LabPostWriterResponse:
        """Transform raw notes into a reviewed markdown blog post."""
        self.logger.info("agent=%s | starting organize_notes pipeline", self.agent_name)
        enriched_context = enrich_context_with_repositories(request.context, self.logger)
        examples_response = self.code_example_agent.extract_examples(
            LabCodeExampleRequest(
                notes_context=request.context,
                max_examples=3,
            )
        )

        code_examples_context = self._build_code_examples_context(examples_response)
        final_context = enriched_context
        if code_examples_context:
            final_context = f"{enriched_context}\n\n{code_examples_context}"
        system_prompt = LabPostWriterPrompt.build_system_prompt()
        messages = [
            SystemMessage(content=system_prompt),
            HumanMessage(content=final_context),
        ]

        response = self.llm.invoke(messages)
        current_markdown = str(getattr(response, "content", "")).strip() or "Unable to generate blog content from the provided notes."

        # Run 3 full review/improvement cycles using the Blog Reviewer and Code Example Agents.
        for iteration in range(1, 4):
            self.logger.info("agent=%s | cycle %s/3 - requesting revision", self.agent_name, iteration)
            revised = self.blog_reviewer.revise(LabReviewerRequest(content=current_markdown))
            improvement_prompt = self._build_improvement_prompt(current_markdown, revised)
            improved_response = self.llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=improvement_prompt)])
            current_markdown = str(getattr(improved_response, "content", "")).strip() or revised.revised_post.strip()

        self.logger.info("agent=%s | pipeline finished (final_chars=%s)", self.agent_name, len(current_markdown))
        return LabPostWriterResponse(reviewed_markdown=current_markdown)

2. Lab Reviewer Agent

This agent reviews lab posts, suggesting improvements to enhance content quality and readability.

class LabReviewerAgent:
    """Agent responsible for revising blog posts in Markdown."""

    def __init__(self, llm: BaseChatModel | None = None) -> None:
        self.logger = logging.getLogger(__name__)
        self.agent_name = AgentRole.REVIEWER
        self.llm = llm or LLMConfig.build_chat_model_for_agent(AgentRole.REVIEWER)

    def revise(self, request: LabReviewerRequest) -> LabReviewerResponse:
        messages = [
            SystemMessage(content=LabReviewerPrompt.build_system_prompt()),
            HumanMessage(content=request.content),
        ]

        response = self.llm.invoke(messages)
        return LabReviewerResponse(
            revised_post=str(getattr(response, "revised_post", request.content)),
            errors_found=[],
            improvement_tips=[],
            next_revision_checklist=[]
        )

3. Lab Code Example Agent

This agent extracts and reviews code examples from GitHub through an API integration, providing examples to the Lab Post Writer Agent.

class LabCodeExampleAgent:
    """Extracts practical code examples from repositories referenced in notes."""

    def extract_examples(self, request: LabCodeExampleRequest) -> LabCodeExampleResponse:
        repositories = self.github_provider.extract_repositories(request.notes_context)
        if not repositories:
            return LabCodeExampleResponse(examples=[], summary="No GitHub repositories found in notes context.", warnings=["No repositories detected."])

        repo_context_sections = []
        for repository in repositories:
            try:
                repo_context_sections.append(self.github_provider.fetch_repo_context(repository))
            except Exception as e:
                self.logger.error(f"Error fetching context for {repository}: {e}")

        messages = [
            SystemMessage(content=LabCodeExamplePrompt.build_system_prompt()),
            HumanMessage(content=self._format_human_context(request, repositories, repo_context_sections)),
        ]

        response = self.llm.invoke(messages)
        return LabCodeExampleResponse(examples=response.get("examples", []), summary=response.get("summary", ""), warnings=response.get("warnings", []))

4. Lab Metadata Reviewer Agent

This agent generates a title and summary for the lab that can be used on the lab website.

class LabPostMetadataAgent:
    """Generate frontmatter metadata from reviewed markdown content."""

    def generate(self, request: LabPostMetadataRequest) -> LabPostMetadataResponse:
        messages = [
            SystemMessage(content=LabPostMetadataPrompt.build_system_prompt()),
            HumanMessage(content=request.content),
        ]

        response = self.llm.invoke(messages)
        return LabPostMetadataResponse(
            title=str(getattr(response, "title", "")),
            date=str(getattr(response, "date", "")),
            summary=str(getattr(response, "summary", "")),
            tags=getattr(response, "tags", []),
            published=getattr(response, "published", True)
        )

5. Lab Post Translator Agent

This agent translates the reviewed lab into Brazilian Portuguese.

class LabPostTranslatorAgent:
    """Agent responsible for translating reviewed posts to pt-BR."""

    def translate(self, request: LabPostTranslatorRequest) -> LabPostTranslatorResponse:
        messages = [
            SystemMessage(content=LabPostTranslatorPrompt.build_system_prompt()),
            HumanMessage(content=request.content),
        ]

        response = self.llm.invoke(messages)
        translated_markdown = str(getattr(response, "content", "")).strip() or request.content
        return LabPostTranslatorResponse(translated_markdown=translated_markdown)

Processing Flow

  1. Writing Content: Users input their raw Markdown notes.
  2. Get Code Examples: The Lab Code Example Agent extracts code examples to enhance the blog post.
  3. Revision Process: The Lab Post Writer Agent organizes and revises the notes through three iterative cycles using the Blog Reviewer Agent.
  4. Metadata Process: The Lab Metadata Reviewer Agent extracts metadata for the lab.
  5. Translation: The Lab Post Translator Agent converts the finalized content into Portuguese.
  6. Output: The application generates two Markdown files:
    • your-roots-are-not-controllers_reviewed_pt_br.md: Translated version
    • your-roots-are-not-controllers_reviewed.md: English version

Logging

The application utilizes Langsmith for detailed logging of agent interactions, which can be explored through a web interface. Additionally, a custom logging system captures interactions with the LLMs during processing to enhance debugging and monitoring.

Challenges

  1. Ensuring the reviewer agent provided constructive feedback was challenging. Performance varied with different models; initial attempts with Grok and Meta-Llama produced less coherent outputs, while OpenAI's GPT-4 significantly improved the quality of revisions.
  2. Allowing each agent to use a different type of LLM was necessary to optimize token usage and reduce costs.
  3. The Lab Code Example Agent faced difficulties in extracting code from GitHub, requiring multiple prompt iterations to achieve satisfactory results.

Testing and Debugging

For stability and reliability, unit tests for each agent's functionality should be written using frameworks like pytest. Logging aids in troubleshooting during development.

Future Improvements

Potential extensions for the application include:

  • Implementing a user-friendly interface for file uploads.
  • Expanding language support for broader accessibility.
  • Decoupling the current background task into a queue-based architecture for improved scalability.
  • Notifying users via email when processing is complete.
  • Adding an Image Create Agent to enhance visual appeal.

Conclusion

The lab reviewer successfully aids in learning and sharing content with improved quality and accessibility. By leveraging AI through FastAPI and Langchain, this application supports content creation and revision while facilitating multilingual support, expanding the reach of written works.

For the complete code and further details, visit the repository.

Repository Context

Project Structure

  • main.py: App entrypoint and middleware.
  • blog/router.py: API routes (/blog-post-writer/*).
  • blog/service.py: Orchestration for writer, translator, and reviewer agents.
  • blog/agents/: Feature-specific agents and schemas.
  • public/markdowns/: Generated output markdown files.
  • core/: Shared config and LLM setup.

Run Locally

Start the application with:

uvicorn main:app --reload --host 0.0.0.0 --port 3015

Check the health of the application:

curl http://127.0.0.1:3015/

API Endpoints

POST /blog-post-writer/review

Accepts a UTF-8 .md upload (file). Processing runs in the background.

Example:

curl -X POST http://127.0.0.1:3015/blog-post-writer/review \
  -F "file=@notes.md"

Returns the output path for the reviewed file. The service also writes a translated file with _pt_br suffix.

GET /outputs/markdown

Lists generated markdown files available under public/markdowns.

Example:

curl http://127.0.0.1:3015/outputs/markdown

Notes

  • Only .md files are accepted in organize-notes.
  • Output filenames are derived from the uploaded filename and written under public/markdowns/.
  • Generated posts always include YAML metadata frontmatter with required fields: title, date, summary, tags, and published.
  • Do not commit .env or real API keys.

ABOUT THE AUTHOR

Phablo Vilas Boas β€” Tech Lead & Senior Full Stack Engineer with 9+ years building platforms with Node.js, Python, React, and Flutter.

Continue reading