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.

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
-
Create a virtual environment and activate it:
python -m venv venv source venv/bin/activate -
Install the required packages:
pip install -r requirements.txt -
Copy the example environment file and configure your environment variables:
cp .env.example .envEdit
.envwith 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
- Writing Content: Users input their raw Markdown notes.
- Get Code Examples: The Lab Code Example Agent extracts code examples to enhance the blog post.
- Revision Process: The Lab Post Writer Agent organizes and revises the notes through three iterative cycles using the Blog Reviewer Agent.
- Metadata Process: The Lab Metadata Reviewer Agent extracts metadata for the lab.
- Translation: The Lab Post Translator Agent converts the finalized content into Portuguese.
- Output: The application generates two Markdown files:
your-roots-are-not-controllers_reviewed_pt_br.md: Translated versionyour-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
- 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.
- Allowing each agent to use a different type of LLM was necessary to optimize token usage and reduce costs.
- 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
.mdfiles are accepted inorganize-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, andpublished. - Do not commit
.envor real API keys.