From 1554087a08edb487cbc18b41ec566c63a59b7f52 Mon Sep 17 00:00:00 2001 From: Steve Nyemba Date: Thu, 24 Sep 2026 19:09:54 -0500 Subject: [PATCH] added tools/MCP to studio, supports AzureOpenAI/Ollama --- www/html/_assets/css/studio.css | 8 +- www/html/_assets/images/http.png | Bin 0 -> 50110 bytes www/html/_assets/js/studio.js | 69 +++- www/html/_assets/themes/default/footer.css | 3 +- www/html/_assets/themes/default/header.css | 10 +- www/html/_assets/themes/default/layout.css | 6 +- www/html/_assets/themes/default/menu.css | 12 +- .../_assets/themes/default/responsive.css | 8 +- www/html/_plugins/agent.py | 388 +++++++++--------- www/html/_plugins/readme.md | 42 ++ www/html/_plugins/register.py | 8 +- www/html/_plugins/transport.py | 9 +- www/html/about.html | 9 +- www/html/index.html | 2 +- www/html/menu.html | 9 +- www/html/search.html | 2 +- www/html/setup-ai.html | 20 +- 17 files changed, 346 insertions(+), 259 deletions(-) create mode 100644 www/html/_assets/images/http.png create mode 100644 www/html/_plugins/readme.md diff --git a/www/html/_assets/css/studio.css b/www/html/_assets/css/studio.css index a44a26a..7ddc040 100644 --- a/www/html/_assets/css/studio.css +++ b/www/html/_assets/css/studio.css @@ -123,10 +123,10 @@ input[type=text]:focus { overflow:hidden; height:20px; } -.main .menu {display:none} -.main .content{grid-row:2; grid-column:1} -.main .pane {grid-row:2; grid-column:2} -.main .footer{grid-row:4; grid-column:1;} +.qcms-main .menu {display:none} +.qcms-main .content{grid-row:2; grid-column:1} +/* .qcms-main .qcms-pane {grid-row:2; grid-column:2} */ +.qcms-main .qcms-footer{display:block; } /** * syncfusion spreadsheet ... */ diff --git a/www/html/_assets/images/http.png b/www/html/_assets/images/http.png new file mode 100644 index 0000000000000000000000000000000000000000..b58510d04c9b8892796165a67be46dd9ff64afa3 GIT binary patch literal 50110 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 - var _record = {} //-- formatted appropriately - row.forEach((value,index)=>{ - _col = _data.columns[index] - _record[_col] = value - }) - rows.push(_record) - }) - + // _data.data.forEach(row=>{ + // var _record = {} //-- formatted appropriately + // row.forEach((value,index)=>{ + // _col = _data.columns[index] + // _record[_col] = value + // }) + // rows.push(_record) + // }) + var spreadsheet = new ej.spreadsheet.Spreadsheet(); _sheet_name = _id.trim().replace(/[#,.]/g,'').replace(/ /g,'-').trim() + spreadsheet.sheets = [ {name:_sheet_name, - ranges:[{dataSource:rows}], + ranges:[{dataSource:_data.data}], } ] + if( $(_id)[0].spreadsheet ){ book = $(_id)[0].spreadsheet ; book.sheets.forEach((sheet)=>{ @@ -246,15 +250,53 @@ studio.frame = function (_args){ $(_id).empty() $(_id).append($('')) if(x.status == 200 && x.readyState == 4){ + console.log(x.responseText) _r = JSON.parse(x.responseText) // sessionStorage._data = x.responseText + if (_r.query == null){ _compute(_label,_r) }else{ - _compute(_label,_r.data) + + _ghandler = _compute(_label,_r) var _id = '.code.'+_args.label.trim() // $(_id).val('-- '+$(_id).val().replace(/\n/g,'-- ') + '\n'+_r['query']) $(_id).val(_r['query']) + if (_r.analysis) { + // + // We need to append document + var spreadsheet = $(`.${_args.label.trim()} .output`)[0].spreadsheet + // + rows = [] + _r.analysis.forEach((row)=>{ + var _record = {} //-- formatted appropriately + + // sheet.ranges: [{ dataSource: row.data }] + + N = spreadsheet.sheets.length + 1 + var sheet = {index:N-1} + sheet.name = row.title + sheet.ranges = [{dataSource:row.data,startCell:'B1'}] + + + + spreadsheet.insertSheet([sheet],-1) + spreadsheet.refresh() + spreadsheet.updateCell({ + value: 'Analysis',style:{fontWeight:'bold'} + },`${row.title}!A1`) + spreadsheet.updateCell({ + value: row.explanation + },`${row.title}!A2`) + + spreadsheet.refresh() + }) + + // spreadsheet.sheets.push(sheet) + // spreadsheet.insertSheet({range:{dataSource:rows}},0) + + + } } sessionStorage[_label] = _query @@ -577,12 +619,13 @@ studio.frame = function (_args){ } $(_id).empty() + _gHandler = new studio.grid() _gHandler.syncfusion(_id,_data) _rows = (_data.data)?_data.data.length : 0 $('.'+_label + ' .status').html(_rows + ' rows') - - + return _gHandler + } } diff --git a/www/html/_assets/themes/default/footer.css b/www/html/_assets/themes/default/footer.css index ee9c89a..77f4124 100644 --- a/www/html/_assets/themes/default/footer.css +++ b/www/html/_assets/themes/default/footer.css @@ -2,13 +2,12 @@ * This file styles the footer of the page */ -#footer { +.qcms-footer { text-align:center; display:grid; grid-template-columns: repeat(3,1fr); gap:4px; - font-size:12px; color:black; align-items: center; diff --git a/www/html/_assets/themes/default/header.css b/www/html/_assets/themes/default/header.css index f5e2d24..eb9ad80 100644 --- a/www/html/_assets/themes/default/header.css +++ b/www/html/_assets/themes/default/header.css @@ -1,12 +1,12 @@ -.main .header { +.qcms-main .qcms-header { grid-column:1 ; display:grid; grid-template-columns:64px auto; gap:4px} -.main .header .title {font-size:28px; font-weight:bold} -.main .header .subtitle {font-size:12px;} -.main .header img {width:55px; height:55px;} +.qcms-main .qcms-header .qcms-title {font-size:28px; font-weight:bold} +.qcms-main .qcms-header .qcms-subtitle {font-size:12px;} +.qcms-main .qcms-header img {width:55px; height:55px;} -.header, .footer { +.qcms-header, .qcms-footer { padding:4px; align-content: center; align-items:center; diff --git a/www/html/_assets/themes/default/layout.css b/www/html/_assets/themes/default/layout.css index d6bb234..ff97d09 100644 --- a/www/html/_assets/themes/default/layout.css +++ b/www/html/_assets/themes/default/layout.css @@ -1,6 +1,6 @@ -.main { +.qcms-main { display:grid; grid-template-rows:70px calc(100% - 70px); grid-template-columns:75% 25%; @@ -9,7 +9,7 @@ height:96vh; line-height: 1.5; font-size:16px; font-family: sans-serif; font-weight:lighter; } -.main #content{ +.qcms-main .qcms-content{ display:grid; @@ -22,7 +22,7 @@ } -/* .main .pane {border-left:1px solid transparent; height:100%;} */ +/* .qcms-main .pane {border-left:1px solid transparent; height:100%;} */ .bold {font-weight:bold} .small-text {font-size:12px; font-weight:lighter; color :#000000;} .active {cursor:pointer; padding:2px; margin:2px; border-bottom:2px solid transparent} diff --git a/www/html/_assets/themes/default/menu.css b/www/html/_assets/themes/default/menu.css index 0e47ae7..dd5cf8e 100644 --- a/www/html/_assets/themes/default/menu.css +++ b/www/html/_assets/themes/default/menu.css @@ -1,4 +1,4 @@ -.menu { +.qcms-menu { padding:8px; border:1px solid #CAD5E0 ; @@ -10,9 +10,9 @@ align-items: center; } -.menu .icon {padding:4px;} -.menu .icon img {width:30px; height:30px;} -.menu .item { +.qcms-menu .qcms-icon {padding:4px;} +.qcms-menu .qcms-icon img {width:30px; height:30px;} +.qcms-menu .item { font-weight:bold; cursor:pointer; padding:4px; @@ -24,7 +24,7 @@ } -.menu .sub-menu { +.qcms-menu .qcms-sub-menu { display:none; position:absolute; @@ -40,7 +40,7 @@ } -.menu .item:hover .sub-menu{ +.qcms-menu .item:hover .qcms-sub-menu{ display:block; height:auto; diff --git a/www/html/_assets/themes/default/responsive.css b/www/html/_assets/themes/default/responsive.css index cd91420..b3aadd3 100644 --- a/www/html/_assets/themes/default/responsive.css +++ b/www/html/_assets/themes/default/responsive.css @@ -1,19 +1,19 @@ /* Responsive Design */ @media (max-width: 1024px) { - .main { + .qcms-main { grid-template-columns: 60% 40%; grid-template-rows: 70px auto 32px; } } @media (max-width: 768px) { - .main { + .qcms-main { grid-template-columns: 100%; grid-template-rows: auto; height: auto; } - .main .pane { + .qcms-main .qcms-pane { border-left: none; border-top: 1px solid #CAD5E0; padding: 8px; @@ -21,7 +21,7 @@ } @media (max-width: 480px) { - .main { + .qcms-main { padding: 5%; font-size: 14px; } diff --git a/www/html/_plugins/agent.py b/www/html/_plugins/agent.py index 836f501..62fe0d1 100644 --- a/www/html/_plugins/agent.py +++ b/www/html/_plugins/agent.py @@ -1,218 +1,214 @@ -""" -This is code that will interact with an LLM (AzureOpenAI/Ollama) leveraging langchain -""" -# _CATALOG = { -# 'sqlite':{'sql':'select tbl_name as table_name, json_group_array(y.name) as columns from sqlite_master x INNER JOIN PRAGMA_TABLE_INFO(tbl_name) y group by table_name'} , -# 'postgresql':{'sql':"SELECT table_name, to_json(array_agg(column_name)) as columns FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = 'public' GROUP BY table_name"}, -# 'bigquery':{'sql':'SELECT table_name, TO_JSON(ARRAY_AGG(column_name)) as columns FROM :dataset.INFORMATION_SCHEMA.COLUMNS','args':['dataset']}, -# 'duckdb':{'sql' :'SELECT table_name, TO_JSON(ARRAY_AGG(column_name)) as columns FROM INFORMATION_SCHEMA.COLUMNS GROUP BY table_name'} -# } - -# _CATALOG['sqlite3'] = _CATALOG['sqlite'] -import transport +from langchain_ollama import ChatOllama +from langchain_openai import AzureChatOpenAI, ChatOpenAI +import requests +from typing import Optional import json -import os +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.tools import StructuredTool +from langchain.agents import create_agent +from pydantic import create_model + +from pydantic import BaseModel, Field, field_validator + +from typing import Dict, Any, List +from langchain.agents.structured_output import ToolStrategy +import transport import cms import pandas as pd -from langchain_openai import AzureOpenAI, AzureChatOpenAI -from langchain_ollama import OllamaEmbeddings, OllamaLLM -from langchain_core.messages import HumanMessage, AIMessage -from langchain_classic.chains import LLMChain -from langchain_core.prompts import PromptTemplate -from langchain_core.runnables import RunnablePassthrough -from langchain_core.output_parsers import StrOutputParser - -class Agent : - def __init__(self,**_args) : +import plugin_ix as plug +import os + +_map ={ + 'postgresql':"Select table_schema||'.'||table_name as table_name, table_schema as schema_name, column_name, data_type as type from information_schema.columns", + 'duckdb':"SELECT table_schema||'.'||table_name as table_name , table_schema AS schema_name, column_name, data_type FROM information_schema.columns", + 'sqlite3':'SELECT * FROM sqlite_master', + 'mysql':'SELECT table_name, table_schema AS schema_name, column_name, data_type FROM information_schema.columns', + 'bigquery':'SELECT table_name, table_schema AS schema_name, column_name, data_type FROM :dataset.INFORMATION_SCHEMA.COLUMNS', + 'iceberg':'SHOW TABLES', 'mariadb':'SHOW TABLES' +} +class AnalysisItem(BaseModel): + title: str = Field(description="Short title for this piece of analysis") + explanation: str = Field(description="Explanation of the insight") + # Using a structured dict or strict List[Dict] + data: List[Dict[str, Any]] = Field(description="Relevant rows supporting this analysis point") + + @field_validator('data', mode='before') + @classmethod + def ensure_list_of_dicts(cls, v): + if isinstance(v, dict): + return [v] # Wrap single dicts if the LLM forgot the list wrapper + return v + +class QueryOutput(BaseModel): + query: str = Field(description="The query that was generated and executed") + data: List[Dict[str, Any]] = Field(description="Rows returned from executing the query") + analysis: List[AnalysisItem] = Field(description="Analysis points derived from the data") + + @field_validator('data', mode='before') + @classmethod + def ensure_list_of_dicts(cls, v): + if isinstance(v, dict): + return [v] + return v + +class Tool: + def __init__(self,_dbr) : + self._dbr = _dbr + self._provider = _dbr._agent._engine.name + + + def catalog(self): """ - :backend OpenAI or Ollama - :kwargs The initialization parameters associated with the backend - The arguments will contain temperature and model name to be used + This function returns the catalog of the current database """ - _instance = AzureChatOpenAI if _args['backend'].lower() in ['azureopenai', 'openai'] else OllamaLLM - self._llm = _instance(**_args['kwargs']) - def isSQL(self,_question): - _template = """Is the provided text a valid sql statmement. Yes or No? Your answer is a properly formatted JSON object with three attributes. - class (1 for valid sql statement and 0 for not a valid sql statement), explanation place a short explanation for the answer and original containing with the original text. + global _map + _query = _map.get(self._provider) - text: - {input_text} + return self.read(_query) + def schema(self,table:str) ->dict: """ - _prompt = PromptTemplate(temperature=0.1,input_variables=['input_text'],template=_template) - - r = self.apply(_prompt,input_text=_question) - # - # @TODO: Make sure the response is properly formatted (output not to be trusted) - return json.loads(r) - def apply(self,_prompt,**_args): - - chain = ( - RunnablePassthrough.assign() - | _prompt - | self._llm - | StrOutputParser()) - _out = chain.invoke(_args) - - return _out #son.loads(_out) - def toSQL(self,_question,_catalog,_about): - _template="""Your task is to convert a question to an SQL query. The query will run on schema that will be provided in csv format. - -Output: -The expected output will be a JSON object with two attributes sql and tables: -- "sql": the SQL query to be executed. -- "tables": list of relevant tables used. + This function returns the schema for a given database, if table is not provided the entire catalog will be returned with the list of tables + """ + _query = f""" {_map[self._provider]} WHERE name = '{table}' """ + if self._provider in ['sqlite','sqlite3'] : + _info = self.read(f"select * from {table} limit 20").dtypes + return pd.DataFrame({"column_name":_info.dtypes.index.tolist()[1:],"data_type": [x.name for x in _info.dtypes.tolist()[1:]]}).to_dict(orient='records') + pass + else: + return self.read(_query) + def read(self,query:str)->dict: + """ + This function will execute a query against a given label/database. The query must adhere to the schema of the tables involved. + """ + return self._dbr.read(sql=query).to_dict(orient='records') -Guidelines: -- If the question can not be answered with the provided schema return empty string in the sql attribute -- Parse the question word by word so as to be able to identify tables, fields and operations associated (Joins, filters ...) -- Under no circumstances will you provide an explanation of tables or reasoning detail. -- Avoid using subqueries, and use field names as represented in the provided schema and their data types +class TransportAgent: + """ + This class is intended to allow an LLM to interact with data-transport MCP server. + """ + def __init__(self, **kwargs): + """ + :backend ollama or openai (for now) + :kwargs parameters that will be used for the openai,ollama classes + """ + self.backend = kwargs.get('backend', 'ollama').lower() + self.kwargs = kwargs.get('kwargs', {}).copy() + # self.mcp_url = kwargs.get('mcp_url', 'http://localhost:8090') + self.chatbot = self.instance(self.backend, **self.kwargs) + # self.tools = self._init_mcp_tools() + # self.agent = self._init_agent() + registry = plug.Registry() + if registry.has(kwargs['parser']) : + self._parser = registry.get(kwargs['parser']) + else: + self._parser = None -question: -{question} + def instance(self, backend, **kwargs): + """ + Returns a chat model instance for the given backend. + Supported: ollama, azureopenai, openai. + """ + if backend == 'azureopenai': + return AzureChatOpenAI(**kwargs) + elif backend == 'openai': + return ChatOpenAI(**kwargs) + else: + return ChatOllama(**kwargs) + def get(self,label,query): + dbr = transport.get.reader(label=label) + tools = Tool(dbr) + # self.chatbot.bind_tools ([tools.catalog,tools.read]) -Database schema: -{catalog} + _message = f""" + You are a specialized Data-Transport Agent designed to facilitate data access and analysis from registered + labels/databases. The user is working in {label} database ({dbr._agent._engine.name} database). + + Your primary goal is to execute a requested operation based on the user's query. You MUST strictly adhere to + the following process and output format. -additional information: -{context} + **Process Flow:** + if and ONLY if the user's question is a database query execute the "read" function associated with the tools. + Otherwise perform the following: + 1. **Catalog Retrieval:** First, return the list of tables with a field describing the table. + schema) from the catalog. + 2. **Query Generation:** Construct an accurate SQL query that directly addresses the user's request using the + the information pulled from the catalog. + 3. Make sure the generated query adheres to constraints of {dbr._agent._engine.name} database + 4. **Execution:** Execute the SQL query against by calling the "read" function of the tools. + 5. **Analysis is Optional and should be provided ONLY if the user requested it:** If the user asks for an analysis, perform the necessary data processing and derive + meaningful insights based on the query data. + + **Output Format Enforcement:** + You MUST respond ONLY with a single JSON object. Do not include any introductory text, explanations, or + conversational language outside of the JSON structure. + + **JSON Schema Requirements:** + The output MUST strictly conform to the following JSON structure, utilizing the `QueryOutput` schema defined by + your execution context: + - **"query"**: The exact SQL query that was executed. + - **"data"**: A list of dictionaries containing the raw data rows returned from the query execution. + - **"analysis"**: An optional list of analysis points. If analysis was performed, this list MUST contain + objects with the following structure: + - "title": A concise title for the derived insight. It can ONLY be 30 characters at most. + - "data": The specific data rows supporting the analysis. + - "text": A detailed explanation of the insight derived from the data. + + Ensure all generated data and analysis are directly traceable back to the executed query and the retrieved + table schemas. """ - - _prompt = PromptTemplate(temperature=0.1,input_variables=['question','catalog','context'],template=_template) - r = self.apply(_prompt,question=_question,catalog=_catalog,context=_about) - # print (' ############### ------------- #####################') - # print (r) - # print (' #########################################') - # - # @TODO: Make sure the response is properly formatted (output not to be trusted) - if '```json' in r : - r = r.split('```json')[-1].replace('```','') - print (r) - return json.loads(r) - - - -# -# We are building an interface to execute an sql query -# - -def AIProxy (_label,_query,_path,_CATALOG) : - _qreader = transport.get.reader(label=_label) - _entry = transport.registry.get(_label) - _provider =_entry['provider'] - _about = _entry.get('about','') - _database = _entry['database'] if 'database' in _entry else '' - - if 'dataset' in _entry : - _database = _entry['dataset'] - _about = f'{_about}, with dataset name {_database}' - else: - _about = f'{_about}, with the database name {_database}' - _catalog = None - _kwargs = None - _data = pd.DataFrame() - r = None - try: - # - # we should run the command here, assuming a valid query - # - - _data = _qreader.apply(_query) - except Exception as e: - # - # here we are assuming we are running text to be translated as a query - # we need to make sure that we have a JSON configurator - # - if _provider in _CATALOG and os.path.exists(_path): - # - # -- reading arguments from the LLM config file, {backend,kwargs} - _kwargs = json.loads((open(_path)).read()) - _agent = Agent(**_kwargs) - _qcat = _CATALOG[_provider] - if 'args' in _CATALOG[_provider] : - _entry = transport.registry.get(_label) - for _var in _CATALOG[_provider]['args'] : - if _var in _entry: - _value = _entry[_var] - _about = f'{_about}\n{_var} = {_value}' - _qcat['sql'] = _qcat['sql'].replace(f':{_var}',_value) - - _catalog = _qreader.read(**_qcat).to_csv(index=0) - _about = f"The queries will run on {_provider} database.\n{_about}" - r = _agent.toSQL(_query,_catalog, _about) - - try: - _data = _qreader.apply(r['sql']) - except Exception as e1: - _data = pd.DataFrame() - pass - # - # returning the data and the information needed - # - _data = _data.astype(str).to_dict(orient='split') - del _data['index'] - if r : - return {'data':_data,'query':r['sql']} - return _data - + _caller = create_agent(self.chatbot,[tools.catalog,tools.schema,tools.read],system_prompt=_message,response_format=QueryOutput) + _output = _caller.invoke({"messages":[("user",query)]})["messages"][-1].content + if self._parser : + return self._parser(_output) + else: + return _output +LLM_ARGS = {} +def InitLLMArgs (_config): + global LLM_ARGS - # if not _path : - # # - # # exececute the query as is ! - # pass - # else: - - # if _provider in _CATALOG and os.path.exists(_path) : - - # # - # # As the agent if it is an SQL Query - # f = open(_path) - # _kwargs = json.loads( f.read() ) - # f.close() - # try: - - # print ([f"Running Model {_kwargs['kwargs']['model']}"]) - # _agent = Agent(**_kwargs) - # # r = _agent.isSQL(_query) - - # # print (f"****** {_query}\n{r['class']}") - # # if r and int(r['class']) == 0 : - # _catalog = _qreader.read(**_CATALOG[_provider]).to_csv(index=0) - # # print (['****** TABLES FOUND ', _catalog]) - # _about = _about if _about else '' - # r = _agent.toSQL(_query,_catalog,f"This is a {_provider} database, and queries generated should account for this. {_about}") - # _query = r['sql'] - # # else: - # # # - # # # provided an sql query - # # pass - # except Exception as e: - # # - # # AI Service is unavailable ... need to report this somehow - # print (e) - # else: - # # - # # Not in catalog ... - # pass - # _data = _qreader.apply(_query) - # if _data.shape[0] : - # _data = _data.astype(str).to_dict(orient='split') - # if 'index' in _data : - # del _data['index'] - # return _data + path = _config['system']['source']['ai']['config-path'] + f = open(path) + LLM_ARGS = json.loads(f.read()) + f.close() + # + # We also have to initialize the plugin path + os.environ['REGISTRY_FOLDER'] = _config['system']['source']['ai']['plugin-path'] + return LLM_ARGS - @cms.Plugin(mimetype="application/json",method="POST") -def apply (**_args): +def apply(**_args): + global LLM_ARGS _request = _args['request'] + _config = _args['config'] + # + # let's make sure the _label is whitelisted _label = _request.json['label'] _query = _request.json['query'] - _source = _args['config']['system']['source'] - _path = _source.get('llm',None) - _CATALOGS = _source.get('catalogs',{}) - return AIProxy(_label,_query,_path,_CATALOGS) + if not LLM_ARGS : + LLM_ARGS = InitLLMArgs(_args['config']) + if _query.strip().lower().startswith("with") or _query.strip().lower().startswith("select") : + dbr = transport.get.reader(label=_label) + + return { "data":dbr.read(sql=_query).fillna(0).to_dict(orient='records') ,"analysis":[]} + elif _query.strip().lower().startswith("{") : + dbr = transport.get.reader(label=_label) + return { "data":dbr.read(json.loads(_query)).to_dict(orient='records'),"analysis":[] } + + _agent = TransportAgent(**LLM_ARGS) + + # we need an output parser for the models (should be loaded as a plugin) + print ([LLM_ARGS['backend'], LLM_ARGS['kwargs']['model']]) + return _agent.get(_label,_query) + + # _data = {"query": "SELECT condition_concept_id, COUNT(*) AS condition_count FROM tabular.condition_occurrence GROUP BY condition_concept_id ORDER BY condition_count DESC LIMIT 10;", "data": [{"condition_concept_id": "316866", "condition_count": 5635}, {"condition_concept_id": "4159131", "condition_count": 5462}, {"condition_concept_id": "2005200547", "condition_count": 5375}, {"condition_concept_id": "4291025", "condition_count": 5037}, {"condition_concept_id": "201826", "condition_count": 4026}, {"condition_concept_id": "433736", "condition_count": 4011}, {"condition_concept_id": "4036620", "condition_count": 3734}, {"condition_concept_id": "4317977", "condition_count": 3341}, {"condition_concept_id": "37018196", "condition_count": 2627}, {"condition_concept_id": "81902", "condition_count": 2458}], "analysis": [{"title": "Top 10 Most Frequent Conditions", "data": [{"condition_concept_id": "316866", "condition_count": 5635}, {"condition_concept_id": "4159131", "condition_count": 5462}, {"condition_concept_id": "2005200547", "condition_count": 5375}, {"condition_concept_id": "4291025", "condition_count": 5037}, {"condition_concept_id": "201826", "condition_count": 4026}, {"condition_concept_id": "433736", "condition_count": 4011}, {"condition_concept_id": "4036620", "condition_count": 3734}, {"condition_concept_id": "4317977", "condition_count": 3341}, {"condition_concept_id": "37018196", "condition_count": 2627}, {"condition_concept_id": "81902", "condition_count": 2458}], "text": "The distribution analysis of conditions, based on the top 10 most frequent condition concept IDs, shows that '316866' is the most commonly recorded condition, with 5,635 occurrences. The top four conditions (316866, 4159131, 2005200547, and 4291025) account for a significant portion of the recorded conditions, indicating these are the most prevalent diagnoses in the dataset."}]} + # _data['data'] = pd.DataFrame(_data['data']).to_dict(orient='split',index=False) + # for _row in _data['analysis'] : + # if 'data' in _row : + # _row['data'] = pd.DataFrame(_row['data']).to_dict(orient='records') + # return _data + @cms.Plugin(mimetype="text/plain") def enabled(**_args): _config = _args['config'] - return str(int('llm' in _config['system']['source'] )) + return str(int('ai' in _config['system']['source'] )) diff --git a/www/html/_plugins/readme.md b/www/html/_plugins/readme.md new file mode 100644 index 0000000..e6a3f4c --- /dev/null +++ b/www/html/_plugins/readme.md @@ -0,0 +1,42 @@ +# Transport Agent Notebook + +This notebook provides a way to interact with the **data-transport MCP server** using LangChain and Marimo. + +## Overview + +The `notebook.py` file implements a `TransportAgent` class that bridges the gap between Large Language Models (LLMs) and the capabilities of the data-transport system via the **Model Context Protocol (MCP)**. It also demonstrates how to perform direct data access using the `transport` library. + +## How it Works + +The `notebook.py` file demonstrates how to integrate MCP with LangChain through the following steps: + +1. **Dynamic Tool Discovery**: Instead of hard-coding tool definitions, the `TransportAgent` fetches the OpenAPI specification from the MCP server (at `http://localhost:8090/openapi.json`). +2. **Schema Mapping**: It automatically parses the OpenAPI `components` and `paths` to generate LangChain `StructuredTool` instances. This ensures that the agent always has the latest tools available as defined by the MCP server. +3. **Agent Construction**: These tools are then passed into a LangChain agent. The agent uses these tools to fulfill user requests, such as listing databases or performing data analysis. +4. **Interactive Interface**: The Marimo framework is used to provide a clean, interactive UI where users can type questions and see the agent's thought process and results. + +## Code Breakdown + +### `TransportAgent` Class +The heart of the notebook. It manages: +- **LLM Configuration**: Supports `ollama`, `openai`, and `azure_openai` backends. +- **Tool Initialization (`_init_mcp_tools`)**: + - Fetches the `openapi.json` from the local MCP server. + - Maps API types (string, integer, etc.) to Python types. + - Dynamically creates `StructuredTool` objects with proper descriptions and argument schemas. +- **Agent Initialization (`_init_agent`)**: Combines the chosen LLM with the discovered tools into a functional agent. +- **Query Handling (`get`)**: Takes a natural language prompt and returns the final response from the agent's execution chain. + +## How to leverage MCP with LangChain (Summary) + +To integrate MCP with LangChain in this project: +1. **Expose via OpenAPI**: The MCP server must provide an `openapi.json` endpoint. +2. **Automate Tool Creation**: Use a helper function (like `_init_mcp_tools` in this notebook) to iterate over the OpenAPI paths and generate `StructuredTool` objects. +3. **Bind to Agent**: Pass the list of generated tools to a LangChain agent (e.g., `create_agent`). +4. **Dynamic Updates**: Because the tools are loaded at runtime from the OpenAPI spec, any update to the MCP server's capabilities is automatically reflected in the agent. + +## Usage +To run this notebook, ensure the MCP server is running on `localhost:8090` and then execute the marimo notebook: +```bash +marimo edit www/html/_plugins/notebook.py +``` diff --git a/www/html/_plugins/register.py b/www/html/_plugins/register.py index ed4d83b..585b96e 100644 --- a/www/html/_plugins/register.py +++ b/www/html/_plugins/register.py @@ -14,10 +14,10 @@ def get (**_args) : transport.registry.load() _data = copy.copy(transport.registry.DATA) _context = _args['config']['system']['context'] - print () - _CATALOGS = _args['config']['system']['source'].get('catalogs',{}) - _HAS_LLM = 'llm' in _args['config']['system']['source'] + # _CATALOGS = _args['config']['system']['source'].get('catalogs',[]) + _HAS_LLM = 'ai' in _args['config']['system']['source'] + _ALLOWED = [] if not _HAS_LLM else _args['config']['system']['source']['ai']['allowed'] _labels = [] for _key in _data : @@ -32,7 +32,7 @@ def get (**_args) : _plugins = [] if 'plugins' not in _data[_key]else _data[_key]['plugins'] _icon = f'{_context}/api/disk/read?uri=www/html/_assets/images/{_provider}.png' _icon = f'_assets/images/{_provider}.png' - _labels.append({"label":_key,"provider":_provider,'table':_table,'icon':_icon,'AI': (_provider in _CATALOGS and _HAS_LLM)}) + _labels.append({"label":_key,"provider":_provider,'table':_table,'icon':_icon,'AI': _key in _ALLOWED }) else: continue return _labels diff --git a/www/html/_plugins/transport.py b/www/html/_plugins/transport.py index 3b3feb5..d6c8f0d 100644 --- a/www/html/_plugins/transport.py +++ b/www/html/_plugins/transport.py @@ -49,18 +49,15 @@ def apply (**_args): try: if _query.strip()[0] not in ['{','[']: _query = _query.replace('%%','%').replace('%','%%') - _data = _qreader.apply(_query) #.astype(str).to_dict(orient='split') + _data = _qreader.read(sql=_query) #.astype(str).to_dict(orient='split') else: _data = _qreader.read( **json.loads(_query)) if _data.shape[0] : - _data = _data.astype(str).to_dict(orient='split') - if 'index' in _data: - del _data['index'] - + _data = _data.astype(str).to_dict(orient='records') #',index=False) else: _data = [] _qreader.close() - return _data + return {"data":_data} except Exception as e: raise Exception( str(e)) diff --git a/www/html/about.html b/www/html/about.html index f6dc5e0..e9b0837 100644 --- a/www/html/about.html +++ b/www/html/about.html @@ -79,6 +79,9 @@ $(document).ready(()=>{ Credit
    +
  • + All-of-Us Program @ National Insitute of Health +
  • HIPLAB @ Vanderbilt University Medical Center
  • @@ -113,11 +116,9 @@ $(document).ready(()=>{ fontawesome,
  • - Iconfinder -
  • -
  • - freepik.com | iconduck + Iconfinder; freepik; iconduck
  • +
diff --git a/www/html/index.html b/www/html/index.html index cbbc495..b25f987 100644 --- a/www/html/index.html +++ b/www/html/index.html @@ -51,7 +51,7 @@ http.get(uri,(x)=>{ var _html = ('
Powered By data-transport '+x.responseText+'
') $ - $('.footer').html(_html) + $('.qcms-footer').html(_html) sessionStorage.wb_powered = x.responseText }) diff --git a/www/html/menu.html b/www/html/menu.html index 9f36e5b..ffb6fd6 100644 --- a/www/html/menu.html +++ b/www/html/menu.html @@ -28,7 +28,7 @@ overflow-y: auto; scroll-behavior: smooth; } -.labels .button {padding:4px; margin:4px; align-items: center; display:grid; grid-template-columns: 50px auto; gap:8px; font-weight:bold;} +.labels .button {padding:4px; margin:4px; align-items: center; display:grid; grid-template-columns: 50px auto 28px; gap:8px; font-weight:bold; height:32px;} .labels img {width:30px; margin:4px;} .studio .output {padding:4px;} @@ -97,7 +97,10 @@ } - + if (_data.AI){ + var _icon = $('') + $(_div).append(_icon) + } $(_prefix+'.search-results .labels').append(_div) }); @@ -213,7 +216,7 @@
-
+
diff --git a/www/html/search.html b/www/html/search.html index 88517e0..4a24a90 100644 --- a/www/html/search.html +++ b/www/html/search.html @@ -14,7 +14,7 @@