Skip to content
Merged
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
1 change: 1 addition & 0 deletions .env.test.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ MIDDLEMAN_BASE_URL=https://middleman-ai.com
MIDDLEMAN_API_KEY=your_test_api_key
MIDDLEMAN_TEST_PDF_TEMPLATE_ID=your_test_pdf_template_id
MIDDLEMAN_TEST_PPTX_TEMPLATE_ID=your_test_pptx_template_id
MIDDLEMAN_TEST_XLSX_TEMPLATE_ID=your_test_xlsx_template_id
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@

フォーマットは[Keep a Changelog](https://keepachangelog.com/ja/)に基づいています。

## [v0.3.1] - 2026-01-15

- 「マークダウンから PDF 変換」ツールでローカル画像ファイルを使って PDF を生成できる機能を追加

### 追加

- 「XLSX → PDF 変換」ツールとの連携を追加(テンプレート解析・変換実行)

## [v0.3.0] - 2025-12-22

### 追加

- 「XLSX → PDF変換」ツールとの連携を追加(テンプレート解析・変換実行)
- 「XLSX → PDF 変換」ツールとの連携を追加(テンプレート解析・変換実行)

## [v0.2.0] - 2025-07-05

### 追加

- 「Mermaid ダイアグラムを画像変換」ツールとの連携を追加
- 「マークダウン → DOCX変換」にテンプレート ID 指定機能を追加
- 「マークダウン → DOCX 変換」にテンプレート ID 指定機能を追加

### 削除

Expand Down
1 change: 1 addition & 0 deletions examples/client_usage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Middleman.ai の ToolsClient メソッドを直接使用する最小限のサン
export MIDDLEMAN_API_KEY="YOUR_API_KEY"
export MIDDLEMAN_PDF_TEMPLATE_ID="YOUR_TEMPLATE_ID" # マークダウン to PDFのテンプレートIDを必要に応じて設定
export MIDDLEMAN_PPTX_TEMPLATE_ID="YOUR_TEMPLATE_ID" # JSON to PPTXの機能を使用する場合sample_template.pptxをMiddleman.aiにアップロードしてそのIDを設定
export MIDDLEMAN_XLSX_TEMPLATE_ID="YOUR_TEMPLATE_ID" # XLSX to PDFの機能を使用する場合、プレースホルダー付きExcelテンプレートをアップロードしてそのIDを設定
```

2. 依存関係のインストール:
Expand Down
28 changes: 27 additions & 1 deletion examples/client_usage/main.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import os

from dotenv import load_dotenv

from middleman_ai import ToolsClient

load_dotenv()


def main() -> None:
# Initialize client
client = ToolsClient(api_key=os.getenv("MIDDLEMAN_API_KEY", ""))
client = ToolsClient(
api_key=os.getenv("MIDDLEMAN_API_KEY", ""),
base_url=os.getenv("MIDDLEMAN_BASE_URL", "https://middleman-ai.com"),
)

# Markdown → PDF
markdown_text = "# Sample\nThis is a test."
pdf_template_id = os.getenv("MIDDLEMAN_PDF_TEMPLATE_ID", None)
pdf_url = client.md_to_pdf(markdown_text, pdf_template_id=pdf_template_id)
print(f"Generated PDF URL (default template): {pdf_url}")

# Markdown → PDF (with local images)
markdown_with_image = "# Image Test\n\n![test](test_image.png)"
pdf_url = client.md_to_pdf(markdown_with_image, image_paths=["test_image.png"])
print(f"Generated PDF URL (with image): {pdf_url}")

# Markdown → DOCX
docx_url = client.md_to_docx(markdown_text)
print(f"Generated DOCX URL: {docx_url}")
Expand Down Expand Up @@ -73,6 +85,20 @@ def main() -> None:
pptx_url = client.json_to_pptx_execute_v2(pptx_template_id, presentation)
print(f"Generated PPTX URL: {pptx_url}")

# XLSX → PDF (analyze)
xlsx_template_id = os.getenv("MIDDLEMAN_XLSX_TEMPLATE_ID", "")
if xlsx_template_id:
result = client.xlsx_to_pdf_analyze(xlsx_template_id)
print(f"XLSX Template sheet: {result.sheet_name}")
print(f"Placeholders: {[p.key for p in result.placeholders]}")

# XLSX → PDF (execute)
placeholders = {p.key: f"Sample {p.key}" for p in result.placeholders}
result = client.xlsx_to_pdf_execute(xlsx_template_id, placeholders)
print(f"Generated PDF URL (from XLSX): {result.pdf_url}")
else:
print("Skipping XLSX → PDF: MIDDLEMAN_XLSX_TEMPLATE_ID not set")


if __name__ == "__main__":
main()
Binary file added examples/client_usage/test_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions examples/client_usage/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions src/middleman_ai/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ def cli() -> None:

@cli.command()
@click.argument("template_id", required=False)
def md_to_pdf(template_id: str | None = None) -> None:
@click.option(
"--images",
"-i",
multiple=True,
type=click.Path(exists=True),
help="画像ファイルパス(複数指定可)。Markdown内でファイル名で参照できます。",
)
def md_to_pdf(template_id: str | None = None, images: tuple[str, ...] = ()) -> None:
"""Convert Markdown to PDF."""
print("md_to_pdf コマンドを実行しています...")
try:
Expand All @@ -53,9 +60,14 @@ def md_to_pdf(template_id: str | None = None) -> None:
print(
f"読み込んだMarkdown ({len(markdown_text)} 文字): {markdown_text[:50]}..."
)
image_paths = list(images) if images else None
if image_paths:
print(f"添付画像: {image_paths}")
with click.progressbar(length=1, label="PDFに変換中...", show_eta=False) as bar:
print("APIを呼び出しています...")
pdf_url = client.md_to_pdf(markdown_text, pdf_template_id=template_id)
pdf_url = client.md_to_pdf(
markdown_text, pdf_template_id=template_id, image_paths=image_paths
)
bar.update(1)
print(f"変換結果URL: {pdf_url}")
if template_id:
Expand Down
55 changes: 46 additions & 9 deletions src/middleman_ai/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import logging
import os
from typing import Any, Dict, List, cast

import requests
Expand Down Expand Up @@ -154,12 +155,18 @@ def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
except json.JSONDecodeError as e:
raise ValidationError("Invalid JSON response") from e

def md_to_pdf(self, markdown_text: str, pdf_template_id: str | None = None) -> str:
def md_to_pdf(
self,
markdown_text: str,
pdf_template_id: str | None = None,
image_paths: List[str] | None = None,
) -> str:
"""Markdown文字列をPDFに変換し、PDFのダウンロードURLを返します。

Args:
markdown_text: 変換対象のMarkdown文字列
pdf_template_id: テンプレートID(UUID)
image_paths: ローカル画像ファイルパスのリスト(Markdown内で参照可能)

Returns:
str: 生成されたPDFのURL
Expand All @@ -169,21 +176,51 @@ def md_to_pdf(self, markdown_text: str, pdf_template_id: str | None = None) -> s
その他、_handle_responseで定義される例外
"""
try:
response = self.session.post(
f"{self.base_url}/api/v1/tools/md-to-pdf",
json={
"markdown": markdown_text,
"pdf_template_id": pdf_template_id,
},
files: List[tuple[str, tuple[str, bytes, str]]] = []
if image_paths:
for path in image_paths:
with open(path, "rb") as f:
filename = os.path.basename(path)
content = f.read()
mime_type = self._get_image_mime_type(filename)
files.append(("files", (filename, content, mime_type)))

data: Dict[str, Any] = {"markdown": markdown_text}
if pdf_template_id:
data["pdf_template_id"] = pdf_template_id

headers = dict(self.session.headers)
del headers["Content-Type"]

response = requests.post(
f"{self.base_url}/api/v1/tools/md-to-pdf/form",
data=data,
files=files if files else None,
headers=headers,
timeout=self.timeout,
)
data = self._handle_response(response)
result = MdToPdfResponse.model_validate(data)
result_data = self._handle_response(response)
result = MdToPdfResponse.model_validate(result_data)
return result.pdf_url
except PydanticValidationError as e:
raise ValidationError(str(e)) from e
except requests.exceptions.RequestException as e:
raise ConnectionError() from e
except OSError as e:
raise ValidationError(f"Failed to read image file: {e}") from e

def _get_image_mime_type(self, filename: str) -> str:
"""ファイル名から画像のMIMEタイプを推測"""
ext = filename.lower().split(".")[-1]
mime_types = {
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"gif": "image/gif",
"webp": "image/webp",
"svg": "image/svg+xml",
}
return mime_types.get(ext, "application/octet-stream")

def md_to_docx(
self,
Expand Down
22 changes: 20 additions & 2 deletions src/middleman_ai/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,20 @@ def md_to_pdf(markdown_text: str, pdf_template_id: str | None = None) -> str:


@mcp.tool()
def md_file_to_pdf(md_file_full_path: str, pdf_template_id: str | None = None) -> str:
def md_file_to_pdf(
md_file_full_path: str,
pdf_template_id: str | None = None,
image_paths: List[str] | None = None,
) -> str:
"""
Convert a Markdown file to PDF and return the download URL.

Args:
md_file_full_path: Path to the local Markdown file
pdf_template_id: Optional ID of the PDF template to use.
If not provided, the default template will be used
image_paths: Optional list of local image file paths.
These images can be referenced in the Markdown by their filename.

Returns:
The URL to download the generated PDF
Expand All @@ -59,9 +65,21 @@ def md_file_to_pdf(md_file_full_path: str, pdf_template_id: str | None = None) -
if not os.access(file_path, os.R_OK):
raise ValueError(f"File not readable: {md_file_full_path}")

if image_paths:
for img_path in image_paths:
img_file = Path(img_path)
if not img_file.exists():
raise ValueError(f"Image file not found: {img_path}")
if not img_file.is_file():
raise ValueError(f"Image path is not a file: {img_path}")
if not os.access(img_file, os.R_OK):
raise ValueError(f"Image file not readable: {img_path}")

with file_path.open("r") as f:
md_text = f.read()
return client.md_to_pdf(md_text, pdf_template_id=pdf_template_id)
return client.md_to_pdf(
md_text, pdf_template_id=pdf_template_id, image_paths=image_paths
)


@mcp.tool()
Expand Down
89 changes: 44 additions & 45 deletions tests/cassettes/test_md_to_pdf_vcr.yaml
Original file line number Diff line number Diff line change
@@ -1,47 +1,46 @@
interactions:
- request:
body:
'{"markdown": "# Test Heading\n\n This is a test markdown document.\n\n ##
Section 1\n - Item 1\n - Item 2\n ", "pdf_template_id": null}'
headers:
Accept:
- "*/*"
Accept-Encoding:
- gzip, deflate, zstd
Connection:
- keep-alive
Content-Length:
- "150"
Content-Type:
- application/json
User-Agent:
- python-requests/2.32.3
authorization:
- DUMMY
method: POST
uri: https://middleman-ai.com/api/v1/tools/md-to-pdf
response:
body:
string:
'{"pdf_url":"https://middleman-ai.com/s/709dd3af/E9S5ohwi9r","important_remark_for_user":"The
URL expires in 1 hour.Please note that you will not be able to download the
file after it has expired."}'
headers:
content-length:
- "201"
content-type:
- application/json
date:
- FILTERED
server:
- FILTERED
vary:
- Accept-Encoding
x-middleware-rewrite:
- FILTERED
x-request-id:
- FILTERED
status:
code: 200
message: OK
- request:
body: markdown=%23+Test+Heading%0A%0A++++This+is+a+test+markdown+document.%0A%0A++++%23%23+Section+1%0A++++-+Item+1%0A++++-+Item+2%0A++++
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Length:
- '131'
Content-Type:
- application/x-www-form-urlencoded
User-Agent:
- python-requests/2.32.3
authorization:
- DUMMY
method: POST
uri: https://middleman-ai.com/api/v1/tools/md-to-pdf/form
response:
body:
string: '{"pdf_url":"https://middleman-ai.com/s/cd6b4bab/1pWXW5I51n","important_remark_for_user":"The
URL expires in 1 hour.Please note that you will not be able to download the
file after it has expired."}'
headers:
content-length:
- '201'
content-type:
- application/json
date:
- FILTERED
server:
- FILTERED
set-cookie:
- FILTERED
vary:
- Accept-Encoding
x-middleware-rewrite:
- FILTERED
x-request-id:
- FILTERED
status:
code: 200
message: OK
version: 1
Loading