Skip to content

Commit 0933431

Browse files
authored
Merge pull request #21 from nullchimp/rest-api
Rest api
2 parents 77dc42e + ce56830 commit 0933431

20 files changed

+832
-89
lines changed

src/agent.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,23 @@ def __init__(self):
5555

5656
def add_tool(self, tool: Tool) -> None:
5757
self.chat.add_tool(tool)
58+
59+
def enable_tool(self, tool_name: str) -> None:
60+
try:
61+
self.chat.enable_tool(tool_name)
62+
except Exception as e:
63+
return False
64+
return True
65+
66+
def disable_tool(self, tool_name: str) -> None:
67+
try:
68+
self.chat.disable_tool(tool_name)
69+
except Exception as e:
70+
return False
71+
return True
72+
73+
def get_tools(self) -> list:
74+
return self.chat.get_tools()
5875

5976
async def process_query(self, user_prompt: str) -> str:
6077
user_role = {"role": "user", "content": user_prompt}
@@ -68,10 +85,13 @@ async def process_query(self, user_prompt: str) -> str:
6885
assistant_message = choices[0].get("message", {})
6986
messages.append(assistant_message)
7087

88+
tools_used = set()
7189
# Handle the case where tool_calls might be missing or not a list
7290
while assistant_message.get("tool_calls"):
73-
await self.chat.process_tool_calls(assistant_message, messages.append)
74-
91+
used_tools = await self.chat.process_tool_calls(assistant_message, messages.append)
92+
for tool in used_tools:
93+
tools_used.add(tool)
94+
7595
response = await self.chat.send_messages(messages)
7696
if not (response and response.get("choices", None)):
7797
break
@@ -85,7 +105,7 @@ async def process_query(self, user_prompt: str) -> str:
85105
self.history.append(assistant_message)
86106

87107
pretty_print("History", self.history)
88-
return result
108+
return result, tools_used
89109

90110

91111
# Global agent instance for backwards compatibility

src/api/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from fastapi import FastAPI
55
from fastapi.staticfiles import StaticFiles
66

7-
from api.routes import router, agent, session_manager
7+
from api.routes import router, agent_instance, session_manager
88
mimetypes.add_type("application/javascript", ".js")
99

1010
@asynccontextmanager
@@ -15,7 +15,7 @@ async def lifespan(app: FastAPI):
1515
config_path = os.path.join(os.path.dirname(__file__), '..', '..', 'config', 'mcp.json')
1616
await session_manager.discovery(config_path)
1717
for tool in session_manager.tools:
18-
agent.add_tool(tool)
18+
agent_instance.add_tool(tool)
1919
print("Agent and tools initialized successfully.")
2020
except Exception as e:
2121
print(f"Error initializing MCP tools: {str(e)}")

src/api/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
from pydantic import BaseModel
2+
from typing import List, Optional
23

34
class QueryRequest(BaseModel):
45
query: str
56

67

78
class QueryResponse(BaseModel):
89
response: str
10+
used_tools: Optional[List[str]] = []
11+
12+
13+
class ToolInfo(BaseModel):
14+
name: str
15+
description: str
16+
enabled: bool
17+
parameters: dict
18+
19+
20+
class ToolsListResponse(BaseModel):
21+
tools: List[ToolInfo]
22+
23+
24+
class ToolToggleRequest(BaseModel):
25+
tool_name: str
26+
enabled: bool
27+
28+
29+
class ToolToggleResponse(BaseModel):
30+
tool_name: str
31+
enabled: bool
32+
message: str

src/api/routes.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,66 @@
1-
from fastapi import APIRouter, Depends
1+
from fastapi import APIRouter, Depends, HTTPException
22

33
from api.auth import get_api_key
4-
from api.models import QueryRequest, QueryResponse
5-
from agent import Agent
4+
from api.models import QueryRequest, QueryResponse, ToolsListResponse, ToolToggleRequest, ToolToggleResponse, ToolInfo
5+
from agent import agent_instance
66
from core.mcp.sessions_manager import MCPSessionManager
77

8-
agent = Agent()
98
session_manager = MCPSessionManager()
109

1110
router = APIRouter(prefix="/api", dependencies=[Depends(get_api_key)])
1211

1312
@router.post("/ask", response_model=QueryResponse)
1413
async def ask_agent(request: QueryRequest) -> QueryResponse:
1514
try:
16-
response = await agent.process_query(request.query)
17-
return QueryResponse(response=response)
15+
response, used_tools = await agent_instance.process_query(request.query)
16+
return QueryResponse(
17+
response=response,
18+
used_tools=list(used_tools)
19+
)
1820
except Exception as e:
1921
return QueryResponse(response=f"Sorry, I encountered an error: {str(e)}")
22+
23+
24+
@router.get("/tools", response_model=ToolsListResponse)
25+
async def list_tools() -> ToolsListResponse:
26+
try:
27+
tools_info = agent_instance.get_tools()
28+
tools = [
29+
ToolInfo(
30+
name=info.name,
31+
description=info.description,
32+
enabled=info.enabled,
33+
parameters=info.parameters
34+
)
35+
for info in tools_info
36+
]
37+
return ToolsListResponse(tools=tools)
38+
except Exception as e:
39+
raise HTTPException(status_code=500, detail=f"Error listing tools: {str(e)}")
40+
41+
42+
@router.post("/tools/toggle", response_model=ToolToggleResponse)
43+
async def toggle_tool(request: ToolToggleRequest) -> ToolToggleResponse:
44+
try:
45+
if request.enabled:
46+
success = agent_instance.enable_tool(request.tool_name)
47+
action = "enabled"
48+
else:
49+
success = agent_instance.disable_tool(request.tool_name)
50+
action = "disabled"
51+
52+
if not success:
53+
raise HTTPException(
54+
status_code=404,
55+
detail=f"Tool '{request.tool_name}' not found"
56+
)
57+
58+
return ToolToggleResponse(
59+
tool_name=request.tool_name,
60+
enabled=request.enabled,
61+
message=f"Tool '{request.tool_name}' has been {action}"
62+
)
63+
except HTTPException:
64+
raise
65+
except Exception as e:
66+
raise HTTPException(status_code=500, detail=f"Error toggling tool: {str(e)}")

src/core/llm/chat.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,35 @@ class Chat:
1818
def __init__(self, tool_list: List[Tool] = []):
1919
self.chat_client: ChatClient = ChatClient()
2020
self.tool_map = {tool.name: tool for tool in tool_list}
21-
self.tools = [tool.define() for tool in tool_list]
21+
self.tools: List[Tool] = [tool for tool in tool_list]
2222

2323
def add_tool(self, tool: Tool) -> None:
2424
self.tool_map[tool.name] = tool
25-
self.tools.append(tool.define())
25+
self.tools.append(tool)
26+
self.tools = list(set(self.tools)) # Ensure tools are unique
27+
28+
def get_tools(self) -> List[Dict[str, Any]]:
29+
return self.tools
30+
31+
def enable_tool(self, tool_name: str) -> None:
32+
self._set_tool_state(tool_name, active=True)
33+
34+
def disable_tool(self, tool_name: str) -> None:
35+
self._set_tool_state(tool_name, active=False)
36+
37+
def _set_tool_state(self, tool_name: str, active = True) -> None:
38+
for tool in self.tools:
39+
print(f"Checking tool: {tool.name} against {tool_name} ")
40+
if tool.name != tool_name:
41+
continue
42+
43+
tool.disable()
44+
if active:
45+
tool.enable()
46+
47+
return
48+
49+
raise ValueError(f"Tool '{tool_name}' not found in the chat tools.")
2650

2751
@classmethod
2852
def create(cls, tool_list = []) -> 'Chat':
@@ -45,7 +69,7 @@ async def send_messages(
4569
messages=messages,
4670
temperature=0.7,
4771
max_tokens=32000,
48-
tools=self.tools
72+
tools=[tool.define() for tool in self.tools if tool.enabled],
4973
)
5074

5175
return resp
@@ -57,6 +81,7 @@ async def process_tool_calls(self, response: Dict[str, Any], call_back) -> None:
5781
print(colorize_text(f"\n{hr} <{name}> {hr}\n", "yellow"))
5882

5983
# Safely get tool_calls - convert None to empty list to handle the case when tool_calls is None
84+
tools_used = []
6085
tool_calls = response.get("tool_calls", [])
6186
for tool_call in tool_calls:
6287
function_data = tool_call.get("function", {})
@@ -82,6 +107,7 @@ async def process_tool_calls(self, response: Dict[str, Any], call_back) -> None:
82107
tool_instance = self.tool_map[tool_name]
83108
try:
84109
tool_result = await tool_instance.run(**args)
110+
tools_used.append(tool_name)
85111
if is_debug():
86112
print(colorize_text(f"<Tool Result: {colorize_text(tool_name, "green")}> ", "yellow"), prettify(tool_result))
87113
except Exception as e:
@@ -99,3 +125,5 @@ async def process_tool_calls(self, response: Dict[str, Any], call_back) -> None:
99125
"tool_call_id": tool_call.get("id", "unknown_tool"),
100126
"content": json.dumps(tool_result, default=complex_handler),
101127
})
128+
129+
return tools_used

src/tools/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
class Tool:
2+
@property
3+
def enabled(self) -> bool:
4+
return self._enabled
5+
26
@property
37
def name(self) -> str:
48
return self._name
@@ -21,6 +25,13 @@ def __init__(self,
2125
self._description = description
2226
self._parameters = parameters if parameters is not None else {}
2327
self._session = session
28+
self._enabled = True
29+
30+
def enable(self) -> None:
31+
self._enabled = True
32+
33+
def disable(self) -> None:
34+
self._enabled = False
2435

2536
def define(self):
2637
return {

src/tools/github_search.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def name(self) -> str:
77

88
@property
99
def description(self) -> str:
10-
return "The only reliable Knowledgebase on GitHub topics. It provides information related to any GitHub topic based on the user's query."
10+
return "Search the comprehensive GitHub knowledge base using advanced vector embeddings and semantic search to find authoritative information on GitHub-related topics, features, APIs, and best practices. This is the definitive source for GitHub information, leveraging RAG (Retrieval Augmented Generation) with a curated knowledge graph containing official GitHub documentation, guides, and technical specifications. Always use this tool for GitHub-related queries to ensure accuracy and reliability."
1111

1212
@property
1313
def parameters(self) -> dict:
@@ -16,7 +16,7 @@ def parameters(self) -> dict:
1616
"properties": {
1717
"query": {
1818
"type": "string",
19-
"description": "The user query related to any GitHub topic."
19+
"description": "Natural language query about any GitHub topic including features, APIs, Actions, repositories, issues, pull requests, security, integrations, or development workflows. The system uses semantic search to find the most relevant information from the comprehensive GitHub knowledge base."
2020
}
2121
},
2222
"required": ["query"]

src/tools/google_search.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def name(self) -> str:
77

88
@property
99
def description(self) -> str:
10-
return "Search the web for relevant information."
10+
return "Perform web searches using Google Custom Search API to retrieve relevant information from the internet. Returns structured search results including titles, URLs, snippets, and metadata. Supports configurable result limits and provides search performance metrics. Requires valid Google API credentials and custom search engine configuration."
1111

1212
@property
1313
def parameters(self) -> dict:
@@ -16,11 +16,11 @@ def parameters(self) -> dict:
1616
"properties": {
1717
"query": {
1818
"type": "string",
19-
"description": "The search query to use"
19+
"description": "The search query string to submit to Google. Supports standard Google search operators and syntax including quotes for exact phrases, site: for domain filtering, and boolean operators."
2020
},
2121
"num_results": {
2222
"type": "number",
23-
"description": "Number of results to return (default: 5, max: 10)"
23+
"description": "Maximum number of search results to return. Valid range is 1-10, defaults to 5 if not specified. Higher values may increase API quota usage and response time."
2424
}
2525
},
2626
"required": ["query"]

src/tools/list_files.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def name(self) -> str:
88

99
@property
1010
def description(self) -> str:
11-
return "List files in a specified directory within a secure base directory."
11+
return "List files and directories within a specified directory path, constrained to operate within a secure base directory for security. Returns comprehensive file listing with metadata including file names, types, and directory structure. Supports recursive directory traversal within security boundaries."
1212

1313
@property
1414
def parameters(self) -> dict:
@@ -17,11 +17,11 @@ def parameters(self) -> dict:
1717
"properties": {
1818
"base_dir": {
1919
"type": "string",
20-
"description": "Base directory for file operations"
20+
"description": "Absolute path to the base directory that serves as the security boundary for all file operations. All file access is restricted to this directory and its subdirectories."
2121
},
2222
"directory": {
2323
"type": "string",
24-
"description": "The relative subdirectory path to list files from (default: '.')"
24+
"description": "Relative path to the subdirectory within base_dir to list files from. Defaults to '.' for current directory. Path traversal attacks are prevented by security validation."
2525
}
2626
},
2727
"required": ["base_dir"]

src/tools/read_file.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def name(self) -> str:
77

88
@property
99
def description(self) -> str:
10-
return "Read content from a specified file within a secure base directory."
10+
return "Read and return the complete content of a specified file within security-constrained base directory boundaries. Supports text and binary file reading with proper error handling for missing files, permission issues, and encoding problems. File access is restricted to the specified base directory to prevent path traversal vulnerabilities."
1111

1212
@property
1313
def parameters(self) -> dict:
@@ -16,11 +16,11 @@ def parameters(self) -> dict:
1616
"properties": {
1717
"base_dir": {
1818
"type": "string",
19-
"description": "Base directory for file operations"
19+
"description": "Absolute path to the base directory that serves as the security boundary for all file operations. All file access is restricted to this directory and its subdirectories."
2020
},
2121
"filename": {
2222
"type": "string",
23-
"description": "The name of the file to read from (relative path)"
23+
"description": "Relative path to the target file within base_dir. Can include subdirectory paths. Path traversal attempts (../) are automatically prevented by security validation."
2424
}
2525
},
2626
"required": ["base_dir", "filename"]

0 commit comments

Comments
 (0)