feat: add PDF research agent example using bindufy#328
feat: add PDF research agent example using bindufy#328raahulrahl merged 5 commits intoGetBindu:mainfrom
Conversation
|
I built this example while learning the Bindu workflow If example agents are not the right direction, I'm happy to |
c33d2c4 to
a149819
Compare
Paraschamoli
left a comment
There was a problem hiding this comment.
@ne-ey use openrouter api key also this is a good agent so don't put it inside beginner make a different folder for it with skill.yaml, readme ,.env.example you can use other agent for refrence
Thanks for the suggestion!! that makes sense, i'll move this out of the beginner folder and restructure it as a proper standalone agent example....and i'll also add the required files and switch to using OPENROUTER |
|
@Paraschamoli
Let me know if anything else needs changes! |
📝 WalkthroughWalkthroughAdded a complete new PDF Research Agent example service featuring environment configuration, comprehensive documentation, Python implementation with PDF text extraction and LLM summarization via OpenRouter, and a skill definition for microservice integration. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Added comprehensive README for PDF Research Agent detailing features, installation, usage, and configuration.
Removed comments and unnecessary lines from the .env.example file, retaining only the OPENROUTER_API_KEY variable.
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (1)
examples/pdf_research_agent/pdf_research_agent.py (1)
23-27: Make the skill path independent of the caller's working directory.
"skills/pdf-research-skill"only resolves when the process starts from this folder. Running the script from repo root breaks skill discovery even though the script path itself is valid.Suggested refactor
from bindu.penguin.bindufy import bindufy from agno.agent import Agent from agno.models.openrouter import OpenRouter from dotenv import load_dotenv +from pathlib import Path import os @@ config = { @@ - "skills": ["skills/pdf-research-skill"], + "skills": [str(Path(__file__).resolve().parent / "skills" / "pdf-research-skill")],Also applies to: 81-92
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 23 - 27, Replace hard-coded relative skill paths with paths resolved from the script location: compute base_dir = os.path.dirname(__file__) (or os.path.abspath(os.path.dirname(__file__))) and build the skill path via os.path.join(base_dir, "skills", "pdf-research-skill") and use that value wherever the literal "skills/pdf-research-skill" appears (e.g., the bindufy/Agent skill registration at top of file and the other occurrences around lines 81-92) so skill discovery works regardless of the current working directory.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/pdf_research_agent/pdf_research_agent.py`:
- Around line 129-133: The handler is incorrectly inferring failures by
inspecting document_text prefixes; update _read_content to return a structured
result (e.g., a tuple or dataclass like (success: bool, content: str, error:
Optional[str])) or throw an exception on failure, and change the caller in
pdf_research_agent.py to check the structured success flag (or catch the
exception) instead of testing document_text.startswith("[" or "Error");
reference the _read_content function and the document_text variable so you
locate and replace the sentinel-string logic with a clear success/error check
and return the real document content unchanged when success is true.
- Around line 35-45: The _read_content function currently opens arbitrary host
PDF paths; change it to disallow arbitrary paths by requiring PDFs live only
under a controlled directory (configured via an env var like SAFE_PDF_DIR) or by
banning path inputs entirely and accepting uploaded file content/bytes instead.
Implement path validation in _read_content using os.path.abspath and
os.path.commonpath to ensure the resolved file is inside SAFE_PDF_DIR, and
return/raise a clear error if the check fails; if you prefer uploads, change the
API to accept file bytes and parse those with PdfReader. Apply the same
validation/behavior to the other PDF-reading call sites referenced (the blocks
handling PDF paths around the other pdf-reading code), and ensure any logging or
forwarding to OpenRouter only happens after path validation or after using
uploaded content.
- Around line 37-53: The code currently treats any ".pdf" path that doesn't
exist as raw text because the os.path.isfile() check prevents entering the PDF
branch; update the logic handling 'source' so that if
source.strip().endswith(".pdf") but not os.path.isfile(source.strip()) you
return or raise a clear file-not-found error instead of returning the path
string. Locate the PDF handling block (the if source.strip().endswith(".pdf")
and os.path.isfile(...) check and the final "return source" fallback) and change
it to first detect a ".pdf" path, then if the file is missing return a message
like "PDF file '<path>' not found" (or raise FileNotFoundError) before
attempting to import/use PdfReader or falling back to treating the input as raw
text.
In `@examples/pdf_research_agent/README.md`:
- Around line 22-25: Update the startup instructions in the README so the
initial directory is examples/pdf_research_agent (not just examples) and update
the subsequent run commands and the relative skills/pdf-research-skill path so
they resolve from that directory; specifically edit the "Clone and Navigate"
step and the two run command blocks referenced around lines 22–25 and 47–54 to
use cd examples/pdf_research_agent and ensure the config relative path points to
skills/pdf-research-skill (from that directory).
- Around line 105-117: The README has multiple anonymous markdown code fences
(the ASCII architecture diagram and several error-output samples) that trigger
markdownlint MD040; update each of those triple-backtick fences to include the
language hint "text" (e.g., change ``` to ```text) so the diagram and outputs
are explicitly marked as plain text—apply this change to the ASCII architecture
diagram block shown in the diff and to the other anonymous fences containing
error-output or sample text elsewhere in the README.
- Around line 210-215: Update the "## 🛡️ Security & Privacy" section so it does
not claim protections the example does not implement: remove or revise the
bullets "Content Filtering", "Input Validation", and "Output Sanitization" and
instead state the actual protections provided (e.g., "Text-only processing, no
file storage") and an explicit disclaimer that the example forwards user input
to the model and does not perform comprehensive input validation, content
filtering, or output sanitization; reference the example script
pdf_research_agent.py (and its main/handler functions) in the disclaimer so
readers know where to add those protections if desired.
In `@examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml`:
- Around line 74-77: Update the transport.port entry in the skill manifest so it
matches the actual service port used by the agent (change the transport.port
value from 3775 to 3773) to align the skill.yaml with the example code and
README; look for the transport block (the "transport:" key and its children
"protocol", "endpoint", "port") in skill.yaml and set port to 3773 so consumers
hit the correct endpoint.
- Around line 27-29: The manifest advertises application/pdf but the runtime
handler in examples/pdf_research_agent/pdf_research_agent.py only accepts plain
text (raw text or a server-local path string); either remove "application/pdf"
from input_modes in skill.yaml so it only lists "text/plain", or implement true
PDF binary handling in the handler (update the request parsing and the function
that processes inputs in pdf_research_agent.py to accept and stream/parse
application/pdf payloads). Ensure the chosen change is applied consistently by
editing input_modes in skill.yaml or by adding PDF binary parsing logic in the
handler so the manifest matches the actual accepted payload shape.
---
Nitpick comments:
In `@examples/pdf_research_agent/pdf_research_agent.py`:
- Around line 23-27: Replace hard-coded relative skill paths with paths resolved
from the script location: compute base_dir = os.path.dirname(__file__) (or
os.path.abspath(os.path.dirname(__file__))) and build the skill path via
os.path.join(base_dir, "skills", "pdf-research-skill") and use that value
wherever the literal "skills/pdf-research-skill" appears (e.g., the
bindufy/Agent skill registration at top of file and the other occurrences around
lines 81-92) so skill discovery works regardless of the current working
directory.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7d45e6b3-d3e2-47c9-88ae-47d2112eefaa
📒 Files selected for processing (4)
examples/pdf_research_agent/.env.exampleexamples/pdf_research_agent/README.mdexamples/pdf_research_agent/pdf_research_agent.pyexamples/pdf_research_agent/skills/pdf-research-skill/skill.yaml
| def _read_content(source: str) -> str: | ||
| """Return plain text from a PDF file path, or the source string itself.""" | ||
| if source.strip().endswith(".pdf") and os.path.isfile(source.strip()): | ||
| try: | ||
| from pypdf import PdfReader # optional dependency | ||
| reader = PdfReader(source.strip()) | ||
| pages = [page.extract_text() or "" for page in reader.pages] | ||
| text = "\n\n".join(pages) | ||
| if len(text.strip()) < 100: | ||
| return f"PDF file '{source.strip()}' appears to be empty or contains very little text." | ||
| return text |
There was a problem hiding this comment.
Don't let callers choose arbitrary server-side PDF paths.
_read_content() opens any user-supplied .pdf on the host, and the extracted text is then sent to OpenRouter while auth is disabled. That is local file exfiltration, not just a convenience feature. Restrict path mode to a safe directory or allowlist, or require uploaded content instead of raw paths.
Also applies to: 69-72, 92-98
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 35 - 45, The
_read_content function currently opens arbitrary host PDF paths; change it to
disallow arbitrary paths by requiring PDFs live only under a controlled
directory (configured via an env var like SAFE_PDF_DIR) or by banning path
inputs entirely and accepting uploaded file content/bytes instead. Implement
path validation in _read_content using os.path.abspath and os.path.commonpath to
ensure the resolved file is inside SAFE_PDF_DIR, and return/raise a clear error
if the check fails; if you prefer uploads, change the API to accept file bytes
and parse those with PdfReader. Apply the same validation/behavior to the other
PDF-reading call sites referenced (the blocks handling PDF paths around the
other pdf-reading code), and ensure any logging or forwarding to OpenRouter only
happens after path validation or after using uploaded content.
| if source.strip().endswith(".pdf") and os.path.isfile(source.strip()): | ||
| try: | ||
| from pypdf import PdfReader # optional dependency | ||
| reader = PdfReader(source.strip()) | ||
| pages = [page.extract_text() or "" for page in reader.pages] | ||
| text = "\n\n".join(pages) | ||
| if len(text.strip()) < 100: | ||
| return f"PDF file '{source.strip()}' appears to be empty or contains very little text." | ||
| return text | ||
| except ImportError: | ||
| return ( | ||
| f"[pypdf not installed — cannot read '{source.strip()}'. " | ||
| "Run: uv add pypdf]" | ||
| ) | ||
| except Exception as e: | ||
| return f"Error reading PDF '{source.strip()}': {str(e)}" | ||
| return source # treat as raw document text |
There was a problem hiding this comment.
Return a file-not-found error instead of summarizing the path string.
When the input ends with .pdf but the file does not exist, the os.path.isfile() guard skips the PDF branch and returns the literal path as raw text. A typo like /tmp/missing.pdf will be “summarized” instead of producing the documented file-read error.
Suggested fix
def _read_content(source: str) -> str:
"""Return plain text from a PDF file path, or the source string itself."""
- if source.strip().endswith(".pdf") and os.path.isfile(source.strip()):
+ candidate = source.strip()
+ if candidate.lower().endswith(".pdf"):
+ if not os.path.isfile(candidate):
+ return f"Error reading PDF '{candidate}': file does not exist."
try:
from pypdf import PdfReader # optional dependency
- reader = PdfReader(source.strip())
+ reader = PdfReader(candidate)
pages = [page.extract_text() or "" for page in reader.pages]
text = "\n\n".join(pages)
if len(text.strip()) < 100:
- return f"PDF file '{source.strip()}' appears to be empty or contains very little text."
+ return f"PDF file '{candidate}' appears to be empty or contains very little text."
return text
except ImportError:
return (
- f"[pypdf not installed — cannot read '{source.strip()}'. "
+ f"[pypdf not installed — cannot read '{candidate}'. "
"Run: uv add pypdf]"
)
except Exception as e:
- return f"Error reading PDF '{source.strip()}': {str(e)}"
+ return f"Error reading PDF '{candidate}': {e!s}"
return source # treat as raw document text🧰 Tools
🪛 Ruff (0.15.7)
[warning] 51-51: Do not catch blind exception: Exception
(BLE001)
[warning] 52-52: Use explicit conversion flag
Replace with conversion flag
(RUF010)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 37 - 53, The
code currently treats any ".pdf" path that doesn't exist as raw text because the
os.path.isfile() check prevents entering the PDF branch; update the logic
handling 'source' so that if source.strip().endswith(".pdf") but not
os.path.isfile(source.strip()) you return or raise a clear file-not-found error
instead of returning the path string. Locate the PDF handling block (the if
source.strip().endswith(".pdf") and os.path.isfile(...) check and the final
"return source" fallback) and change it to first detect a ".pdf" path, then if
the file is missing return a message like "PDF file '<path>' not found" (or
raise FileNotFoundError) before attempting to import/use PdfReader or falling
back to treating the input as raw text.
| document_text = _read_content(user_input) | ||
|
|
||
| # Check if document processing failed | ||
| if document_text.startswith("[") or document_text.startswith("Error"): | ||
| return document_text |
There was a problem hiding this comment.
Don't infer internal failures from user text prefixes.
A real document that starts with [Draft] or Error analysis... will be returned unsummarized because the handler treats those prefixes as processing errors. _read_content() should return structured status instead of sentinel strings.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/pdf_research_agent/pdf_research_agent.py` around lines 129 - 133,
The handler is incorrectly inferring failures by inspecting document_text
prefixes; update _read_content to return a structured result (e.g., a tuple or
dataclass like (success: bool, content: str, error: Optional[str])) or throw an
exception on failure, and change the caller in pdf_research_agent.py to check
the structured success flag (or catch the exception) instead of testing
document_text.startswith("[" or "Error"); reference the _read_content function
and the document_text variable so you locate and replace the sentinel-string
logic with a clear success/error check and return the real document content
unchanged when success is true.
| ### 1. Clone and Navigate | ||
| ```bash | ||
| cd examples | ||
| ``` |
There was a problem hiding this comment.
The startup steps land in the wrong directory.
After cd examples, both run commands miss the script. The relative skills/pdf-research-skill path in the config also only resolves when the current directory is examples/pdf_research_agent.
Suggested README fix
### 1. Clone and Navigate
```bash
-cd examples
+cd examples/pdf_research_agent</details>
Also applies to: 47-54
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against the current code and only fix it if needed.
In @examples/pdf_research_agent/README.md around lines 22 - 25, Update the
startup instructions in the README so the initial directory is
examples/pdf_research_agent (not just examples) and update the subsequent run
commands and the relative skills/pdf-research-skill path so they resolve from
that directory; specifically edit the "Clone and Navigate" step and the two run
command blocks referenced around lines 22–25 and 47–54 to use cd
examples/pdf_research_agent and ensure the config relative path points to
skills/pdf-research-skill (from that directory).
</details>
<!-- fingerprinting:phantom:medusa:grasshopper:d61549d5-677b-496f-93f2-bf000b710cd4 -->
<!-- This is an auto-generated comment by CodeRabbit -->
| ``` | ||
| ┌─────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ | ||
| │ Client │───▶│ Bindu Core │───▶│ PDF Agent │ | ||
| │ (Postman) │ │ (A2A Protocol) │ │ (Agno + OpenRouter)│ | ||
| └─────────────────┘ └─────────────────────┘ └─────────────────────┘ | ||
| │ │ | ||
| ▼ ▼ | ||
| ┌─────────────┐ ┌─────────────────┐ | ||
| │ DID │ │ PDF Parser │ | ||
| │ Auth │ │ Text Extract │ | ||
| │ Storage │ │ Summary Gen │ | ||
| └─────────────┘ └─────────────────┘ | ||
| ``` |
There was a problem hiding this comment.
Add language hints to the plain-text fences.
markdownlint MD040 is firing on these anonymous blocks. text is enough for the architecture diagram and the error-output samples.
Suggested README fix
-```
+```text-```
+```text</details>
Also applies to: 193-195, 198-200, 203-205
<details>
<summary>🧰 Tools</summary>
<details>
<summary>🪛 markdownlint-cli2 (0.22.0)</summary>
[warning] 105-105: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
</details>
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against the current code and only fix it if needed.
In @examples/pdf_research_agent/README.md around lines 105 - 117, The README has
multiple anonymous markdown code fences (the ASCII architecture diagram and
several error-output samples) that trigger markdownlint MD040; update each of
those triple-backtick fences to include the language hint "text" (e.g., change
totext) so the diagram and outputs are explicitly marked as plain
text—apply this change to the ASCII architecture diagram block shown in the diff
and to the other anonymous fences containing error-output or sample text
elsewhere in the README.
</details>
<!-- fingerprinting:phantom:medusa:grasshopper:d61549d5-677b-496f-93f2-bf000b710cd4 -->
<!-- This is an auto-generated comment by CodeRabbit -->
| ## 🛡️ Security & Privacy | ||
|
|
||
| - **Data Privacy**: Text-only processing, no file storage | ||
| - **Content Filtering**: Educational-appropriate content filtering | ||
| - **Input Validation**: Enabled for all inputs | ||
| - **Output Sanitization**: Enabled for safe responses |
There was a problem hiding this comment.
Don't claim security controls the example doesn't implement.
This example does not perform content filtering, output sanitization, or comprehensive input validation in examples/pdf_research_agent/pdf_research_agent.py; it mostly forwards user input to the model. Leaving these bullets here advertises protections that do not exist.
Suggested README fix
## 🛡️ Security & Privacy
- **Data Privacy**: Text-only processing, no file storage
-- **Content Filtering**: Educational-appropriate content filtering
-- **Input Validation**: Enabled for all inputs
-- **Output Sanitization**: Enabled for safe responses
+- **Input Handling**: Basic empty-input and file-read checks only
+- **Authentication**: Disabled by default for local development
+- **Production Hardening**: Add auth, path restrictions, validation, and output checks before deployment🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/pdf_research_agent/README.md` around lines 210 - 215, Update the "##
🛡️ Security & Privacy" section so it does not claim protections the example
does not implement: remove or revise the bullets "Content Filtering", "Input
Validation", and "Output Sanitization" and instead state the actual protections
provided (e.g., "Text-only processing, no file storage") and an explicit
disclaimer that the example forwards user input to the model and does not
perform comprehensive input validation, content filtering, or output
sanitization; reference the example script pdf_research_agent.py (and its
main/handler functions) in the disclaimer so readers know where to add those
protections if desired.
| input_modes: | ||
| - text/plain # Raw document text pasted directly | ||
| - application/pdf # Local PDF file path resolved server-side |
There was a problem hiding this comment.
Advertise only the input mode the handler actually supports.
The service currently accepts a text message containing either raw text or a server-local path string. There is no binary application/pdf handling in examples/pdf_research_agent/pdf_research_agent.py, so clients following this manifest will send the wrong payload shape.
Suggested manifest fix
input_modes:
- - text/plain # Raw document text pasted directly
- - application/pdf # Local PDF file path resolved server-side
+ - text/plain # Raw document text or a server-local PDF path string📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| input_modes: | |
| - text/plain # Raw document text pasted directly | |
| - application/pdf # Local PDF file path resolved server-side | |
| input_modes: | |
| - text/plain # Raw document text or a server-local PDF path string |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml` around
lines 27 - 29, The manifest advertises application/pdf but the runtime handler
in examples/pdf_research_agent/pdf_research_agent.py only accepts plain text
(raw text or a server-local path string); either remove "application/pdf" from
input_modes in skill.yaml so it only lists "text/plain", or implement true PDF
binary handling in the handler (update the request parsing and the function that
processes inputs in pdf_research_agent.py to accept and stream/parse
application/pdf payloads). Ensure the chosen change is applied consistently by
editing input_modes in skill.yaml or by adding PDF binary parsing logic in the
handler so the manifest matches the actual accepted payload shape.
| transport: | ||
| protocol: A2A (JSON-RPC over HTTP) | ||
| endpoint: POST / | ||
| port: 3775 |
There was a problem hiding this comment.
Sync the declared port with the actual service.
transport.port still says 3775, but the example code and README both start the agent on 3773. Consumers using this manifest will hit the wrong endpoint.
Suggested manifest fix
transport:
protocol: A2A (JSON-RPC over HTTP)
endpoint: POST /
- port: 3775
+ port: 3773📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| transport: | |
| protocol: A2A (JSON-RPC over HTTP) | |
| endpoint: POST / | |
| port: 3775 | |
| transport: | |
| protocol: A2A (JSON-RPC over HTTP) | |
| endpoint: POST / | |
| port: 3773 |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/pdf_research_agent/skills/pdf-research-skill/skill.yaml` around
lines 74 - 77, Update the transport.port entry in the skill manifest so it
matches the actual service port used by the agent (change the transport.port
value from 3775 to 3773) to align the skill.yaml with the example code and
README; look for the transport block (the "transport:" key and its children
"protocol", "endpoint", "port") in skill.yaml and set port to 3773 so consumers
hit the correct endpoint.
|
@raahulrahl Thank you for merging the pull request. I’m currently occupied with college commitments, so I’m unable to look into the issue right now. I’ll review and address it in the next few days. |
Summary
Problem:
There was no beginner example demonstrating how to build a document research agent that processes PDFs and raw text using Bindu.
Why it matters:
Developers learning Bindu need a clear real-world example showing how an AI agent can be converted into a live service using
bindufy().What changed:
Added a new example agent
pdf_research_agent.pyunderexamples/beginner/.The agent accepts either a PDF file path or raw document text and returns a structured summary.
What did NOT change:
No changes were made to core Bindu functionality, APIs, storage, or scheduler components.
Change Type
Scope
Linked Issue/PR
None
User-Visible / Behavior Changes
A new beginner example agent is available:
examples/beginner/pdf_research_agent.py
This demonstrates how to:
pypdfbindufy()Security Impact
New permissions/capabilities? No
Secrets handling changed? No
New network calls? No
Database changes? No
Auth changes? No
Verification
Environment
OS: Windows 11
Python: 3.13
Storage backend: InMemoryStorage
Scheduler backend: InMemoryScheduler
Steps to Test
python pdf_research_agent.py
curl -X POST http://localhost:3775/
tasks/get.Expected Behavior
Agent returns a structured summary of the provided document text or PDF content.
Actual Behavior
Agent successfully returns summarized output through the Bindu task system.
Human Verification
Verified scenarios:
Edge cases checked:
Not verified:
Compatibility / Migration
Backward compatible: Yes
Risks and Mitigations
None
Checklist
Summary by CodeRabbit
New Features
Documentation