Skip to content

Commit

Permalink
adds mcp-server-vscode to allow assistants to access problems from VS…
Browse files Browse the repository at this point in the history
…Code, improved support for reasoning models (#315)
  • Loading branch information
bkrabach authored Feb 6, 2025
1 parent dce0c55 commit 58eb20b
Show file tree
Hide file tree
Showing 48 changed files with 11,123 additions and 233 deletions.
1 change: 1 addition & 0 deletions assistants/codespace-assistant/assistant/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ async def on_message_created(

try:
await respond_to_conversation(
message=message,
attachments_extension=attachments_extension,
context=context,
config=config,
Expand Down
13 changes: 11 additions & 2 deletions assistants/codespace-assistant/assistant/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ class ExtensionsConfigModel(BaseModel):
] = AttachmentsConfigModel()


# the workbench app builds dynamic forms based on the configuration model and UI schema
class AssistantConfigModel(BaseModel):
class PromptsConfigModel(BaseModel):
instruction_prompt: Annotated[
str,
Field(
Expand Down Expand Up @@ -117,6 +116,16 @@ class AssistantConfigModel(BaseModel):
UISchema(widget="textarea", enable_markdown_in_description=True),
] = helpers.load_text_include("guardrails_prompt.txt")


# the workbench app builds dynamic forms based on the configuration model and UI schema
class AssistantConfigModel(BaseModel):
prompts: Annotated[
PromptsConfigModel,
Field(
title="Prompts Configuration",
),
] = PromptsConfigModel()

welcome_message: Annotated[
str,
Field(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@


def get_mcp_server_configs(tools_config: ToolsConfigModel) -> List[MCPServerConfig]:
file_system_paths = [f"/workspaces/{file_system_path}" for file_system_path in tools_config.file_system_paths]
return [
MCPServerConfig(
key="filesystem",
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", *file_system_paths],
args=["-y", "@modelcontextprotocol/server-filesystem", *tools_config.file_system_paths],
),
MCPServerConfig(
key="memory",
Expand Down Expand Up @@ -45,6 +44,13 @@ def get_mcp_server_configs(tools_config: ToolsConfigModel) -> List[MCPServerConf
b) Store facts about them as observations
"""),
),
MCPServerConfig(
key="vscode",
command="http://127.0.0.1:6010/sse",
args=[
*tools_config.file_system_paths,
],
),
MCPServerConfig(
key="giphy",
command="http://127.0.0.1:6000/sse",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async def connect_to_mcp_server_sse(server_config: MCPServerConfig) -> AsyncIter

def is_mcp_server_enabled(server_config: MCPServerConfig, tools_config: ToolsConfigModel) -> bool:
"""Check if an MCP server is enabled."""
return tools_config.tools_enabled.model_dump().get(f"{server_config.key}_enabled", False)
return tools_config.tool_servers_enabled.model_dump().get(f"{server_config.key}_enabled", False)


async def establish_mcp_sessions(tools_config: ToolsConfigModel, stack: AsyncExitStack) -> List[MCPSession]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@
from mcp import Tool
from mcp.types import EmbeddedResource, ImageContent, TextContent

from .__model import MCPSession, ToolCall, ToolCallResult, ToolMessageType
from .__model import MCPSession, ToolCall, ToolCallResult, ToolMessageType, ToolsConfigModel

logger = logging.getLogger(__name__)


def retrieve_tools_from_sessions(mcp_sessions: List[MCPSession]) -> List[Tool]:
def retrieve_tools_from_sessions(mcp_sessions: List[MCPSession], tools_config: ToolsConfigModel) -> List[Tool]:
"""
Retrieve tools from all MCP sessions.
Retrieve tools from all MCP sessions, excluding any tools that are disabled in the tools config.
"""
return [tool for mcp_session in mcp_sessions for tool in mcp_session.tools]
return [
tool
for mcp_session in mcp_sessions
for tool in mcp_session.tools
if tool.name not in tools_config.tools_disabled
]


async def handle_tool_call(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ class MCPServersEnabledConfigModel(BaseModel):
),
] = True

vscode_enabled: Annotated[
bool,
Field(
title="VSCode Enabled",
description=dedent("""
Enable VSCode tools, supporting testing and evaluation of code via VSCode integration.
To use this tool, the project must be running in VSCode (tested in Codespaces, but may work
locally), and the `mcp-server-vscode` VSCode extension must be running.
""").strip(),
),
] = False

sequential_thinking_enabled: Annotated[
bool,
Field(
Expand All @@ -98,7 +110,10 @@ class MCPServersEnabledConfigModel(BaseModel):
bool,
Field(
title="Giphy Enabled",
description="Enable Giphy tools for searching and retrieving GIFs. Must start the Giphy server.",
description=dedent("""
Enable Giphy tools for searching and retrieving GIFs. Must start the Giphy server via the
VSCode Run and Debug panel.
""").strip(),
),
] = False

Expand Down Expand Up @@ -142,15 +157,12 @@ class ToolsConfigModel(BaseModel):
UISchema(widget="textarea", enable_markdown_in_description=True),
] = dedent("""
- Use the available tools to assist with specific tasks.
- Before performing any file operations, use the `allowed_directories` tool to get a list of directories
that are allowed for file operations.
- Before performing any file operations, use the `list_allowed_directories` tool to get a list of directories
that are allowed for file operations. Always use paths relative to an allowed directory.
- When searching or browsing for files, consider the kinds of folders and files that should be avoided:
- For example, for coding projects exclude folders like `.git`, `.vscode`, `node_modules`, and `dist`.
- For each turn, always re-read a file before using it to ensure the most up-to-date information, especially
when writing or editing files.
- Either use search or specific list files in directories instead of using `directory_tree` to avoid
issues with large directory trees as that tool is not optimized for large trees nor does it allow
for filtering.
- The search tool does not appear to support wildcards, but does work with partial file names.
""").strip()

Expand Down Expand Up @@ -184,10 +196,21 @@ class ToolsConfigModel(BaseModel):
list[str],
Field(
title="File System Paths",
description="Paths to the file system for tools to use, relative to `/workspaces/` in the container.",
description="Paths to the file system for tools to use in the container or local.",
),
] = ["semanticworkbench"]
] = ["/workspaces/semanticworkbench"]

tools_enabled: Annotated[MCPServersEnabledConfigModel, Field(title="Tools Enabled")] = (
tool_servers_enabled: Annotated[MCPServersEnabledConfigModel, Field(title="Tool Servers Enabled")] = (
MCPServersEnabledConfigModel()
)

tools_disabled: Annotated[
list[str],
Field(
title="Disabled Tools",
description=dedent("""
List of individual tools to disable. Use this if there is a problem tool that you do not want
made visible to your assistant.
""").strip(),
),
] = ["directory_tree"]
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from typing import List

import deepmerge
import openai_client
from openai.types.chat import (
ChatCompletion,
ChatCompletionToolMessageParam,
ParsedChatCompletion,
)
from openai_client import OpenAIRequestConfig, num_tokens_from_messages
from semantic_workbench_api_model.workbench_model import (
MessageType,
NewConversationMessage,
Expand All @@ -19,7 +19,6 @@

from assistant.extensions.tools.__model import MCPSession

from ..config import AssistantConfigModel
from ..extensions.tools import (
ToolCall,
handle_tool_call,
Expand All @@ -39,7 +38,7 @@ async def handle_completion(
completion: ParsedChatCompletion | ChatCompletion,
mcp_sessions: List[MCPSession],
context: ConversationContext,
config: AssistantConfigModel,
request_config: OpenAIRequestConfig,
silence_token: str,
metadata_key: str,
response_start_time: float,
Expand All @@ -57,10 +56,10 @@ async def handle_error(error_message: str) -> StepResult:
return step_result

# Get service and request configuration for generative model
generative_request_config = config.generative_ai_client_config.request_config
generative_request_config = request_config

# get the total tokens used for the completion
completion_total_tokens = completion.usage.total_tokens if completion.usage else 0
total_tokens = completion.usage.total_tokens if completion.usage else 0

response_content: list[str] = []

Expand Down Expand Up @@ -113,8 +112,17 @@ async def handle_error(error_message: str) -> StepResult:
footer_items = []

# Add the token usage message to the footer items
if completion_total_tokens > 0:
footer_items.append(get_token_usage_message(generative_request_config.max_tokens, completion_total_tokens))
if total_tokens > 0:
completion_tokens = completion.usage.completion_tokens if completion.usage else 0
request_tokens = total_tokens - completion_tokens
footer_items.append(
get_token_usage_message(
max_tokens=generative_request_config.max_tokens,
total_tokens=total_tokens,
request_tokens=request_tokens,
completion_tokens=completion_tokens,
)
)

# Track the end time of the response generation and calculate duration
response_end_time = time.time()
Expand All @@ -132,7 +140,7 @@ async def handle_error(error_message: str) -> StepResult:
)

# Set the conversation tokens for the turn result
step_result.conversation_tokens = completion_total_tokens
step_result.conversation_tokens = total_tokens

# strip out the username from the response
if content.startswith("["):
Expand Down Expand Up @@ -175,12 +183,16 @@ async def handle_error(error_message: str) -> StepResult:
# Update content and metadata with tool call result metadata
deepmerge.always_merger.merge(step_result.metadata, tool_call_result.metadata)

content = (
tool_call_result.content if len(tool_call_result.content) > 0 else "[tool call returned no content]"
)

# Add the token count for the tool call result to the total token count
step_result.conversation_tokens += openai_client.num_tokens_from_messages(
step_result.conversation_tokens += num_tokens_from_messages(
messages=[
ChatCompletionToolMessageParam(
role="tool",
content=tool_call_result.content,
content=content,
tool_call_id=tool_call.id,
)
],
Expand All @@ -192,15 +204,15 @@ async def handle_error(error_message: str) -> StepResult:
step_result.metadata,
{
"tool_result": {
"content": tool_call_result.content,
"content": content,
"tool_call_id": tool_call.id,
},
},
)

await context.send_messages(
NewConversationMessage(
content=tool_call_result.content,
content=content,
message_type=MessageType.note,
metadata=step_result.metadata,
)
Expand Down
Loading

0 comments on commit 58eb20b

Please sign in to comment.