suzannejin commited on
Commit
7d87a4b
·
2 Parent(s): 33c1975e1b14f6

unify modal with tools

Browse files
Files changed (2) hide show
  1. main.py +48 -26
  2. tools/meta_yml_tools.py +122 -0
main.py CHANGED
@@ -5,22 +5,13 @@ import modal
5
  import sys
6
  import subprocess
7
  import time
8
- # import litellm
9
 
10
- # define modal app
11
- app = modal.App("agent-ontology")
12
-
13
- # define ollama image for modal
14
- OLLAMA_IMAGE = (
15
  modal.Image.debian_slim()
16
- # pkill/pgrep come from procps
17
- .apt_install(
18
- "curl", "gnupg", "software-properties-common",
19
- "procps" # ← adds pkill, pgrep, ps …
20
- )
21
- # install Ollama
22
  .run_commands("curl -fsSL https://ollama.com/install.sh | sh")
23
- # spin up daemon, pull the model, shut daemon down
24
  .run_commands(
25
  "bash -c 'ollama serve >/dev/null 2>&1 & "
26
  "PID=$!; "
@@ -29,7 +20,6 @@ OLLAMA_IMAGE = (
29
  "ollama pull qwen3:0.6b && "
30
  "kill $PID'"
31
  )
32
- # python deps
33
  .pip_install(
34
  "fastmcp>=2.6.1",
35
  "gradio[mcp]>=5.0.0",
@@ -40,6 +30,9 @@ OLLAMA_IMAGE = (
40
  )
41
  )
42
 
 
 
 
43
  def chat_with_agent(message, history):
44
  """ Function to handle chat messages and interact with the agent.
45
  This function creates a new MCP connection for each request, allowing the agent to use tools from the MCP server.
@@ -73,33 +66,62 @@ def chat_with_agent(message, history):
73
 
74
  except Exception as e:
75
  return f"❌ Error: {e}\nType: {type(e).__name__}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- def run_agent():
78
  """ Function to run the agent with a Gradio interface.
79
  This function sets up the Gradio interface and launches it.
80
  """
81
- demo = gr.ChatInterface(
82
- fn=chat_with_agent,
83
- type="messages",
84
- examples=["can you extract input/output metadata from fastqc nf-core module ?"],
85
- title="Agent with MCP Tools (Per-Request Connection)",
86
- description="This version creates a new MCP connection for each request."
87
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  demo.launch(share=True)
89
 
90
- @app.function(image=OLLAMA_IMAGE, gpu="A10G", timeout=2400)
91
  def main_remote():
92
  # spin up Ollama daemon in the background
93
  server = subprocess.Popen(["ollama", "serve"])
94
  time.sleep(6) # give it a moment to bind :11434
95
  try:
96
- # litellm._turn_on_debug()
97
- run_agent()
98
  finally:
99
  server.terminate()
100
 
101
  def main_local():
102
- run_agent()
103
 
104
  if __name__ == "__main__":
105
  # check if it is modal running the script or python running the script
 
5
  import sys
6
  import subprocess
7
  import time
8
+ from .tools.meta_yml_tools import fetch_meta_yml,get_meta_yml_file, extract_tools_from_meta_json, extract_information_from_meta_json, extract_module_name_description
9
 
10
+ # Define the custom image
11
+ ollama_image = (
 
 
 
12
  modal.Image.debian_slim()
13
+ .apt_install("curl", "gnupg", "software-properties-common", "procps")
 
 
 
 
 
14
  .run_commands("curl -fsSL https://ollama.com/install.sh | sh")
 
15
  .run_commands(
16
  "bash -c 'ollama serve >/dev/null 2>&1 & "
17
  "PID=$!; "
 
20
  "ollama pull qwen3:0.6b && "
21
  "kill $PID'"
22
  )
 
23
  .pip_install(
24
  "fastmcp>=2.6.1",
25
  "gradio[mcp]>=5.0.0",
 
30
  )
31
  )
32
 
33
+ # Initialize the Modal app with the custom image
34
+ app = modal.App("agent-ontology", image=ollama_image)
35
+
36
  def chat_with_agent(message, history):
37
  """ Function to handle chat messages and interact with the agent.
38
  This function creates a new MCP connection for each request, allowing the agent to use tools from the MCP server.
 
66
 
67
  except Exception as e:
68
  return f"❌ Error: {e}\nType: {type(e).__name__}"
69
+
70
+ def run_multi_agent(module_name):
71
+ meta_yml = get_meta_yml_file(module_name=module_name)
72
+ module_info = extract_module_name_description(meta_file=meta_yml)
73
+ module_tools = extract_tools_from_meta_json(meta_file=meta_yml)
74
+ # TODO: agent to choose the right tool
75
+ # Only call the agent if there is more than one tool, otherwise get the first name
76
+ first_prompt = f"""
77
+ The module {module_info[0]} with desciption '{module_info[1]}' contains a series of tools.
78
+ Find the tool that best describes the module. Return only one tool. Return the name.
79
+ This is the list of tools:
80
+ {"\n\t".join(f"{tool[0]}: {tool[1]}" for tool in module_tools)}
81
+ """
82
+ tool_name = "fastqc" # this would be the answer of the first agent
83
+ meta_info = extract_information_from_meta_json(meta_file=meta_yml, tool_name=tool_name)
84
+ return(meta_info)
85
 
86
+ def run_interface():
87
  """ Function to run the agent with a Gradio interface.
88
  This function sets up the Gradio interface and launches it.
89
  """
90
+ # create the Gradio interface
91
+ with gr.Blocks() as demo:
92
+ gr.Markdown("### 🔍 Update an nf-core module `meta.yml` file by adding EDAM ontology terms.")
93
+
94
+ # create the input textbox for the nf-core module name
95
+ module_input = gr.Textbox(label="nf-core Module Name", placeholder="e.g. fastqc")
96
+
97
+ # create the button to fetch the meta.yml file
98
+ fetch_btn = gr.Button("Update meta.yml")
99
+
100
+ # create the output textbox for the meta.yml content and a download button
101
+ meta_output = gr.Textbox(label="meta.yml content", lines=20)
102
+ download_button = gr.File(label="Download meta.yml")
103
+
104
+ # set the function to run when the button is clicked
105
+ fetch_btn.click(
106
+ fn=run_multi_agent, # TODO: change to final function
107
+ inputs=module_input,
108
+ outputs=[meta_output]
109
+ )
110
+
111
  demo.launch(share=True)
112
 
113
+ @app.function(keep_warm=1, gpu="A10G", timeout=2400)
114
  def main_remote():
115
  # spin up Ollama daemon in the background
116
  server = subprocess.Popen(["ollama", "serve"])
117
  time.sleep(6) # give it a moment to bind :11434
118
  try:
119
+ run_interface()
 
120
  finally:
121
  server.terminate()
122
 
123
  def main_local():
124
+ run_interface()
125
 
126
  if __name__ == "__main__":
127
  # check if it is modal running the script or python running the script
tools/meta_yml_tools.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ import yaml
4
+
5
+ # TODO: placeholder function
6
+ def fetch_meta_yml(module_name):
7
+ # Adjust the URL or path to your actual source of nf-core modules
8
+ base_url = f"https://raw.githubusercontent.com/nf-core/modules/refs/heads/master/modules/nf-core/{module_name}/meta.yml"
9
+ try:
10
+ response = requests.get(base_url)
11
+ response.raise_for_status()
12
+ content = response.text
13
+
14
+ # Save for download
15
+ with open("meta.yml", "w") as f:
16
+ f.write(content)
17
+
18
+ return content, "meta.yml"
19
+ except Exception as e:
20
+ return f"Error: Could not retrieve meta.yml for module '{module_name}'\n{e}", None
21
+
22
+ def get_meta_yml_file(module_name: str) -> dict:
23
+ """
24
+ Access the nf-core/modules repository and return the meta.yml file of the given module.
25
+
26
+ Args:
27
+ module_name (str): The name of the module to get the meta.yml file for.
28
+ The module_name must be provided in the format <tool>_<subtool> or <tool>/<subtool> or <tool> <subtool>.
29
+ The subtool is optional.
30
+ For example, "bwa_align" or "fastqc" or "bwa align" or "bwa/align".
31
+ Returns:
32
+ dict: The meta.yml file of the given module in json/yaml format as a dictionary.
33
+ """
34
+ if "_" in module_name:
35
+ tool, subtool = module_name.split("_")
36
+ elif "/" in module_name:
37
+ tool, subtool = module_name.split("/")
38
+ elif " " in module_name:
39
+ tool, subtool = module_name.split(" ")
40
+ else:
41
+ tool, subtool = module_name, ""
42
+
43
+ if subtool:
44
+ url = f"https://raw.githubusercontent.com/nf-core/modules/refs/heads/master/modules/nf-core/{tool}/{subtool}/meta.yml"
45
+ else:
46
+ url = f"https://raw.githubusercontent.com/nf-core/modules/refs/heads/master/modules/nf-core/{tool}/meta.yml"
47
+ try:
48
+ response = requests.get(url)
49
+ response.raise_for_status() # Raise an error for bad status codes
50
+ return yaml.safe_load(response.text)
51
+ except requests.exceptions.RequestException as e:
52
+ raise RuntimeError(f"An error occurred while connecting to the URL: {url}. Error message: {e}")
53
+
54
+ def extract_module_name_description(meta_file: dict) -> list:
55
+ """
56
+ Extract the name and description of the module from the meta.yml file.
57
+
58
+ Args:
59
+ meta_file (str): The content of the module meta.yml file in json format.
60
+
61
+ Returns:
62
+ list: A list containing two elements, the module name and the module description.
63
+ """
64
+ name = meta_file.get("name", "")
65
+ description = meta_file.get("description", "")
66
+ return [name, description]
67
+
68
+ def extract_tools_from_meta_json(meta_file: dict) -> list[list]:
69
+ """
70
+ Extract the tools and description from the meta.yml file.
71
+
72
+ Args:
73
+ meta_file (str): The content of the module meta.yml file in json format.
74
+
75
+ Returns:
76
+ list: A list of lists. Each element of the list is one tool, the sub-list contains two elements, the name and description of the tool.
77
+ """
78
+ module_tools = []
79
+ tools_list = meta_file.get("tools", [])
80
+ for tool in tools_list:
81
+ name = list(tool.keys())[0]
82
+ description = tool[name].get("description", "")
83
+ module_tools.append([name, description])
84
+ return module_tools
85
+
86
+ def extract_information_from_meta_json(meta_file: dict, tool_name: str) -> dict:
87
+ """
88
+ Extract information metadata from an nf-core module meta.yml file.
89
+ Information extracted:
90
+ - inputs
91
+ - outputs
92
+ - homepage URL
93
+ - documentation URL
94
+ - bio.tools ID
95
+
96
+
97
+ Args:
98
+ meta_file (str): The content of the module meta.yml file in json format.
99
+ tool_name (str): The name of the tool to extract information for.
100
+
101
+ Returns:
102
+ dict: A dictionary with the extracted metadata.
103
+ Each file or term can also contain additional metadata like 'description' or 'type'.
104
+
105
+ Example output:
106
+ {
107
+ "inputs": [...],
108
+ "outputs": [...],
109
+ "homepage": "https://www.bioinformatics.babraham.ac.uk/projects/fastqc/",
110
+ "documentation": "https://www.bioinformatics.babraham.ac.uk/projects/fastqc/Help/",
111
+ "bio_tools_id": "biotools:fastqc"
112
+ }
113
+ """
114
+ inputs = meta_file.get("input", [])
115
+ outputs = meta_file.get("output", [])
116
+ for tool in meta_file.get("tools", []):
117
+ if list(tool.keys())[0] == tool_name:
118
+ homepage_url = tool.get("homepage", "")
119
+ documentation_rul = tool.get("documentation", "")
120
+ bio_tools_id = tool.get("identifier", "")
121
+ print("Extracted metadata information from nf-core module meta.yml")
122
+ return {"inputs": inputs, "outputs": outputs, "homepage": homepage_url, "documentation": documentation_rul, "bio_tools_id": bio_tools_id}