Geevarghese George commited on
Commit
33fcbb0
·
1 Parent(s): 0a897f3

fix flow with async

Browse files
app.py CHANGED
@@ -4,9 +4,9 @@ from pathlib import Path
4
 
5
  import gradio as gr
6
  from mcp import StdioServerParameters
7
- from mcpadapt.core import MCPAdapt
8
- from mcpadapt.smolagents_adapter import SmolAgentsAdapter
9
  from smolagents import InferenceClientModel
 
10
 
11
  from config import (
12
  AGENT_MODEL,
@@ -25,8 +25,7 @@ from src.upgrade_advisor.chat.chat import (
25
  )
26
  from src.upgrade_advisor.misc import (
27
  _monkeypatch_gradio_save_history,
28
- get_example_pyproject_question,
29
- get_example_requirements_question,
30
  )
31
  from src.upgrade_advisor.theme import christmas
32
 
@@ -180,12 +179,21 @@ def main():
180
  # Gradio chat interface state to persist uploaded files
181
  files_state = gr.State([])
182
 
183
- with MCPAdapt(
184
- serverparams=[
 
 
 
 
 
 
 
 
 
185
  gh_mcp_params,
186
  upload_mcp_params,
187
  ],
188
- adapter=SmolAgentsAdapter(structured_output=True),
189
  ) as toolset:
190
  logger.info("MCP clients connected successfully")
191
 
@@ -200,42 +208,30 @@ def main():
200
  demo = gr.ChatInterface(
201
  fn=chat_fn,
202
  chatbot=gr.Chatbot(
203
- height=600,
204
  type="messages",
205
  ),
206
  title="Package Upgrade Advisor",
207
  type="messages",
208
- # additional_inputs_accordion="Attach pyproject.toml file",
 
 
 
209
  textbox=gr.MultimodalTextbox(
210
- label="pyproject.toml",
211
- file_types=[".toml"],
212
  file_count="single",
213
  min_width=100,
214
  sources="upload",
215
- inputs=files_state,
216
  ),
217
- # additional_inputs=[files_state],
218
- additional_outputs=files_state,
219
  save_history=True,
220
- examples=[
221
- ["Tell me about the 'requests' package. How to use it with JSON ?"],
222
- [get_example_requirements_question()],
223
- [get_example_pyproject_question()],
224
- ["Which version of 'pandas' is compatible with 'numpy' 2.0?"],
225
- [
226
- {
227
- "text": """Can I upgrade my dependencies from
228
- the attached pyproject.toml to work with
229
- python 3.14? Any suggestions on
230
- potential issues I should be aware of?""",
231
- "files": ["tests/test2.toml"],
232
- }
233
- ],
234
- ],
235
  stop_btn=True,
236
  theme=christmas,
237
  )
238
- demo.launch()
239
 
240
  finally:
241
  logger.info("Cleaning up MCP client resources")
 
4
 
5
  import gradio as gr
6
  from mcp import StdioServerParameters
7
+ from mcpadapt.core import MCPAdapt # noqa: F401
 
8
  from smolagents import InferenceClientModel
9
+ from smolagents.mcp_client import MCPClient
10
 
11
  from config import (
12
  AGENT_MODEL,
 
25
  )
26
  from src.upgrade_advisor.misc import (
27
  _monkeypatch_gradio_save_history,
28
+ get_example_questions,
 
29
  )
30
  from src.upgrade_advisor.theme import christmas
31
 
 
179
  # Gradio chat interface state to persist uploaded files
180
  files_state = gr.State([])
181
 
182
+ # with MCPAdapt(
183
+ # serverparams=[
184
+ # gh_mcp_params,
185
+ # upload_mcp_params,
186
+ # ],
187
+ # adapter=SmolAgentsAdapter(structured_output=True),
188
+ # ) as toolset:
189
+ example_questions = get_example_questions(n=4)
190
+
191
+ with MCPClient(
192
+ server_parameters=[
193
  gh_mcp_params,
194
  upload_mcp_params,
195
  ],
196
+ structured_output=True,
197
  ) as toolset:
198
  logger.info("MCP clients connected successfully")
199
 
 
208
  demo = gr.ChatInterface(
209
  fn=chat_fn,
210
  chatbot=gr.Chatbot(
211
+ height=800,
212
  type="messages",
213
  ),
214
  title="Package Upgrade Advisor",
215
  type="messages",
216
+ additional_inputs_accordion="""
217
+ You may attach a pyproject.toml or requirements.txt file to get
218
+ specific upgrade advice for your project.
219
+ """,
220
  textbox=gr.MultimodalTextbox(
221
+ label="pyproject.toml or requirements.txt file can be attached",
222
+ file_types=[".toml", ".txt"],
223
  file_count="single",
224
  min_width=100,
225
  sources="upload",
 
226
  ),
227
+ additional_inputs=[files_state],
228
+ additional_outputs=[files_state],
229
  save_history=True,
230
+ examples=example_questions,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  stop_btn=True,
232
  theme=christmas,
233
  )
234
+ demo.launch(mcp_server=True, share=False)
235
 
236
  finally:
237
  logger.info("Cleaning up MCP client resources")
src/upgrade_advisor/agents/prompts.py CHANGED
@@ -34,6 +34,10 @@ def get_package_discovery_prompt(
34
  - "How to safely upgrade the packages in pyproject.toml to their highest
35
  versions without breaking my project?"
36
 
 
 
 
 
37
  Your knowledge cutoff may prevent you from knowing what's recent.
38
  NO MATTER WHAT, always use the current date (ISO format YYYY-MM-DD): {today_date}
39
  when reasoning about dates and
 
34
  - "How to safely upgrade the packages in pyproject.toml to their highest
35
  versions without breaking my project?"
36
 
37
+ DO NOT waste your time answering questions that are not related to Python package
38
+ discovery or upgrade advice. Politely inform the user that you can only help
39
+ with Python package-related queries.
40
+
41
  Your knowledge cutoff may prevent you from knowing what's recent.
42
  NO MATTER WHAT, always use the current date (ISO format YYYY-MM-DD): {today_date}
43
  when reasoning about dates and
src/upgrade_advisor/agents/tools/tools.py CHANGED
@@ -1,8 +1,6 @@
1
- import asyncio
2
  import logging
3
  import shutil
4
  from pathlib import Path
5
- from typing import Any
6
 
7
  from smolagents.tools import Tool
8
 
@@ -15,6 +13,7 @@ from src.upgrade_advisor.schema import (
15
  UVResolutionResultSchema,
16
  )
17
 
 
18
  from .pypi_api import (
19
  github_repo_and_releases,
20
  pypi_search,
@@ -28,55 +27,6 @@ logger.setLevel(logging.INFO)
28
  logger.addHandler(logging.StreamHandler())
29
 
30
 
31
- class AsyncTool(Tool):
32
- """A least-effort base class for defining an asynchronous tool.
33
-
34
- Improvements that can be made:
35
- - Timeout handling for async tasks.
36
- - Cancellation support for long-running tasks.
37
- """
38
-
39
- loop = asyncio.get_event_loop()
40
- task = None
41
-
42
- async def forward(self) -> Any:
43
- raise NotImplementedError(
44
- "AsyncTool subclasses must implement the forward method."
45
- )
46
-
47
- def async_forward(self, *args, **kwargs) -> Any:
48
- self.task = asyncio.create_task(self.forward(*args, **kwargs))
49
- try:
50
- # call the async forward method and wait for result
51
- return self.loop.run_until_complete(self.task)
52
- except asyncio.CancelledError as e:
53
- logger.error(f"Tool {self.name} execution was cancelled: {e}")
54
- raise e
55
- except Exception as e:
56
- logger.error(f"Error in tool {self.name} execution: {e}")
57
- raise e
58
- finally:
59
- logger.info(f"Tool {self.name} execution completed.")
60
-
61
- def _inspect_args(self):
62
- import inspect
63
-
64
- sig = inspect.signature(self.forward)
65
- bound = sig.bind_partial()
66
- return bound.args, bound.kwargs
67
-
68
- def __call__(self, *args, **kwargs):
69
- """Stripped down call method to support async forward calls."""
70
- if not self.is_initialized:
71
- self.setup()
72
- encoded_inputs = self.encode(*args, **kwargs)
73
- # call async forward instead of forward
74
- # we could aalso just await here, but this way we can have a sync interface
75
- outputs = self.async_forward({**encoded_inputs})
76
- decoded_outputs = self.decode(outputs)
77
- return decoded_outputs
78
-
79
-
80
  class ReadUploadFileTool(Tool):
81
  """Tool to safely read files saved in the `uploads` directory."""
82
 
@@ -138,6 +88,9 @@ class WriteTomlFileTool(Tool):
138
  pyproject.toml file before resolving dependencies.
139
  Also useful if the user cannot upload files directly or if the uploaded
140
  file has missing sections or formatting issues.
 
 
 
141
  """
142
  inputs = {
143
  "content": {
@@ -231,7 +184,34 @@ class ResolvePyProjectTOMLTool(Tool):
231
  return result
232
 
233
 
234
- class PypiSearchTool(AsyncTool):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  """Tool to search PyPI for package metadata."""
236
 
237
  name = "pypi_search"
@@ -255,12 +235,14 @@ class PypiSearchTool(AsyncTool):
255
  def __init__(self):
256
  super().__init__()
257
 
258
- async def forward(self, package: str, cutoff: int) -> dict:
259
- result = await pypi_search(package, cutoff=cutoff)
 
 
260
  return result
261
 
262
 
263
- class PypiSearchVersionTool(AsyncTool):
264
  """Tool to search PyPI for specific package version metadata."""
265
 
266
  name = "pypi_search_version"
@@ -288,39 +270,13 @@ class PypiSearchVersionTool(AsyncTool):
288
  def __init__(self):
289
  super().__init__()
290
 
291
- async def forward(self, package: str, version: str, cutoff: int) -> dict:
292
- result = await pypi_search_version(package, version, cutoff=cutoff)
293
- return result
294
-
295
-
296
- class RepoFromURLTool(Tool):
297
- """Tool to extract GitHub repository information from a URL."""
298
-
299
- name = "repo_from_url"
300
- description = """
301
- Extract GitHub repository information from a given URL.
302
- Returns a dictionary containing the owner and repository name.
303
- It returns a dictionary with the schema described in `output_schema` attribute.
304
- """
305
- inputs = {
306
- "url": {
307
- "type": "string",
308
- "description": "GitHub repository URL with https:// prefix.",
309
- }
310
- }
311
- output_type = "object"
312
- output_schema = GithubRepoSchema.schema()
313
-
314
- def __init__(self):
315
- super().__init__()
316
-
317
- def forward(self, url: str) -> dict:
318
- result = resolve_repo_from_url(url)
319
-
320
  return result
321
 
322
 
323
- class RepoFromPyPITool(AsyncTool):
324
  """Tool to extract GitHub repository information from a PyPI package."""
325
 
326
  name = "repo_from_pypi"
@@ -349,7 +305,8 @@ class RepoFromPyPITool(AsyncTool):
349
  def __init__(self):
350
  super().__init__()
351
 
352
- async def forward(self, package: str, cutoff: int) -> dict:
353
- result = await github_repo_and_releases(package, cutoff=cutoff)
 
354
 
355
  return result
 
 
1
  import logging
2
  import shutil
3
  from pathlib import Path
 
4
 
5
  from smolagents.tools import Tool
6
 
 
13
  UVResolutionResultSchema,
14
  )
15
 
16
+ from ...misc import run_coro_sync
17
  from .pypi_api import (
18
  github_repo_and_releases,
19
  pypi_search,
 
27
  logger.addHandler(logging.StreamHandler())
28
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  class ReadUploadFileTool(Tool):
31
  """Tool to safely read files saved in the `uploads` directory."""
32
 
 
88
  pyproject.toml file before resolving dependencies.
89
  Also useful if the user cannot upload files directly or if the uploaded
90
  file has missing sections or formatting issues.
91
+ If the user has provided a requirements.txt file instead of a pyproject.toml,
92
+ this tool can be used to convert the requirements.txt content into a
93
+ minimal pyproject.toml file. But make sure to follow the PEP621 format.
94
  """
95
  inputs = {
96
  "content": {
 
184
  return result
185
 
186
 
187
+ class RepoFromURLTool(Tool):
188
+ """Tool to extract GitHub repository information from a URL."""
189
+
190
+ name = "repo_from_url"
191
+ description = """
192
+ Extract GitHub repository information from a given URL.
193
+ Returns a dictionary containing the owner and repository name.
194
+ It returns a dictionary with the schema described in `output_schema` attribute.
195
+ """
196
+ inputs = {
197
+ "url": {
198
+ "type": "string",
199
+ "description": "GitHub repository URL with https:// prefix.",
200
+ }
201
+ }
202
+ output_type = "object"
203
+ output_schema = GithubRepoSchema.schema()
204
+
205
+ def __init__(self):
206
+ super().__init__()
207
+
208
+ def forward(self, url: str) -> dict:
209
+ result = resolve_repo_from_url(url)
210
+
211
+ return result
212
+
213
+
214
+ class PypiSearchTool(Tool):
215
  """Tool to search PyPI for package metadata."""
216
 
217
  name = "pypi_search"
 
235
  def __init__(self):
236
  super().__init__()
237
 
238
+ def forward(self, package: str, cutoff: int) -> dict:
239
+ coro = pypi_search(package, cutoff=cutoff)
240
+ result = run_coro_sync(coro)
241
+
242
  return result
243
 
244
 
245
+ class PypiSearchVersionTool(Tool):
246
  """Tool to search PyPI for specific package version metadata."""
247
 
248
  name = "pypi_search_version"
 
270
  def __init__(self):
271
  super().__init__()
272
 
273
+ def forward(self, package: str, version: str, cutoff: int) -> dict:
274
+ coro = pypi_search_version(package, version, cutoff=cutoff)
275
+ result = run_coro_sync(coro)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  return result
277
 
278
 
279
+ class RepoFromPyPITool(Tool):
280
  """Tool to extract GitHub repository information from a PyPI package."""
281
 
282
  name = "repo_from_pypi"
 
305
  def __init__(self):
306
  super().__init__()
307
 
308
+ def forward(self, package: str, cutoff: int) -> dict:
309
+ coro = github_repo_and_releases(package, cutoff=cutoff)
310
+ result = run_coro_sync(coro)
311
 
312
  return result
src/upgrade_advisor/misc.py CHANGED
@@ -1,49 +1,155 @@
 
1
  import logging
 
 
 
 
 
 
 
2
 
3
  logger = logging.getLogger(__name__)
4
  logger.setLevel(logging.INFO)
5
  logger.addHandler(logging.StreamHandler())
6
 
7
 
8
- def get_example_requirements_question() -> str:
9
- return """
10
- Here are the contents of my requirements file:
11
 
12
- numpy==1.19.5
13
- pandas==2.1.3
14
- scipy==1.12.0
15
- opencv-python==4.8.1.78
16
- torch==2.1.2
17
 
18
- help me fix it, its giving errors
19
- """
 
20
 
 
 
 
 
 
21
 
22
- def get_example_pyproject_question() -> str:
23
- return """
24
- Here are the contents of my pyproject.toml file:
25
 
26
- [tool.poetry]
27
- name = "example-project"
28
- version = "0.1.0"
29
- description = "An example Python project"
30
- authors = ["Your Name <[email protected]>"]
31
 
32
- [tool.poetry.dependencies]
33
- python = "^3.8"
34
- numpy = "^1.21.0"
35
- pandas = "^1.3.0"
36
- requests = "^2.26.0"
37
- fastapi = "^0.70.0"
38
- uvicorn = "^0.15.0"
39
 
40
- [tool.poetry.dev-dependencies]
41
- pytest = "^6.2.5"
42
- black = "^21.9b0"
43
 
44
- Help me identify any potential package upgrade issues.
45
- I wish to upgrade numpy to version 1.23.0 and pandas to version 1.5.0.
46
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
 
49
  def _monkeypatch_gradio_save_history():
@@ -72,3 +178,38 @@ def _monkeypatch_gradio_save_history():
72
 
73
  gr.ChatInterface._save_conversation = _safe_save_conversation
74
  gr.ChatInterface._ua_safe_patch = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
  import logging
3
+ import random
4
+ import threading
5
+ from typing import Any
6
+
7
+ _bg_loop: asyncio.AbstractEventLoop | None = None
8
+ _bg_thread: threading.Thread | None = None
9
+ _bg_lock = threading.Lock()
10
 
11
  logger = logging.getLogger(__name__)
12
  logger.setLevel(logging.INFO)
13
  logger.addHandler(logging.StreamHandler())
14
 
15
 
16
+ def get_example_questions(n: int = 3) -> str:
17
+ requirements_question = """ Here are the contents of my requirements file:
 
18
 
19
+ numpy==1.19.5
20
+ pandas==2.1.3
21
+ scipy==1.12.0
22
+ opencv-python==4.8.1.78
23
+ torch==2.1.2
24
 
25
+ help me fix it, its giving errors
26
+ """
27
+ requirements_question_2 = """Here are the contents of my requirements.txt:
28
 
29
+ numpy==1.19.5
30
+ pandas==2.2.0
31
+ scipy==1.12.0
32
+ scikit-learn==1.4.2
33
+ python-dateutil==2.8.2
34
 
 
 
 
35
 
36
+ I’m on Python 3.11 and running:
 
 
 
 
37
 
38
+ pip install -r requirements.txt
 
 
 
 
 
 
39
 
40
+ I get this error:
 
 
41
 
42
+ ERROR: Cannot install pandas==2.2.0 and numpy==1.19.5 because these
43
+ package versions have conflicting dependencies
44
+
45
+ Can you help me adjust the versions so they all work together on Python
46
+ 3.11, and explain how you picked them?"""
47
+ pyproject_question = """
48
+ Here are the contents of my pyproject.toml file:
49
+
50
+ [tool.poetry]
51
+ name = "example-project"
52
+ version = "0.1.0"
53
+ description = "An example Python project"
54
+ authors = ["Your Name <[email protected]>"]
55
+
56
+ [tool.poetry.dependencies]
57
+ python = "^3.8"
58
+ numpy = "^1.21.0"
59
+ pandas = "^1.3.0"
60
+ requests = "^2.26.0"
61
+ fastapi = "^0.70.0"
62
+ uvicorn = "^0.15.0"
63
+
64
+ [tool.poetry.dev-dependencies]
65
+ pytest = "^6.2.5"
66
+ black = "^21.9b0"
67
+
68
+ Help me identify any potential package upgrade issues.
69
+ I wish to upgrade numpy to version 1.23.0 and pandas to version 1.5.0.
70
+ """
71
+ requiments_question_apple = """Here’s my requirements.txt:
72
+
73
+ torch==2.1.2
74
+ torchvision==0.16.0
75
+ torchaudio==2.1.2
76
+ numpy==1.26.2
77
+
78
+
79
+ I’m on a MacBook with Apple Silicon (M2), Python 3.10, using:
80
+
81
+ pip install -r requirements.txt
82
+
83
+ I get errors about no matching wheel for torch:
84
+
85
+ ERROR: Could not find a version that satisfies the requirement torch==2.1.2
86
+
87
+ How should I change these dependencies so they work correctly on M2,
88
+ and what install commands should I use?"""
89
+
90
+ upgraded_python_question = """I had a project running on Python 3.8 with
91
+ this requirements.txt:
92
+
93
+ numpy==1.20.0
94
+ pandas==1.2.3
95
+ scikit-learn==0.24.1
96
+ xgboost==1.4.0
97
+
98
+
99
+ I upgraded my system to Python 3.12 and tried:
100
+
101
+ pip install -r requirements.txt
102
+
103
+ Now I get multiple errors about incompatible versions and missing wheels.
104
+
105
+ I’d like to:
106
+
107
+ Make this environment work on Python 3.12
108
+
109
+ Keep roughly the same libraries, but I’m fine upgrading to newer
110
+ compatible versions
111
+
112
+ Can you propose updated versions for these packages that work on Python
113
+ 3.12 and explain any major breaking changes I should watch for?"""
114
+
115
+ pip_confusion_question = """I’m using Poetry, but I also ran pip install
116
+ manually a few times and now things are broken.
117
+
118
+ My pyproject.toml dependencies:
119
+
120
+ [tool.poetry.dependencies]
121
+ python = "^3.10"
122
+ fastapi = "^0.95.0"
123
+ uvicorn = "^0.22.0"
124
+
125
+
126
+ pip list shows different versions:
127
+
128
+ fastapi 0.100.0
129
+
130
+ uvicorn 0.23.0
131
+
132
+ When I run poetry run uvicorn app.main:app, I get import errors like:
133
+
134
+ ImportError: cannot import name 'FastAPI' from 'fastapi'
135
+
136
+ Can you explain what’s going on with Poetry vs pip in this situation,
137
+ and give me a clear set of steps to get back to a consistent
138
+ environment?"""
139
+
140
+ all_questions = [
141
+ requirements_question,
142
+ requirements_question_2,
143
+ pyproject_question,
144
+ requiments_question_apple,
145
+ upgraded_python_question,
146
+ pip_confusion_question,
147
+ ]
148
+ choices = random.sample(all_questions, k=n)
149
+ # format as list of lists for gradio examples
150
+ choices = [[choice] for choice in choices]
151
+
152
+ return choices
153
 
154
 
155
  def _monkeypatch_gradio_save_history():
 
178
 
179
  gr.ChatInterface._save_conversation = _safe_save_conversation
180
  gr.ChatInterface._ua_safe_patch = True
181
+
182
+
183
+ def run_coro_sync(coro) -> Any:
184
+ """Run an async coroutine and return its result from sync code.
185
+
186
+ - If no event loop is running: use asyncio.run().
187
+ - If a loop is already running: dispatch to a dedicated background loop.
188
+ """
189
+ try:
190
+ loop = asyncio.get_running_loop()
191
+ loop_running = loop.is_running()
192
+ except RuntimeError:
193
+ loop = None
194
+ loop_running = False
195
+
196
+ # No loop: safest case
197
+ if not loop_running:
198
+ return asyncio.run(coro)
199
+
200
+ # Loop already running: use background loop in another thread
201
+ global _bg_loop, _bg_thread
202
+ with _bg_lock:
203
+ if _bg_loop is None or _bg_loop.is_closed():
204
+ _bg_loop = asyncio.new_event_loop()
205
+ _bg_thread = threading.Thread(
206
+ target=_bg_loop.run_forever,
207
+ name="async-tool-background-loop",
208
+ daemon=True,
209
+ )
210
+ _bg_thread.start()
211
+
212
+ bg_loop = _bg_loop
213
+
214
+ future = asyncio.run_coroutine_threadsafe(coro, bg_loop)
215
+ return future.result()
tests/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ numpy==1.19.5
2
+ pandas==2.1.3
3
+ scipy==1.12.0
4
+ opencv-python==4.8.1.78
5
+ torch==2.1.2