Skip to content

Commit c56ab57

Browse files
committed
Add search tool module
1 parent 8e83e12 commit c56ab57

File tree

1 file changed

+219
-0
lines changed

1 file changed

+219
-0
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
MCP Search Tool Module
3+
4+
Defines search tools that are registered with the MCP server for advanced query processing
5+
and file attachment handling.
6+
"""
7+
8+
import textwrap
9+
from pathlib import Path
10+
from typing import Literal
11+
12+
from mcp.server.fastmcp import FastMCP
13+
from pydantic import BaseModel, Field
14+
15+
from .api_client import call_provider
16+
from .config import PROVIDER_CONFIG
17+
from .types import APIKeyError, ModelConfig, ProviderType, QueryType
18+
19+
20+
class Message(BaseModel):
21+
"""Message model for chat interactions."""
22+
23+
role: str = Field(description="The role of the message sender (user/assistant)")
24+
content: str = Field(description="The content of the message")
25+
26+
27+
# Create MCP server instance
28+
mcp: FastMCP = FastMCP(
29+
"perplexity-advanced",
30+
log_level="WARNING",
31+
dependencies=["httpx"],
32+
)
33+
34+
35+
def process_attachments(attachment_paths: list[str]) -> str:
36+
"""
37+
Processes file attachments and formats them into an XML string.
38+
39+
Reads the contents of each file and wraps them in XML tags with the following structure:
40+
<files>
41+
<file path="/absolute/path/to/file1">
42+
[file1 contents]
43+
</file>
44+
<file path="/absolute/path/to/file2">
45+
[file2 contents]
46+
</file>
47+
</files>
48+
49+
Args:
50+
attachment_paths: List of absolute file paths to process
51+
52+
Returns:
53+
str: XML-formatted string containing file contents
54+
55+
Raises:
56+
ValueError: If a file is not found, is invalid, or cannot be read
57+
"""
58+
if not attachment_paths:
59+
return ""
60+
61+
result = ["<files>"]
62+
63+
# Process each file
64+
for file_path in attachment_paths:
65+
try:
66+
abs_path = Path(file_path).resolve(strict=True)
67+
if not abs_path.is_file():
68+
raise ValueError(f"'{abs_path}' is not a valid file")
69+
70+
# Read file content
71+
with abs_path.open(encoding="utf-8") as f:
72+
file_content = f.read()
73+
74+
# Add file content with proper indentation
75+
result.append(f'\t<file path="{abs_path}">')
76+
# Indent each line of the content
77+
content_lines = file_content.splitlines()
78+
result.extend(f"\t\t{line}" for line in content_lines)
79+
result.append("\t</file>")
80+
81+
except FileNotFoundError:
82+
raise ValueError(f"File not found: {file_path}")
83+
except Exception as e:
84+
raise ValueError(f"Error processing file '{file_path}': {e}") from e
85+
86+
result.append("</files>")
87+
formatted_xml = "\n".join(result)
88+
return formatted_xml
89+
90+
91+
@mcp.tool(
92+
name="ask_perplexity",
93+
description=(
94+
"""Perplexity is fundamentally an LLM that can search the internet, gather information, and answer users’ queries.
95+
96+
For example, let's suppose we want to find out the latest version of Python.
97+
1. You would search on Google.
98+
2. Then read the top two or three results directly to verify.
99+
100+
Perplexity does that work for you.
101+
102+
To answer a user's query, Perplexity searches, opens the top search results, finds information on those websites, and then provides the answer.
103+
104+
Perplexity can be used with two types of queries: simple and complex. Choosing the right query type to fulfill the user’s request is most important.
105+
106+
SIMPLE Query:
107+
- Cheap and fast (on average, 10x cheaper and 3x faster than complex queries).
108+
- Suitable for straightforward questions such as "What is the latest version of Python?"
109+
- Pricing: $1/M input tokens, $1/M output tokens.
110+
111+
COMPLEX Query:
112+
- Slower and more expensive (on average, 10x more expensive and 3x slower).
113+
- Suitable for tasks requiring multiple steps of reasoning or deep analysis, such as "Analyze the attached code to examine the current status of a specific library and create a migration plan."
114+
- Pricing: $1/M input tokens, $5/M output tokens.
115+
116+
Instructions:
117+
- When reviewing the user’s request, if you find anything unexpected, uncertain, or questionable, do not hesitate to use the "ask_perplexity" tool to consult Perplexity.
118+
- Since Perplexity is also an LLM, prompt engineering techniques are paramount.
119+
- Remember the basics of prompt engineering, such as providing clear instructions, sufficient context, and examples.
120+
- Include as much context and relevant files as possible to smoothly fulfill the user’s request.
121+
122+
Note: All queries must be in English for optimal results.
123+
"""
124+
),
125+
)
126+
async def ask_perplexity(
127+
query: str = Field(description="The query to search for"),
128+
query_type: Literal["simple", "complex"] = Field(description="Type of query to determine model selection"),
129+
attachment_paths: list[str] = Field(
130+
description="An optional list of absolute file paths to attach as context for the search query",
131+
),
132+
) -> str:
133+
"""
134+
Performs an advanced search using the appropriate API provider and model.
135+
136+
This function processes any attached files by reading their contents and formatting them
137+
into XML before appending them to the original query. The combined query is then sent
138+
to either OpenRouter or Perplexity API based on the available configuration.
139+
140+
Args:
141+
query: The search query text
142+
query_type: Query complexity type ('simple' or 'complex')
143+
attachment_paths: Optional list of files to include as context
144+
145+
Returns:
146+
str: XML-formatted result containing reasoning (if available) and answer
147+
148+
Raises:
149+
ValueError: If the query is empty or attachments cannot be processed
150+
APIKeyError: If no API provider is configured
151+
"""
152+
if not query:
153+
raise ValueError("Query must not be empty")
154+
155+
# Process any file attachments and get the XML string
156+
attachments_xml = ""
157+
if attachment_paths:
158+
attachments_xml = process_attachments(attachment_paths)
159+
160+
# Combine the original query with the attachment contents
161+
if query_type == "complex":
162+
query = textwrap.dedent(
163+
f"""
164+
<system>
165+
Think or reason about the user's words in as much detail as possible. Summarize everything thoroughly.
166+
List all the elements that need to be considered regarding the user’s question or prompt.
167+
168+
<idea-reflection>
169+
Form your opinions about these elements from an objective standpoint, avoiding an overly pessimistic or overly optimistic view. Opinions should be specific and realistic.
170+
Then logically verify these opinions once more. If they hold up, proceed to the next thought; if not, re-examine them.
171+
</idea-reflection>
172+
173+
By carrying out this reflection process, you can accumulate opinions that have been logically reviewed and verified.
174+
Finally, combine these logically validated pieces of reasoning to form your answer. By doing this way, provide responses that are verifiable and logically sound.
175+
</system>
176+
177+
<user-request>
178+
{query}
179+
</user-request>
180+
"""[1:]
181+
)
182+
query_with_attachments = query + attachments_xml
183+
184+
# Retrieve available providers
185+
available_providers: list[ProviderType] = [p for p, cfg in PROVIDER_CONFIG.items() if cfg["key"]]
186+
if not available_providers:
187+
raise APIKeyError("No API key available")
188+
provider: ProviderType = available_providers[0]
189+
config: ModelConfig = PROVIDER_CONFIG[provider]
190+
191+
# Map query_type from string to QueryType enum
192+
match query_type:
193+
case "simple":
194+
query_type_enum = QueryType.SIMPLE
195+
case "complex":
196+
query_type_enum = QueryType.COMPLEX
197+
case _:
198+
raise ValueError(f"Invalid query type: {query_type}")
199+
200+
model: str = config["models"][query_type_enum]
201+
include_reasoning = provider == "openrouter" and query_type_enum == QueryType.COMPLEX
202+
203+
# Call the provider with the combined query
204+
response = await call_provider(
205+
provider, model, [{"role": "user", "content": query_with_attachments}], include_reasoning
206+
)
207+
# Format the result as raw text with XML-like tags
208+
result = ""
209+
210+
# Add reasoning if available
211+
reasoning = response.get("reasoning")
212+
if reasoning is not None:
213+
result += f"<think>\n{reasoning}\n</think>\n\n"
214+
215+
# Add answer
216+
content = response.get("content", "")
217+
result += f"<answer>\n{content}\n</answer>"
218+
219+
return result

0 commit comments

Comments
 (0)