added tools/MCP to studio, supports AzureOpenAI/Ollama

main
Steve Nyemba 4 weeks ago
parent 394b24fc7d
commit 1554087a08

@ -123,10 +123,10 @@ input[type=text]:focus {
overflow:hidden; overflow:hidden;
height:20px; height:20px;
} }
.main .menu {display:none} .qcms-main .menu {display:none}
.main .content{grid-row:2; grid-column:1} .qcms-main .content{grid-row:2; grid-column:1}
.main .pane {grid-row:2; grid-column:2} /* .qcms-main .qcms-pane {grid-row:2; grid-column:2} */
.main .footer{grid-row:4; grid-column:1;} .qcms-main .qcms-footer{display:block; }
/** /**
* syncfusion spreadsheet ... * syncfusion spreadsheet ...
*/ */

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

@ -3,6 +3,7 @@
ej.base.registerLicense('ORg4AjUWIQA/Gnt2XFhhQlJHfV5AQmBIYVp/TGpJfl96cVxMZVVBJAtUQF1hTH5VdkxhWnpWcHZcTmhfWkZ/') ej.base.registerLicense('ORg4AjUWIQA/Gnt2XFhhQlJHfV5AQmBIYVp/TGpJfl96cVxMZVVBJAtUQF1hTH5VdkxhWnpWcHZcTmhfWkZ/')
ej.spreadsheet.Spreadsheet.Inject(ej.spreadsheet.ExcelExport); ej.spreadsheet.Spreadsheet.Inject(ej.spreadsheet.ExcelExport);
var studio = {_context:''} var studio = {_context:''}
r = [{name:'steve',age:46},{name:'elon',age:10}]
studio.defaults = { studio.defaults = {
'duckdb':'SELECT * \nFROM INFORMATION_SCHEMA.TABLES', 'duckdb':'SELECT * \nFROM INFORMATION_SCHEMA.TABLES',
'sqlite':'SELECT * \nFROM sqlite_master', 'sqlite':'SELECT * \nFROM sqlite_master',
@ -70,6 +71,7 @@ function triggerDownload(blob, filename) {
URL.revokeObjectURL(url); // cleanup URL.revokeObjectURL(url); // cleanup
} }
studio.grid = function (){ studio.grid = function (){
this.columns = function (_data){ this.columns = function (_data){
var _columns = [] var _columns = []
if(_data.columns){ if(_data.columns){
@ -162,23 +164,25 @@ studio.grid = function (){
var rows = [] var rows = []
_data.data.forEach(row=>{ // _data.data.forEach(row=>{
var _record = {} //-- formatted appropriately // var _record = {} //-- formatted appropriately
row.forEach((value,index)=>{ // row.forEach((value,index)=>{
_col = _data.columns[index] // _col = _data.columns[index]
_record[_col] = value // _record[_col] = value
}) // })
rows.push(_record) // rows.push(_record)
}) // })
var spreadsheet = new ej.spreadsheet.Spreadsheet(); var spreadsheet = new ej.spreadsheet.Spreadsheet();
_sheet_name = _id.trim().replace(/[#,.]/g,'').replace(/ /g,'-').trim() _sheet_name = _id.trim().replace(/[#,.]/g,'').replace(/ /g,'-').trim()
spreadsheet.sheets = [ spreadsheet.sheets = [
{name:_sheet_name, {name:_sheet_name,
ranges:[{dataSource:rows}], ranges:[{dataSource:_data.data}],
} }
] ]
if( $(_id)[0].spreadsheet ){ if( $(_id)[0].spreadsheet ){
book = $(_id)[0].spreadsheet ; book = $(_id)[0].spreadsheet ;
book.sheets.forEach((sheet)=>{ book.sheets.forEach((sheet)=>{
@ -246,15 +250,53 @@ studio.frame = function (_args){
$(_id).empty() $(_id).empty()
$(_id).append($('<i class="fa-solid fa-play"></i>')) $(_id).append($('<i class="fa-solid fa-play"></i>'))
if(x.status == 200 && x.readyState == 4){ if(x.status == 200 && x.readyState == 4){
console.log(x.responseText)
_r = JSON.parse(x.responseText) _r = JSON.parse(x.responseText)
// sessionStorage._data = x.responseText // sessionStorage._data = x.responseText
if (_r.query == null){ if (_r.query == null){
_compute(_label,_r) _compute(_label,_r)
}else{ }else{
_compute(_label,_r.data)
_ghandler = _compute(_label,_r)
var _id = '.code.'+_args.label.trim() var _id = '.code.'+_args.label.trim()
// $(_id).val('-- '+$(_id).val().replace(/\n/g,'-- ') + '\n'+_r['query']) // $(_id).val('-- '+$(_id).val().replace(/\n/g,'-- ') + '\n'+_r['query'])
$(_id).val(_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 sessionStorage[_label] = _query
@ -577,12 +619,13 @@ studio.frame = function (_args){
} }
$(_id).empty() $(_id).empty()
_gHandler = new studio.grid() _gHandler = new studio.grid()
_gHandler.syncfusion(_id,_data) _gHandler.syncfusion(_id,_data)
_rows = (_data.data)?_data.data.length : 0 _rows = (_data.data)?_data.data.length : 0
$('.'+_label + ' .status').html(_rows + ' rows') $('.'+_label + ' .status').html(_rows + ' rows')
return _gHandler
} }
} }

@ -2,13 +2,12 @@
* This file styles the footer of the page * This file styles the footer of the page
*/ */
#footer { .qcms-footer {
text-align:center; text-align:center;
display:grid; display:grid;
grid-template-columns: repeat(3,1fr); grid-template-columns: repeat(3,1fr);
gap:4px; gap:4px;
font-size:12px; font-size:12px;
color:black; color:black;
align-items: center; align-items: center;

@ -1,12 +1,12 @@
.main .header { .qcms-main .qcms-header {
grid-column:1 ; grid-column:1 ;
display:grid; grid-template-columns:64px auto; gap:4px} display:grid; grid-template-columns:64px auto; gap:4px}
.main .header .title {font-size:28px; font-weight:bold} .qcms-main .qcms-header .qcms-title {font-size:28px; font-weight:bold}
.main .header .subtitle {font-size:12px;} .qcms-main .qcms-header .qcms-subtitle {font-size:12px;}
.main .header img {width:55px; height:55px;} .qcms-main .qcms-header img {width:55px; height:55px;}
.header, .footer { .qcms-header, .qcms-footer {
padding:4px; padding:4px;
align-content: center; align-content: center;
align-items:center; align-items:center;

@ -1,6 +1,6 @@
.main { .qcms-main {
display:grid; display:grid;
grid-template-rows:70px calc(100% - 70px); grid-template-rows:70px calc(100% - 70px);
grid-template-columns:75% 25%; grid-template-columns:75% 25%;
@ -9,7 +9,7 @@
height:96vh; height:96vh;
line-height: 1.5; font-size:16px; font-family: sans-serif; font-weight:lighter; line-height: 1.5; font-size:16px; font-family: sans-serif; font-weight:lighter;
} }
.main #content{ .qcms-main .qcms-content{
display:grid; 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} .bold {font-weight:bold}
.small-text {font-size:12px; font-weight:lighter; color :#000000;} .small-text {font-size:12px; font-weight:lighter; color :#000000;}
.active {cursor:pointer; padding:2px; margin:2px; border-bottom:2px solid transparent} .active {cursor:pointer; padding:2px; margin:2px; border-bottom:2px solid transparent}

@ -1,4 +1,4 @@
.menu { .qcms-menu {
padding:8px; padding:8px;
border:1px solid #CAD5E0 ; border:1px solid #CAD5E0 ;
@ -10,9 +10,9 @@
align-items: center; align-items: center;
} }
.menu .icon {padding:4px;} .qcms-menu .qcms-icon {padding:4px;}
.menu .icon img {width:30px; height:30px;} .qcms-menu .qcms-icon img {width:30px; height:30px;}
.menu .item { .qcms-menu .item {
font-weight:bold; font-weight:bold;
cursor:pointer; cursor:pointer;
padding:4px; padding:4px;
@ -24,7 +24,7 @@
} }
.menu .sub-menu { .qcms-menu .qcms-sub-menu {
display:none; display:none;
position:absolute; position:absolute;
@ -40,7 +40,7 @@
} }
.menu .item:hover .sub-menu{ .qcms-menu .item:hover .qcms-sub-menu{
display:block; display:block;
height:auto; height:auto;

@ -1,19 +1,19 @@
/* Responsive Design */ /* Responsive Design */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.main { .qcms-main {
grid-template-columns: 60% 40%; grid-template-columns: 60% 40%;
grid-template-rows: 70px auto 32px; grid-template-rows: 70px auto 32px;
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.main { .qcms-main {
grid-template-columns: 100%; grid-template-columns: 100%;
grid-template-rows: auto; grid-template-rows: auto;
height: auto; height: auto;
} }
.main .pane { .qcms-main .qcms-pane {
border-left: none; border-left: none;
border-top: 1px solid #CAD5E0; border-top: 1px solid #CAD5E0;
padding: 8px; padding: 8px;
@ -21,7 +21,7 @@
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.main { .qcms-main {
padding: 5%; padding: 5%;
font-size: 14px; font-size: 14px;
} }

@ -1,218 +1,214 @@
""" from langchain_ollama import ChatOllama
This is code that will interact with an LLM (AzureOpenAI/Ollama) leveraging langchain from langchain_openai import AzureChatOpenAI, ChatOpenAI
""" import requests
# _CATALOG = { from typing import Optional
# '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
import json 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 cms
import pandas as pd import pandas as pd
from langchain_openai import AzureOpenAI, AzureChatOpenAI import plugin_ix as plug
from langchain_ollama import OllamaEmbeddings, OllamaLLM import os
from langchain_core.messages import HumanMessage, AIMessage
from langchain_classic.chains import LLMChain _map ={
from langchain_core.prompts import PromptTemplate 'postgresql':"Select table_schema||'.'||table_name as table_name, table_schema as schema_name, column_name, data_type as type from information_schema.columns",
from langchain_core.runnables import RunnablePassthrough 'duckdb':"SELECT table_schema||'.'||table_name as table_name , table_schema AS schema_name, column_name, data_type FROM information_schema.columns",
from langchain_core.output_parsers import StrOutputParser 'sqlite3':'SELECT * FROM sqlite_master',
'mysql':'SELECT table_name, table_schema AS schema_name, column_name, data_type FROM information_schema.columns',
class Agent : 'bigquery':'SELECT table_name, table_schema AS schema_name, column_name, data_type FROM :dataset.INFORMATION_SCHEMA.COLUMNS',
def __init__(self,**_args) : '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 This function returns the catalog of the current database
:kwargs The initialization parameters associated with the backend
The arguments will contain temperature and model name to be used
""" """
_instance = AzureChatOpenAI if _args['backend'].lower() in ['azureopenai', 'openai'] else OllamaLLM global _map
self._llm = _instance(**_args['kwargs']) _query = _map.get(self._provider)
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.
text: return self.read(_query)
{input_text} def schema(self,table:str) ->dict:
""" """
_prompt = PromptTemplate(temperature=0.1,input_variables=['input_text'],template=_template) 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
"""
r = self.apply(_prompt,input_text=_question) _query = f""" {_map[self._provider]} WHERE name = '{table}' """
# if self._provider in ['sqlite','sqlite3'] :
# @TODO: Make sure the response is properly formatted (output not to be trusted) _info = self.read(f"select * from {table} limit 20").dtypes
return json.loads(r) 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')
def apply(self,_prompt,**_args): pass
else:
chain = ( return self.read(_query)
RunnablePassthrough.assign() def read(self,query:str)->dict:
| _prompt """
| self._llm This function will execute a query against a given label/database. The query must adhere to the schema of the tables involved.
| StrOutputParser()) """
_out = chain.invoke(_args) return self._dbr.read(sql=query).to_dict(orient='records')
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.
Guidelines: class TransportAgent:
- 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 ...) This class is intended to allow an LLM to interact with data-transport MCP server.
- 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 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: def instance(self, backend, **kwargs):
{question} """
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: _message = f"""
{catalog} 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: **Process Flow:**
{context} 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.
""" """
_caller = create_agent(self.chatbot,[tools.catalog,tools.schema,tools.read],system_prompt=_message,response_format=QueryOutput)
_prompt = PromptTemplate(temperature=0.1,input_variables=['question','catalog','context'],template=_template) _output = _caller.invoke({"messages":[("user",query)]})["messages"][-1].content
r = self.apply(_prompt,question=_question,catalog=_catalog,context=_about) if self._parser :
# print (' ############### ------------- #####################') return self._parser(_output)
# print (r) else:
# print (' #########################################') return _output
# LLM_ARGS = {}
# @TODO: Make sure the response is properly formatted (output not to be trusted) def InitLLMArgs (_config):
if '```json' in r : global LLM_ARGS
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
# if not _path : path = _config['system']['source']['ai']['config-path']
# # f = open(path)
# # exececute the query as is ! LLM_ARGS = json.loads(f.read())
# pass f.close()
# else: #
# We also have to initialize the plugin path
# if _provider in _CATALOG and os.path.exists(_path) : os.environ['REGISTRY_FOLDER'] = _config['system']['source']['ai']['plugin-path']
return LLM_ARGS
# #
# # 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
@cms.Plugin(mimetype="application/json",method="POST") @cms.Plugin(mimetype="application/json",method="POST")
def apply (**_args): def apply(**_args):
global LLM_ARGS
_request = _args['request'] _request = _args['request']
_config = _args['config']
#
# let's make sure the _label is whitelisted
_label = _request.json['label'] _label = _request.json['label']
_query = _request.json['query'] _query = _request.json['query']
_source = _args['config']['system']['source'] if not LLM_ARGS :
_path = _source.get('llm',None) LLM_ARGS = InitLLMArgs(_args['config'])
_CATALOGS = _source.get('catalogs',{}) if _query.strip().lower().startswith("with") or _query.strip().lower().startswith("select") :
return AIProxy(_label,_query,_path,_CATALOGS) 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") @cms.Plugin(mimetype="text/plain")
def enabled(**_args): def enabled(**_args):
_config = _args['config'] _config = _args['config']
return str(int('llm' in _config['system']['source'] )) return str(int('ai' in _config['system']['source'] ))

@ -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
```

@ -14,10 +14,10 @@ def get (**_args) :
transport.registry.load() transport.registry.load()
_data = copy.copy(transport.registry.DATA) _data = copy.copy(transport.registry.DATA)
_context = _args['config']['system']['context'] _context = _args['config']['system']['context']
print ()
_CATALOGS = _args['config']['system']['source'].get('catalogs',{}) # _CATALOGS = _args['config']['system']['source'].get('catalogs',[])
_HAS_LLM = 'llm' in _args['config']['system']['source'] _HAS_LLM = 'ai' in _args['config']['system']['source']
_ALLOWED = [] if not _HAS_LLM else _args['config']['system']['source']['ai']['allowed']
_labels = [] _labels = []
for _key in _data : for _key in _data :
@ -32,7 +32,7 @@ def get (**_args) :
_plugins = [] if 'plugins' not in _data[_key]else _data[_key]['plugins'] _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'{_context}/api/disk/read?uri=www/html/_assets/images/{_provider}.png'
_icon = f'_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: else:
continue continue
return _labels return _labels

@ -49,18 +49,15 @@ def apply (**_args):
try: try:
if _query.strip()[0] not in ['{','[']: if _query.strip()[0] not in ['{','[']:
_query = _query.replace('%%','%').replace('%','%%') _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: else:
_data = _qreader.read( **json.loads(_query)) _data = _qreader.read( **json.loads(_query))
if _data.shape[0] : if _data.shape[0] :
_data = _data.astype(str).to_dict(orient='split') _data = _data.astype(str).to_dict(orient='records') #',index=False)
if 'index' in _data:
del _data['index']
else: else:
_data = [] _data = []
_qreader.close() _qreader.close()
return _data return {"data":_data}
except Exception as e: except Exception as e:
raise Exception( str(e)) raise Exception( str(e))

@ -79,6 +79,9 @@ $(document).ready(()=>{
Credit Credit
</h4> </h4>
<ul class="border-left"> <ul class="border-left">
<li>
<i class="fa-solid fa-minus"></i> <span class="active" onclick="window.open('https://www.nih.gov/allofus')">All-of-Us</span> Program @ National Insitute of Health
</li>
<li> <li>
<i class="fa-solid fa-minus"></i> <span class="active" onclick="window.open('https://hiplab.mc.vanderbilt.edu')">HIPLAB</span> @ Vanderbilt University Medical Center <i class="fa-solid fa-minus"></i> <span class="active" onclick="window.open('https://hiplab.mc.vanderbilt.edu')">HIPLAB</span> @ Vanderbilt University Medical Center
</li> </li>
@ -113,11 +116,9 @@ $(document).ready(()=>{
<i class="fa-solid fa-minus"></i> fontawesome, <i class="fa-solid fa-minus"></i> fontawesome,
</li> </li>
<li class="active" onclick="window.open('https://iconfinder.com','iconfinder')"> <li class="active" onclick="window.open('https://iconfinder.com','iconfinder')">
<i class="fa-solid fa-minus"></i> Iconfinder <i class="fa-solid fa-minus"></i> Iconfinder; freepik; iconduck
</li>
<li class="active" onclick="window.open('https://freepik.com','freepik')">
<i class="fa-solid fa-minus"></i> freepik.com | iconduck
</li> </li>
</ul> </ul>

@ -51,7 +51,7 @@
http.get(uri,(x)=>{ http.get(uri,(x)=>{
var _html = ('<div>Powered By <span style="text-transform:lowercase; color:#4682B4;">data-transport</span> '+x.responseText+'</div>') var _html = ('<div>Powered By <span style="text-transform:lowercase; color:#4682B4;">data-transport</span> '+x.responseText+'</div>')
$ $
$('.footer').html(_html) $('.qcms-footer').html(_html)
sessionStorage.wb_powered = x.responseText sessionStorage.wb_powered = x.responseText
}) })

@ -28,7 +28,7 @@
overflow-y: auto; overflow-y: auto;
scroll-behavior: smooth; 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;} .labels img {width:30px; margin:4px;}
.studio .output {padding:4px;} .studio .output {padding:4px;}
@ -97,7 +97,10 @@
} }
if (_data.AI){
var _icon = $('<i class="fa-solid fa-microchip" style="color:#FF6500;" title="AI Enabled"></i>')
$(_div).append(_icon)
}
$(_prefix+'.search-results .labels').append(_div) $(_prefix+'.search-results .labels').append(_div)
}); });
@ -213,7 +216,7 @@
</div> </div>
</div> </div>
<div class="border item" title="Setup AI" align="center" onclick="dialog.show({uri:'www/html/setup-ai.html',title:'Setup AI',context:'{{system.context}}'})"> <div class="border item" title="Setup AI" align="center" onclick="dialog.show({uri:'www/html/setup-ai.html',title:'Setup AI',context:'{{system.context}}'})">
<div class="active"><i class="fa-solid fa-microchip"></i></div> <div class="active"><i class="fa-solid fa-microchip" style="color:#FF6500"></i></div>
</div> </div>

@ -14,7 +14,7 @@
<div class="setting-frame lookup"> <div class="setting-frame lookup">
<div class="search-box border" > <div class="search-box border" >
<input type="text" class="search-label" placeholder="[Search transport registry]" onkeyup="_find()"/> <input type="text" class="search-label" placeholder="[Search database in registry]" onkeyup="_find()"/>
<div class="border-left" align="center"> <div class="border-left" align="center">
<div class="active" onclick="_reset()"><i class="fa-solid fa-trash"></i> </div> <div class="active" onclick="_reset()"><i class="fa-solid fa-trash"></i> </div>
</div> </div>

@ -5,9 +5,10 @@
$('.labels').children().each((_index)=>{ $('.labels').children().each((_index)=>{
var _data = $('.labels').children()[_index]._data var _data = $('.labels').children()[_index]._data
if(_data.AI){ if(_data.AI){
_aiTech.push(_data.provider) _aiTech.push(_data.label)
} }
}) })
console.log(_aiTech)
_aiTech = jx.utils.unique(_aiTech) _aiTech = jx.utils.unique(_aiTech)
$('.ai-tech').html( _aiTech.join(' , ')) $('.ai-tech').html( _aiTech.join(' , '))
$('.ai-setup .ai-button').on('click',ai.toggle) $('.ai-setup .ai-button').on('click',ai.toggle)
@ -21,24 +22,29 @@
</script> </script>
<div class="studio-dialog ai-setup"> <div class="studio-dialog ai-setup">
<p style="line-height:1.5; font-weight: lighter;"> <p style="line-height:1.5; font-weight: lighter;">
Setup system wide AI capability to translate a prompt to an SQL query. This feature is supported for the following technologies : Setup system wide AI capability to translate a prompt to an SQL query. This feature is supported for the following databases :
<ul> <ul>
<div style="margin:4px;"><i class="fa-solid fa-minus"></i> <span class="ai-tech bold"></span></div> <div style="margin:4px;"><i class="fa-solid fa-minus"></i> <span class="ai-tech bold"></span></div>
<div style="margin:4px;"><i class="fa-solid fa-minus"></i> <span> Ollama & AzureOpenAI</span></div> <div style="margin:4px;"><i class="fa-solid fa-minus"></i> <span> Ollama & AzureOpenAI (for now)</span></div>
</ul>
</ul>
<p>
Databases are whitelisted to enable the AI agent to interact with them
</p>
<ul> <ul>
<br> <br>
<p> <p>
<div class="border" style="align-items:center; width:34%; margin-left:33%; padding:8px; display:grid; grid-template-columns: auto 64px; gap:8px; background-color: #f3f3f3;"> <div class="border" style="align-items:center; width:34%; margin-left:33%; padding:8px; display:grid; grid-template-columns: auto 64px; gap:8px; background-color: #f3f3f3;">
<div> <div>
<i class="fa-solid fa-microchip"></i> <i class="fa-solid fa-microchip" style="color:#FF6500"></i>
<span class="bold">AI</span> <span class="bold">AI</span>
<span class="ai-status bold">Enabled</span> <span class="ai-status bold">Enabled</span>
</div> </div>
<div align="center" class="active ai-button" > <div class="border-left" style="display:grid;">
<span class="fa-solid fa-check" style="color:green"></span> <div align="center" class="active ai-button " >
<span class="fa-solid fa-check" style="color:green"></span>
</div>
</div> </div>
</div> </div>
</p> </p>

Loading…
Cancel
Save