feat: chatgpt tools for search and fetch (#305)

Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Drew Cain <groksrc@users.noreply.github.com>
This commit is contained in:
Paul Hernandez
2025-09-25 11:55:56 -05:00
committed by GitHub
parent bcf7f40979
commit f40ab31685
7 changed files with 912 additions and 16 deletions
+3 -1
View File
@@ -24,7 +24,7 @@ jobs:
pull-requests: write
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -36,7 +36,9 @@ jobs:
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
track_progress: true # Enable visual progress tracking
allowed_bots: '*'
prompt: |
Review this Basic Memory PR against our team checklist:
+4
View File
@@ -24,6 +24,8 @@ from basic_memory.mcp.tools.project_management import (
create_memory_project,
delete_project,
)
# ChatGPT-compatible tools
from basic_memory.mcp.tools.chatgpt_tools import search, fetch
__all__ = [
"build_context",
@@ -32,12 +34,14 @@ __all__ = [
"delete_note",
"delete_project",
"edit_note",
"fetch",
"list_directory",
"list_memory_projects",
"move_note",
"read_content",
"read_note",
"recent_activity",
"search",
"search_notes",
"sync_status",
"view_note",
+202
View File
@@ -0,0 +1,202 @@
"""ChatGPT-compatible MCP tools for Basic Memory.
These adapters expose Basic Memory's search/fetch functionality using the exact
tool names and response structure OpenAI's MCP clients expect: each call returns
a list containing a single `{"type": "text", "text": "{...json...}"}` item.
"""
import json
from typing import Any, Dict, List, Optional
from loguru import logger
from fastmcp import Context
from basic_memory.mcp.server import mcp
from basic_memory.mcp.tools.search import search_notes
from basic_memory.mcp.tools.read_note import read_note
from basic_memory.schemas.search import SearchResponse
def _format_search_results_for_chatgpt(results: SearchResponse) -> List[Dict[str, Any]]:
"""Format search results according to ChatGPT's expected schema.
Returns a list of result objects with id, title, and url fields.
"""
formatted_results = []
for result in results.results:
formatted_result = {
"id": result.permalink or f"doc-{len(formatted_results)}",
"title": result.title if result.title and result.title.strip() else "Untitled",
"url": result.permalink or ""
}
formatted_results.append(formatted_result)
return formatted_results
def _format_document_for_chatgpt(
content: str, identifier: str, title: Optional[str] = None
) -> Dict[str, Any]:
"""Format document content according to ChatGPT's expected schema.
Returns a document object with id, title, text, url, and metadata fields.
"""
# Extract title from markdown content if not provided
if not title and isinstance(content, str):
lines = content.split('\n')
if lines and lines[0].startswith('# '):
title = lines[0][2:].strip()
else:
title = identifier.split('/')[-1].replace('-', ' ').title()
# Ensure title is never None
if not title:
title = "Untitled Document"
# Handle error cases
if isinstance(content, str) and content.startswith("# Note Not Found"):
return {
"id": identifier,
"title": title or "Document Not Found",
"text": content,
"url": identifier,
"metadata": {"error": "Document not found"}
}
return {
"id": identifier,
"title": title or "Untitled Document",
"text": content,
"url": identifier,
"metadata": {"format": "markdown"}
}
@mcp.tool(
description="Search for content across the knowledge base"
)
async def search(
query: str,
context: Context | None = None,
) -> List[Dict[str, Any]]:
"""ChatGPT/OpenAI MCP search adapter returning a single text content item.
Args:
query: Search query (full-text syntax supported by `search_notes`)
context: Optional FastMCP context passed through for auth/session data
Returns:
List with one dict: `{ "type": "text", "text": "{...JSON...}" }`
where the JSON body contains `results`, `total_count`, and echo of `query`.
"""
logger.info(f"ChatGPT search request: query='{query}'")
try:
# Call underlying search_notes with sensible defaults for ChatGPT
results = await search_notes.fn(
query=query,
project=None, # Let project resolution happen automatically
page=1,
page_size=10, # Reasonable default for ChatGPT consumption
search_type="text", # Default to full-text search
context=context
)
# Handle string error responses from search_notes
if isinstance(results, str):
logger.warning(f"Search failed with error: {results[:100]}...")
search_results = {
"results": [],
"error": "Search failed",
"error_details": results[:500] # Truncate long error messages
}
else:
# Format successful results for ChatGPT
formatted_results = _format_search_results_for_chatgpt(results)
search_results = {
"results": formatted_results,
"total_count": len(results.results), # Use actual count from results
"query": query
}
logger.info(f"Search completed: {len(formatted_results)} results returned")
# Return in MCP content array format as required by OpenAI
return [
{
"type": "text",
"text": json.dumps(search_results, ensure_ascii=False)
}
]
except Exception as e:
logger.error(f"ChatGPT search failed for query '{query}': {e}")
error_results = {
"results": [],
"error": "Internal search error",
"error_message": str(e)[:200]
}
return [
{
"type": "text",
"text": json.dumps(error_results, ensure_ascii=False)
}
]
@mcp.tool(
description="Fetch the full contents of a search result document"
)
async def fetch(
id: str,
context: Context | None = None,
) -> List[Dict[str, Any]]:
"""ChatGPT/OpenAI MCP fetch adapter returning a single text content item.
Args:
id: Document identifier (permalink, title, or memory URL)
context: Optional FastMCP context passed through for auth/session data
Returns:
List with one dict: `{ "type": "text", "text": "{...JSON...}" }`
where the JSON body includes `id`, `title`, `text`, `url`, and metadata.
"""
logger.info(f"ChatGPT fetch request: id='{id}'")
try:
# Call underlying read_note function
content = await read_note.fn(
identifier=id,
project=None, # Let project resolution happen automatically
page=1,
page_size=10, # Default pagination
context=context
)
# Format the document for ChatGPT
document = _format_document_for_chatgpt(content, id)
logger.info(f"Fetch completed: id='{id}', content_length={len(document.get('text', ''))}")
# Return in MCP content array format as required by OpenAI
return [
{
"type": "text",
"text": json.dumps(document, ensure_ascii=False)
}
]
except Exception as e:
logger.error(f"ChatGPT fetch failed for id '{id}': {e}")
error_document = {
"id": id,
"title": "Fetch Error",
"text": f"Failed to fetch document: {str(e)[:200]}",
"url": id,
"metadata": {"error": "Fetch failed"}
}
return [
{
"type": "text",
"text": json.dumps(error_document, ensure_ascii=False)
}
]
+15 -15
View File
@@ -25,7 +25,7 @@ async def read_note(
page_size: int = 10,
context: Context | None = None,
) -> str:
"""Read a markdown note from the knowledge base.
"""Return the raw markdown for a note, or guidance text if no match is found.
Finds and retrieves a note by its title, permalink, or content search,
returning the raw markdown content including observations, relations, and metadata.
@@ -171,25 +171,25 @@ def format_not_found_message(project: str | None, identifier: str) -> str:
"""Format a helpful message when no note was found."""
return dedent(f"""
# Note Not Found in {project}: "{identifier}"
I couldn't find any notes matching "{identifier}". Here are some suggestions:
## Check Identifier Type
- If you provided a title, try using the exact permalink instead
- If you provided a permalink, check for typos or try a broader search
## Search Instead
Try searching for related content:
```
search_notes(project="{project}", query="{identifier}")
```
## Recent Activity
Check recently modified notes:
```
recent_activity(timeframe="7d")
```
## Create New Note
This might be a good opportunity to create a new note on this topic:
```
@@ -198,13 +198,13 @@ def format_not_found_message(project: str | None, identifier: str) -> str:
title="{identifier.capitalize()}",
content='''
# {identifier.capitalize()}
## Overview
[Your content here]
## Observations
- [category] [Observation about {identifier}]
## Relations
- relates_to [[Related Topic]]
''',
@@ -218,9 +218,9 @@ def format_related_results(project: str | None, identifier: str, results) -> str
"""Format a helpful message with related results when an exact match wasn't found."""
message = dedent(f"""
# Note Not Found in {project}: "{identifier}"
I couldn't find an exact match for "{identifier}", but I found some related notes:
""")
for i, result in enumerate(results):
@@ -228,24 +228,24 @@ def format_related_results(project: str | None, identifier: str, results) -> str
## {i + 1}. {result.title}
- **Type**: {result.type.value}
- **Permalink**: {result.permalink}
You can read this note with:
```
read_note(project="{project}", {result.permalink}")
```
""")
message += dedent(f"""
## Try More Specific Lookup
For exact matches, try using the full permalink from one of the results above.
## Search For More Results
To see more related content:
```
search_notes(project="{project}", query="{identifier}")
```
## Create New Note
If none of these match what you're looking for, consider creating a new note:
```
+1
View File
@@ -122,6 +122,7 @@ def app_config(config_home, tmp_path, monkeypatch) -> BasicMemoryConfig:
env="test",
projects=projects,
default_project="test-project",
default_project_mode=True,
update_permalinks_on_move=True,
)
return app_config
@@ -0,0 +1,459 @@
"""
Integration tests for ChatGPT-compatible MCP tools.
Tests the complete flow of search and fetch tools designed for ChatGPT integration,
ensuring they properly wrap Basic Memory's MCP tools and return OpenAI-compatible
MCP content array format.
"""
import json
import pytest
from fastmcp import Client
def extract_mcp_json_content(mcp_result):
"""
Helper to extract JSON content from MCP CallToolResult.
FastMCP auto-serializes our List[Dict[str, Any]] return values, so we need to:
1. Get the content list from the CallToolResult
2. Parse the JSON string in the text field (which is our serialized list)
3. Extract the actual JSON from the MCP content array structure
"""
content_list = mcp_result.content
mcp_content_list = json.loads(content_list[0].text)
return json.loads(mcp_content_list[0]["text"])
@pytest.mark.asyncio
async def test_chatgpt_search_basic(mcp_server, app, test_project):
"""Test basic ChatGPT search functionality with MCP content array format."""
async with Client(mcp_server) as client:
# Create test notes for searching
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "Machine Learning Fundamentals",
"folder": "ai",
"content": (
"# Machine Learning Fundamentals\n\n"
"Introduction to ML concepts and algorithms."
),
"tags": "ml,ai,fundamentals",
},
)
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "Deep Learning with PyTorch",
"folder": "ai",
"content": (
"# Deep Learning with PyTorch\n\n"
"Building neural networks using PyTorch framework."
),
"tags": "pytorch,deep-learning,ai",
},
)
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "Data Visualization Guide",
"folder": "data",
"content": (
"# Data Visualization Guide\n\n"
"Creating charts and graphs for data analysis."
),
"tags": "visualization,data,charts",
},
)
# Test ChatGPT search tool
search_result = await client.call_tool(
"search",
{
"query": "Machine Learning",
},
)
# Extract JSON content from MCP result
results_json = extract_mcp_json_content(search_result)
assert "results" in results_json
assert len(results_json["results"]) > 0
# Check result structure
first_result = results_json["results"][0]
assert "id" in first_result
assert "title" in first_result
assert "url" in first_result
# Verify correct content found
titles = [r["title"] for r in results_json["results"]]
assert "Machine Learning Fundamentals" in titles
assert "Data Visualization Guide" not in titles
@pytest.mark.asyncio
async def test_chatgpt_search_empty_results(mcp_server, app, test_project):
"""Test ChatGPT search with no matching results."""
async with Client(mcp_server) as client:
# Search for non-existent content
search_result = await client.call_tool(
"search",
{
"query": "NonExistentTopic12345",
},
)
# Extract JSON content from MCP result
results_json = extract_mcp_json_content(search_result)
assert "results" in results_json
assert len(results_json["results"]) == 0
assert results_json["query"] == "NonExistentTopic12345"
@pytest.mark.asyncio
async def test_chatgpt_search_with_boolean_operators(mcp_server, app, test_project):
"""Test ChatGPT search with boolean operators."""
async with Client(mcp_server) as client:
# Create test notes
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "Python Web Frameworks",
"folder": "dev",
"content": (
"# Python Web Frameworks\n\n"
"Comparing Django and Flask for web development."
),
"tags": "python,web,frameworks",
},
)
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "JavaScript Frameworks",
"folder": "dev",
"content": "# JavaScript Frameworks\n\nReact, Vue, and Angular comparison.",
"tags": "javascript,web,frameworks",
},
)
# Test with AND operator
search_result = await client.call_tool(
"search",
{
"query": "Python AND frameworks",
},
)
results_json = extract_mcp_json_content(search_result)
titles = [r["title"] for r in results_json["results"]]
assert "Python Web Frameworks" in titles
assert "JavaScript Frameworks" not in titles
@pytest.mark.asyncio
async def test_chatgpt_fetch_document(mcp_server, app, test_project):
"""Test ChatGPT fetch tool for retrieving full document content."""
async with Client(mcp_server) as client:
# Create a test note
note_content = """# Advanced Python Techniques
## Overview
This document covers advanced Python programming techniques.
## Topics Covered
- Decorators
- Context Managers
- Metaclasses
- Async/Await patterns
## Code Examples
```python
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
```
"""
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "Advanced Python Techniques",
"folder": "programming",
"content": note_content,
"tags": "python,advanced,programming",
},
)
# Fetch the document using its title
fetch_result = await client.call_tool(
"fetch",
{
"id": "Advanced Python Techniques",
},
)
# Extract JSON content from MCP result
document_json = extract_mcp_json_content(fetch_result)
assert "id" in document_json
assert "title" in document_json
assert "text" in document_json
assert "url" in document_json
assert "metadata" in document_json
# Verify content
assert document_json["title"] == "Advanced Python Techniques"
assert "Decorators" in document_json["text"]
assert "Context Managers" in document_json["text"]
assert "def my_decorator" in document_json["text"]
@pytest.mark.asyncio
async def test_chatgpt_fetch_by_permalink(mcp_server, app, test_project):
"""Test ChatGPT fetch using permalink identifier."""
async with Client(mcp_server) as client:
# Create a note with known content
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "Test Document",
"folder": "test",
"content": "# Test Document\n\nThis is test content for permalink fetching.",
"tags": "test",
},
)
# First search to get the permalink
search_result = await client.call_tool(
"search",
{
"query": "Test Document",
},
)
results_json = extract_mcp_json_content(search_result)
assert len(results_json["results"]) > 0
permalink = results_json["results"][0]["id"]
# Fetch using the permalink
fetch_result = await client.call_tool(
"fetch",
{
"id": permalink,
},
)
# Verify the fetched document
document_json = extract_mcp_json_content(fetch_result)
assert document_json["id"] == permalink
assert "Test Document" in document_json["title"]
assert "test content for permalink fetching" in document_json["text"]
@pytest.mark.asyncio
async def test_chatgpt_fetch_nonexistent_document(mcp_server, app, test_project):
"""Test ChatGPT fetch with non-existent document ID."""
async with Client(mcp_server) as client:
# Try to fetch a non-existent document
fetch_result = await client.call_tool(
"fetch",
{
"id": "NonExistentDocument12345",
},
)
# Extract JSON content from MCP result
document_json = extract_mcp_json_content(fetch_result)
# Should have document structure even for errors
assert "id" in document_json
assert "title" in document_json
assert "text" in document_json
# Check for error indication
assert document_json["id"] == "NonExistentDocument12345"
assert "Not Found" in document_json["text"] or "not found" in document_json["text"]
@pytest.mark.asyncio
async def test_chatgpt_fetch_with_empty_title(mcp_server, app, test_project):
"""Test ChatGPT fetch handles documents with empty or missing titles."""
async with Client(mcp_server) as client:
# Create a note without a title in the content
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "untitled-note",
"folder": "misc",
"content": "This is content without a markdown header.\n\nJust plain text.",
"tags": "misc",
},
)
# Fetch the document
fetch_result = await client.call_tool(
"fetch",
{
"id": "untitled-note",
},
)
# Parse JSON response
document_json = extract_mcp_json_content(fetch_result)
# Should have a title even if content doesn't have one
assert "title" in document_json
assert document_json["title"] != ""
assert document_json["title"] is not None
assert "content without a markdown header" in document_json["text"]
@pytest.mark.asyncio
async def test_chatgpt_search_pagination_default(mcp_server, app, test_project):
"""Test that ChatGPT search uses reasonable pagination defaults."""
async with Client(mcp_server) as client:
# Create more than 10 notes to test pagination
for i in range(15):
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": f"Test Note {i}",
"folder": "bulk",
"content": f"# Test Note {i}\n\nThis is test content number {i}.",
"tags": "test,bulk",
},
)
# Search should return max 10 results by default
search_result = await client.call_tool(
"search",
{
"query": "Test Note",
},
)
results_json = extract_mcp_json_content(search_result)
# Should have at most 10 results (the default page_size)
assert len(results_json["results"]) <= 10
assert results_json["total_count"] <= 10
@pytest.mark.asyncio
async def test_chatgpt_tools_error_handling(mcp_server, app, test_project):
"""Test error handling in ChatGPT tools returns proper MCP format."""
async with Client(mcp_server) as client:
# Test search with invalid query (if validation exists)
# Using empty query to potentially trigger an error
search_result = await client.call_tool(
"search",
{
"query": "", # Empty query might cause an error
},
)
# Should still return MCP content array format
assert hasattr(search_result, 'content')
content_list = search_result.content
assert isinstance(content_list, list)
assert len(content_list) == 1
assert content_list[0].type == "text"
# Should be valid JSON even on error
results_json = extract_mcp_json_content(search_result)
assert "results" in results_json # Should have results key even if empty
@pytest.mark.asyncio
async def test_chatgpt_integration_workflow(mcp_server, app, test_project):
"""Test complete workflow: search then fetch, as ChatGPT would use it."""
async with Client(mcp_server) as client:
# Step 1: Create multiple documents
docs = [
{
"title": "API Design Best Practices",
"content": (
"# API Design Best Practices\n\n"
"RESTful API design principles and patterns."
),
"tags": "api,rest,design",
},
{
"title": "GraphQL vs REST",
"content": "# GraphQL vs REST\n\nComparing GraphQL and REST API architectures.",
"tags": "api,graphql,rest",
},
{
"title": "Database Design Patterns",
"content": (
"# Database Design Patterns\n\n"
"Common database design patterns and anti-patterns."
),
"tags": "database,design,patterns",
},
]
for doc in docs:
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": doc["title"],
"folder": "architecture",
"content": doc["content"],
"tags": doc["tags"],
},
)
# Step 2: Search for API-related content (as ChatGPT would)
search_result = await client.call_tool(
"search",
{
"query": "API",
},
)
results_json = extract_mcp_json_content(search_result)
assert len(results_json["results"]) >= 2
# Step 3: Fetch one of the search results (as ChatGPT would)
first_result_id = results_json["results"][0]["id"]
fetch_result = await client.call_tool(
"fetch",
{
"id": first_result_id,
},
)
document_json = extract_mcp_json_content(fetch_result)
# Verify the fetched document matches search result
assert document_json["id"] == first_result_id
assert "API" in document_json["text"] or "api" in document_json["text"].lower()
# Verify document has expected structure
assert document_json["metadata"]["format"] == "markdown"
+228
View File
@@ -0,0 +1,228 @@
"""Tests for ChatGPT-compatible MCP tools."""
import json
import pytest
from unittest.mock import AsyncMock, patch
from basic_memory.schemas.search import SearchResponse, SearchResult, SearchItemType
@pytest.mark.asyncio
async def test_search_successful_results():
"""Test search with successful results returns proper MCP content array format."""
# Mock successful search results
mock_results = SearchResponse(
results=[
SearchResult(
title="Test Document 1",
permalink="docs/test-doc-1",
content="This is test content for document 1",
type=SearchItemType.ENTITY,
score=1.0,
file_path="/test/docs/test-doc-1.md"
),
SearchResult(
title="Test Document 2",
permalink="docs/test-doc-2",
content="This is test content for document 2",
type=SearchItemType.ENTITY,
score=0.9,
file_path="/test/docs/test-doc-2.md"
)
],
current_page=1,
page_size=10
)
with patch(
'basic_memory.mcp.tools.chatgpt_tools.search_notes.fn',
new_callable=AsyncMock
) as mock_search:
mock_search.return_value = mock_results
# Import and call the actual function
from basic_memory.mcp.tools.chatgpt_tools import search
result = await search.fn("test query")
# Verify MCP content array format
assert isinstance(result, list)
assert len(result) == 1
assert result[0]["type"] == "text"
# Parse the JSON content
content = json.loads(result[0]["text"])
assert "results" in content
assert "query" in content
# Verify result structure
assert len(content["results"]) == 2
assert content["query"] == "test query"
# Verify individual result format
result_item = content["results"][0]
assert result_item["id"] == "docs/test-doc-1"
assert result_item["title"] == "Test Document 1"
assert result_item["url"] == "docs/test-doc-1"
@pytest.mark.asyncio
async def test_search_with_error_response():
"""Test search when underlying search_notes returns error string."""
error_message = "# Search Failed - Invalid Syntax\n\nThe search query contains errors..."
with patch(
'basic_memory.mcp.tools.chatgpt_tools.search_notes.fn',
new_callable=AsyncMock
) as mock_search:
mock_search.return_value = error_message
from basic_memory.mcp.tools.chatgpt_tools import search
result = await search.fn("invalid query")
# Verify MCP content array format
assert isinstance(result, list)
assert len(result) == 1
assert result[0]["type"] == "text"
# Parse the JSON content
content = json.loads(result[0]["text"])
assert content["results"] == []
assert content["error"] == "Search failed"
assert "error_details" in content
@pytest.mark.asyncio
async def test_fetch_successful_document():
"""Test fetch with successful document retrieval."""
document_content = """# Test Document
This is the content of a test document.
## Section 1
Some content here.
## Observations
- [observation] This is a test observation
## Relations
- relates_to [[Another Document]]
"""
with patch(
'basic_memory.mcp.tools.chatgpt_tools.read_note.fn',
new_callable=AsyncMock
) as mock_read:
mock_read.return_value = document_content
from basic_memory.mcp.tools.chatgpt_tools import fetch
result = await fetch.fn("docs/test-document")
# Verify MCP content array format
assert isinstance(result, list)
assert len(result) == 1
assert result[0]["type"] == "text"
# Parse the JSON content
content = json.loads(result[0]["text"])
assert content["id"] == "docs/test-document"
assert content["title"] == "Test Document" # Extracted from markdown
assert content["text"] == document_content
assert content["url"] == "docs/test-document"
assert content["metadata"]["format"] == "markdown"
@pytest.mark.asyncio
async def test_fetch_document_not_found():
"""Test fetch when document is not found."""
error_content = """# Note Not Found: "nonexistent-doc"
I couldn't find any notes matching "nonexistent-doc". Here are some suggestions:
## Check Identifier Type
- If you provided a title, try using the exact permalink instead
"""
with patch(
'basic_memory.mcp.tools.chatgpt_tools.read_note.fn',
new_callable=AsyncMock
) as mock_read:
mock_read.return_value = error_content
from basic_memory.mcp.tools.chatgpt_tools import fetch
result = await fetch.fn("nonexistent-doc")
# Verify MCP content array format
assert isinstance(result, list)
assert len(result) == 1
assert result[0]["type"] == "text"
# Parse the JSON content
content = json.loads(result[0]["text"])
assert content["id"] == "nonexistent-doc"
assert content["text"] == error_content
assert content["metadata"]["error"] == "Document not found"
def test_format_search_results_for_chatgpt():
"""Test search results formatting."""
from basic_memory.mcp.tools.chatgpt_tools import _format_search_results_for_chatgpt
mock_results = SearchResponse(
results=[
SearchResult(
title="Document One",
permalink="docs/doc-one",
content="Content for document one",
type=SearchItemType.ENTITY,
score=1.0,
file_path="/test/docs/doc-one.md"
),
SearchResult(
title="", # Test empty title handling
permalink="docs/untitled",
content="Content without title",
type=SearchItemType.ENTITY,
score=0.8,
file_path="/test/docs/untitled.md"
)
],
current_page=1,
page_size=10
)
formatted = _format_search_results_for_chatgpt(mock_results)
assert len(formatted) == 2
assert formatted[0]["id"] == "docs/doc-one"
assert formatted[0]["title"] == "Document One"
assert formatted[0]["url"] == "docs/doc-one"
# Test empty title handling
assert formatted[1]["title"] == "Untitled"
def test_format_document_for_chatgpt():
"""Test document formatting."""
from basic_memory.mcp.tools.chatgpt_tools import _format_document_for_chatgpt
content = "# Test Document\n\nThis is test content."
result = _format_document_for_chatgpt(content, "docs/test")
assert result["id"] == "docs/test"
assert result["title"] == "Test Document"
assert result["text"] == content
assert result["url"] == "docs/test"
assert result["metadata"]["format"] == "markdown"
def test_format_document_error_handling():
"""Test document formatting with error content."""
from basic_memory.mcp.tools.chatgpt_tools import _format_document_for_chatgpt
error_content = "# Note Not Found: \"missing-doc\"\n\nDocument not found."
result = _format_document_for_chatgpt(error_content, "missing-doc", "Missing Doc")
assert result["id"] == "missing-doc"
assert result["title"] == "Missing Doc"
assert result["text"] == error_content
assert result["metadata"]["error"] == "Document not found"