Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/scheduled-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
jobs:
scheduled-integration-tests:
runs-on: ubuntu-latest
env:
HAS_NUTRIENT_API_KEY: ${{ secrets.NUTRIENT_API_KEY != '' }}

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand All @@ -24,7 +26,7 @@ jobs:
pip install ".[dev]"

- name: Run all integration tests
if: secrets.NUTRIENT_API_KEY != ''
if: env.HAS_NUTRIENT_API_KEY == 'true'
env:
NUTRIENT_API_KEY: ${{ secrets.NUTRIENT_API_KEY }}
run: |
Expand All @@ -35,7 +37,7 @@ jobs:
id: test-run

- name: Skip scheduled tests (no API key)
if: secrets.NUTRIENT_API_KEY == ''
if: env.HAS_NUTRIENT_API_KEY != 'true'
run: |
echo "⏭️ Skipping scheduled integration tests - NUTRIENT_API_KEY not available"
echo "Configure NUTRIENT_API_KEY secret to enable scheduled API validation"
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Load example credentials from `.env` before client creation, attach a working DWS extraction tool to CrewAI, and keep LangChain redaction results in `output/redacted.pdf` instead of returning PDF bytes to the model.

## [3.1.0] - 2026-05-27

- Added `client.parse()` for the Data Extraction API (`/extraction/parse`)
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ from nutrient_dws import NutrientClient
client = NutrientClient(api_key='your_api_key')
```

## Framework Quickstarts

Framework-oriented snippets are available in `examples/src/`:

- `framework_openai_agents.py`
- `framework_langchain.py`
- `framework_crewai.py`

Syntax-check commands:

```bash
python -m py_compile examples/src/framework_openai_agents.py
python -m py_compile examples/src/framework_langchain.py
python -m py_compile examples/src/framework_crewai.py
```

## Direct Methods

The client provides numerous async methods for document processing:
Expand Down
54 changes: 54 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ This example project demonstrates how to use the Nutrient DWS Python Client for
- `src/` - Contains Python source files
- `direct_method.py` - Examples using direct method calls
- `workflow.py` - Examples using the workflow builder pattern
- `framework_openai_agents.py` - OpenAI Agents SDK integration sketch
- `framework_langchain.py` - LangChain/LangGraph integration sketch
- `framework_crewai.py` - CrewAI integration sketch
- `output/` - Directory where processed files will be saved
- `.env.example` - Example environment variables file

Expand Down Expand Up @@ -122,6 +125,57 @@ This will:
3. Extract text with JSON output
4. Execute a complex multi-step workflow

### Framework Examples

These examples are intentionally minimal and focus on framework wiring:

Set both `NUTRIENT_API_KEY` and `OPENAI_API_KEY` in `examples/.env` before running them. Each example loads `.env` and fails clearly when either key is missing. The examples use `gpt-4.1-mini` and can make billable DWS and model calls when run normally. CrewAI attaches a real text-extraction tool before planning subsequent processing. The LangChain tool saves the redacted PDF to `output/redacted.pdf` and returns only its path and byte count to the model.

Choose **one framework per fresh virtual environment**. Their OpenAI dependency ranges can differ, so installing all three together can cause lengthy dependency backtracking. From `examples/`, follow only the option you want; use Python 3.10–3.13 for CrewAI. Activation commands below are for macOS/Linux; on Windows use the environment's `Scripts\activate`.

OpenAI Agents:

```bash
python -m venv example_venv_agents
source example_venv_agents/bin/activate
pip install -e '..[dev]' openai-agents
python src/framework_openai_agents.py
```

LangChain/LangGraph, in a separate environment:

```bash
python -m venv example_venv_langchain
source example_venv_langchain/bin/activate
pip install -e '..[dev]' langchain langchain-openai langgraph
python src/framework_langchain.py
```

CrewAI, in a separate environment:

```bash
python -m venv example_venv_crewai
source example_venv_crewai/bin/activate
pip install -e '..[dev]' crewai
python src/framework_crewai.py
```

Syntax-check only:

```bash
python -m py_compile src/framework_openai_agents.py
python -m py_compile src/framework_langchain.py
python -m py_compile src/framework_crewai.py
```

From the repository root, after installing the optional frameworks and development dependencies, run the offline framework smoke tests:

```bash
python -m pytest tests/unit/test_framework_examples.py -v
```

These tests register and invoke the actual framework tools with a controlled Nutrient client. They block network connections and do not run an LLM or upload a document. Framework-specific cases skip if their optional dependency is absent.

## Output

All processed files will be saved to the `output/` directory. You can examine these files to see the results of the document processing operations.
Expand Down
66 changes: 66 additions & 0 deletions examples/src/framework_crewai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import os

from crewai import Agent, Crew, Task
from crewai.tools import tool
from dotenv import load_dotenv

from nutrient_dws import NutrientClient

load_dotenv()
api_key = os.getenv("NUTRIENT_API_KEY")
if not api_key:
raise SystemExit("NUTRIENT_API_KEY is required in your environment or .env file.")
if not os.getenv("OPENAI_API_KEY"):
raise SystemExit("OPENAI_API_KEY is required in your environment or .env file.")

client = NutrientClient(api_key=api_key)


@tool("extract_document_text")
async def extract_document_text(input_path: str) -> str:
"""Extract document text with Nutrient DWS before planning further processing."""
result = await client.extract_text(input_path)
return str(result)


document_planner = Agent(
role="Document Workflow Planner",
goal="Design a robust Nutrient DWS processing pipeline for incoming files.",
backstory="You optimize OCR, extraction, and redaction workflows for accuracy.",
tools=[extract_document_text],
llm="gpt-4.1-mini",
)

document_reviewer = Agent(
role="Document QA Reviewer",
goal="Review proposed workflow plans for failure modes and compliance risks.",
backstory="You specialize in validating document automation quality.",
llm="gpt-4.1-mini",
)

plan_task = Task(
description=(
"Use your extraction tool on ./assets/sample.pdf, then design a pipeline "
"that redacts emails and exports a cleaned PDF."
),
expected_output="Extracted text and a numbered plan for the remaining processing.",
agent=document_planner,
)

review_task = Task(
description=(
"Review the proposed pipeline. Flag risks and add fallback handling steps."
),
expected_output="Risk review with concrete mitigations.",
agent=document_reviewer,
)

crew = Crew(
agents=[document_planner, document_reviewer],
tasks=[plan_task, review_task],
verbose=True,
)


if __name__ == "__main__":
print(crew.kickoff())
54 changes: 54 additions & 0 deletions examples/src/framework_langchain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import asyncio
import os
from pathlib import Path

from dotenv import load_dotenv
from langchain_core.tools import StructuredTool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

from nutrient_dws import NutrientClient

load_dotenv()
api_key = os.getenv("NUTRIENT_API_KEY")
if not api_key:
raise SystemExit("NUTRIENT_API_KEY is required in your environment or .env file.")
if not os.getenv("OPENAI_API_KEY"):
raise SystemExit("OPENAI_API_KEY is required in your environment or .env file.")

client = NutrientClient(api_key=api_key)


async def redact_emails(path: str) -> str:
result = await client.create_redactions_ai(
path,
criteria="Redact all email addresses.",
redaction_state="apply",
)
output_path = Path("output") / "redacted.pdf"
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_bytes(result["buffer"])
return f"Saved redacted PDF to {output_path} ({len(result['buffer'])} bytes)."


tool = StructuredTool.from_function(
coroutine=redact_emails,
name="redact_emails",
description="Redact email addresses from a document using Nutrient DWS.",
)


async def main() -> None:
model = ChatOpenAI(
model="gpt-4.1-mini",
api_key=os.environ["OPENAI_API_KEY"],
)
Comment thread
jdrhyne marked this conversation as resolved.
agent = create_react_agent(model, [tool])
state = await agent.ainvoke(
{"messages": [("user", "Redact emails from ./assets/sample.pdf.")]}
)
print(state["messages"][-1].content)


if __name__ == "__main__":
asyncio.run(main())
43 changes: 43 additions & 0 deletions examples/src/framework_openai_agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import asyncio
import os

from agents import Agent, Runner, function_tool
from dotenv import load_dotenv

from nutrient_dws import NutrientClient

load_dotenv()
api_key = os.getenv("NUTRIENT_API_KEY")
if not api_key:
raise SystemExit("NUTRIENT_API_KEY is required in your environment or .env file.")
if not os.getenv("OPENAI_API_KEY"):
raise SystemExit("OPENAI_API_KEY is required in your environment or .env file.")

client = NutrientClient(api_key=api_key)


@function_tool
async def extract_text(input_path: str) -> str:
"""Extract text from a document using Nutrient DWS."""
result = await client.extract_text(input_path)
return str(result)


assistant = Agent(
name="nutrient-openai-agents-demo",
model="gpt-4.1-mini",
instructions="Help users extract text and summarize document workflows.",
tools=[extract_text],
)


async def main() -> None:
run = await Runner.run(
assistant,
"Extract text from ./assets/sample.pdf and summarize it in three bullets.",
)
print(run.final_output)


if __name__ == "__main__":
asyncio.run(main())
2 changes: 1 addition & 1 deletion src/nutrient_dws/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ async def convert(
result = await builder.output_markdown().execute()
elif target_format in ["png", "jpeg", "jpg", "webp"]:
result = await builder.output_image(
cast("Literal['png', 'jpeg', 'jpg', 'webp']", target_format),
target_format,
{"dpi": 300},
).execute()
else:
Expand Down
2 changes: 1 addition & 1 deletion src/nutrient_dws/types/instant_json/form_field_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

class FormFieldValue(TypedDict):
name: str
value: NotRequired[str | None | list[str]]
value: NotRequired[str | list[str] | None]
type: Literal["pspdfkit/form-field-value"]
v: Literal[1]
optionIndexes: NotRequired[list[int]]
Expand Down
Loading
Loading