|
14 | 14 | from starlette.requests import Request |
15 | 15 | from starlette.datastructures import Headers |
16 | 16 |
|
| 17 | +from ..tools import create_mcp_server |
| 18 | + |
17 | 19 | logger = logging.getLogger(__name__) |
18 | 20 |
|
19 | 21 |
|
@@ -80,81 +82,67 @@ async def post(self): |
80 | 82 | self.finish(json.dumps({"error": "Chat agent not initialized"})) |
81 | 83 | return |
82 | 84 |
|
83 | | - # Create request adapter (Starlette-compatible) |
84 | | - tornado_request = TornadoRequestAdapter(self) |
85 | | - |
86 | | - # Parse request body to extract model if specified |
87 | | - try: |
88 | | - body = await tornado_request.json() |
89 | | - model = body.get('model') if isinstance(body, dict) else None |
90 | | - except: |
91 | | - model = None |
92 | | - |
93 | | - # Get builtin tools (empty list - tools metadata is only for UI display) |
94 | | - # The actual pydantic-ai tools are registered in the agent itself |
95 | | - builtin_tools = [] |
96 | | - |
97 | | - # Create usage limits for the agent |
98 | | - from pydantic_ai import UsageLimits |
99 | | - usage_limits = UsageLimits( |
100 | | - output_tokens_limit=5000, |
101 | | - total_tokens_limit=100000, |
102 | | - ) |
103 | | - |
104 | | - # Use VercelAIAdapter.dispatch_request (new API) |
105 | | - # This is now a classmethod that takes the request and agent directly |
106 | | - response = await VercelAIAdapter.dispatch_request( |
107 | | - tornado_request, |
108 | | - agent=agent, |
109 | | - model=model, |
110 | | -# usage=usage_limits, |
111 | | - builtin_tools=builtin_tools, |
112 | | - ) |
113 | | - |
114 | | - # Set headers from FastAPI response |
115 | | - for key, value in response.headers.items(): |
116 | | - self.set_header(key, value) |
117 | | - |
118 | | - # Stream the response body |
119 | | - # FastAPI StreamingResponse has body_iterator |
120 | | - # Wrap in try-except to catch cancel scope errors |
121 | | - if hasattr(response, 'body_iterator'): |
122 | | - try: |
123 | | - async for chunk in response.body_iterator: |
124 | | - """ |
125 | | - # Filter out benign cancel scope errors from the stream |
126 | | - # These are internal anyio errors that don't affect functionality |
127 | | - if isinstance(chunk, bytes): |
128 | | - chunk_str = chunk.decode('utf-8', errors='ignore') |
129 | | - else: |
130 | | - chunk_str = str(chunk) |
131 | | - |
132 | | - # Skip chunks that contain cancel scope errors |
133 | | - if 'cancel scope' in chunk_str.lower() and 'error' in chunk_str.lower(): |
134 | | - self.log.debug(f"Filtered out begin cancel scope error from stream") |
135 | | - continue |
136 | | - """ |
| 85 | + # Lazily create the MCP server connection for this request |
| 86 | + base_url = self.settings.get('chat_base_url') |
| 87 | + token = self.settings.get('chat_token') |
| 88 | + mcp_server = create_mcp_server(base_url, token) |
137 | 89 |
|
138 | | - # Write the chunk |
139 | | - if isinstance(chunk, bytes): |
140 | | - self.write(chunk) |
141 | | - else: |
142 | | - self.write(chunk.encode('utf-8') if isinstance(chunk, str) else chunk) |
143 | | - await self.flush() |
144 | | - except Exception as stream_error: |
145 | | - # Log but don't crash - the stream might have completed successfully |
146 | | - # Cancel scope errors often happen during cleanup after successful completion |
147 | | - self.log.debug(f"Stream iteration completed with: {stream_error}") |
148 | | - else: |
149 | | - # Fallback for non-streaming response |
150 | | - body = response.body |
151 | | - if isinstance(body, bytes): |
152 | | - self.write(body) |
153 | | - else: |
154 | | - self.write(body.encode('utf-8') if isinstance(body, str) else body) |
| 90 | + async with mcp_server: |
| 91 | + # Create request adapter (Starlette-compatible) |
| 92 | + tornado_request = TornadoRequestAdapter(self) |
| 93 | + |
| 94 | + # Parse request body to extract model if specified |
| 95 | + try: |
| 96 | + body = await tornado_request.json() |
| 97 | + model = body.get('model') if isinstance(body, dict) else None |
| 98 | + except Exception: |
| 99 | + model = None |
| 100 | + |
| 101 | + # Get builtin tools (empty list - tools metadata is only for UI display) |
| 102 | + # The actual pydantic-ai tools are registered in the agent itself |
| 103 | + builtin_tools = [] |
| 104 | + |
| 105 | + # Create usage limits for the agent |
| 106 | + usage_limits = UsageLimits( |
| 107 | + tool_calls_limit=5, |
| 108 | + output_tokens_limit=5000, |
| 109 | + total_tokens_limit=100000, |
| 110 | + ) |
| 111 | + |
| 112 | + # Use VercelAIAdapter.dispatch_request (new API) |
| 113 | + response = await VercelAIAdapter.dispatch_request( |
| 114 | + tornado_request, |
| 115 | + agent=agent, |
| 116 | + model=model, |
| 117 | + usage_limits=usage_limits, |
| 118 | + toolsets=[mcp_server], |
| 119 | + builtin_tools=builtin_tools, |
| 120 | + ) |
155 | 121 |
|
156 | | - # Finish the response |
157 | | - self.finish() |
| 122 | + # Set headers from FastAPI response |
| 123 | + for key, value in response.headers.items(): |
| 124 | + self.set_header(key, value) |
| 125 | + |
| 126 | + # Stream the response body |
| 127 | + if hasattr(response, 'body_iterator'): |
| 128 | + try: |
| 129 | + async for chunk in response.body_iterator: |
| 130 | + if isinstance(chunk, bytes): |
| 131 | + self.write(chunk) |
| 132 | + else: |
| 133 | + self.write(chunk.encode('utf-8') if isinstance(chunk, str) else chunk) |
| 134 | + await self.flush() |
| 135 | + except Exception as stream_error: |
| 136 | + self.log.debug(f"Stream iteration completed with: {stream_error}") |
| 137 | + else: |
| 138 | + body = response.body |
| 139 | + if isinstance(body, bytes): |
| 140 | + self.write(body) |
| 141 | + else: |
| 142 | + self.write(body.encode('utf-8') if isinstance(body, str) else body) |
| 143 | + |
| 144 | + # Finish the response while MCP context is active |
| 145 | + self.finish() |
158 | 146 |
|
159 | 147 | except Exception as e: |
160 | 148 | self.log.error(f"Error in chat handler: {e}", exc_info=True) |
|
0 commit comments