From 4282a4277ae283a51fa67a81a2e8e11a2e7308bd Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 10 Oct 2025 18:40:05 +0530 Subject: [PATCH 001/446] SimpleChatToolCalling: Test/Explore srvr initial hs using cmdline --- .../public_simplechat/test-tools-cmdline.sh | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tools/server/public_simplechat/test-tools-cmdline.sh diff --git a/tools/server/public_simplechat/test-tools-cmdline.sh b/tools/server/public_simplechat/test-tools-cmdline.sh new file mode 100644 index 0000000000..6aa97753c9 --- /dev/null +++ b/tools/server/public_simplechat/test-tools-cmdline.sh @@ -0,0 +1,83 @@ +echo "DONT FORGET TO RUN llama-server" +echo "build/bin/llama-server -m ~/Downloads/GenAi.Text/gemma-3n-E4B-it-Q8_0.gguf --path tools/server/public_simplechat --jinja" +curl http://localhost:8080/v1/chat/completions -d '{ + "model": "gpt-3.5-turbo", + "tools": [ + { + "type":"function", + "function":{ + "name":"javascript", + "description":"Runs code in an javascript interpreter and returns the result of the execution after 60 seconds.", + "parameters":{ + "type":"object", + "properties":{ + "code":{ + "type":"string", + "description":"The code to run in the javascript interpreter." + } + }, + "required":["code"] + } + } + }, + { + "type":"function", + "function":{ + "name":"web_fetch", + "description":"Connects to the internet and fetches the specified url, may take few seconds", + "parameters":{ + "type":"object", + "properties":{ + "url":{ + "type":"string", + "description":"The url to fetch from internet." + } + }, + "required":["url"] + } + } + }, + { + "type":"function", + "function":{ + "name":"simple_calc", + "description":"Calculates the provided arithmatic expression using javascript interpreter and returns the result of the execution after few seconds.", + "parameters":{ + "type":"object", + "properties":{ + "arithexp":{ + "type":"string", + "description":"The arithmatic expression that will be calculated using javascript interpreter." + } + }, + "required":["arithexp"] + } + } + } + ], + "messages": [ + { + "role": "user", + "content": "Add 324 to todays temperature in celcius in kochi. Dont forget to get todays weather info about kochi so that the temperature is valid. Use a free weather info site which doesnt require any api keys to get the info" + } + ] +}' + + +exit + + + "content": "Print a hello world message with python." + "content": "Print a hello world message with javascript." + "content": "Calculate the sum of 5 and 27." + "content": "Can you get me todays date." + "content": "Can you get todays date. And inturn add 10 to todays date" + "content": "Who is known as father of the nation in India, also is there a similar figure for USA as well as UK" + "content": "Who is known as father of the nation in India, Add 10 to double his year of birth and show me the results." + "content": "How is the weather today in london." + "content": "How is the weather today in london. Add 324 to todays temperature in celcius in london" + "content": "How is the weather today in london. Add 324 to todays temperature in celcius in kochi" + "content": "Add 324 to todays temperature in celcius in london" + "content": "Add 324 to todays temperature in celcius in delhi. Dont forget to get todays weather info about delhi so that the temperature is valid" + "content": "Add 324 to todays temperature in celcius in mumbai. Dont forget to get todays weather info about mumbai so that the temperature is valid. Use a free weather info site which doesnt require any api keys to get the info" + "content": "Can you get the cutoff rank for all the deemed medical universities in India for UGNeet 25" From 9341c507f2d271ed78ccf2dbd2b7d1d12867fd8d Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 10 Oct 2025 20:18:16 +0530 Subject: [PATCH 002/446] SimpleChatTools: Add boolean to allow user control of tools use --- tools/server/public_simplechat/simplechat.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 2fcd24a860..bed0afbcdf 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -726,6 +726,7 @@ class Me { this.defaultChatIds = [ "Default", "Other" ]; this.multiChat = new MultiChatUI(); this.bStream = true; + this.bTools = false; this.bCompletionFreshChatAlways = true; this.bCompletionInsertStandardRolePrefix = false; this.bTrimGarbage = true; @@ -804,6 +805,8 @@ class Me { ui.el_create_append_p(`bStream:${this.bStream}`, elDiv); + ui.el_create_append_p(`bTools:${this.bTools}`, elDiv); + ui.el_create_append_p(`bTrimGarbage:${this.bTrimGarbage}`, elDiv); ui.el_create_append_p(`ApiEndPoint:${this.apiEP}`, elDiv); @@ -878,6 +881,11 @@ class Me { }); elDiv.appendChild(bb.div); + bb = ui.el_creatediv_boolbutton("SetTools", "Tools", {true: "[+] yes tools", false: "[-] no tools"}, this.bTools, (val)=>{ + this.bTools = val; + }); + elDiv.appendChild(bb.div); + bb = ui.el_creatediv_boolbutton("SetTrimGarbage", "TrimGarbage", {true: "[+] yes trim", false: "[-] dont trim"}, this.bTrimGarbage, (val)=>{ this.bTrimGarbage = val; }); From 48c9f07982f653458d6660d1d6bc29d358a60d88 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 10 Oct 2025 23:02:30 +0530 Subject: [PATCH 003/446] SimpleChatTC: Update test shell script a bit Enable streaming by default, to check the handshake before going on to change the code, given that havent looked into this for more than a year now and have been busy with totally different stuff. Also updated the user messages used for testing a bit --- tools/server/public_simplechat/test-tools-cmdline.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/test-tools-cmdline.sh b/tools/server/public_simplechat/test-tools-cmdline.sh index 6aa97753c9..e5fb7652f4 100644 --- a/tools/server/public_simplechat/test-tools-cmdline.sh +++ b/tools/server/public_simplechat/test-tools-cmdline.sh @@ -1,7 +1,10 @@ echo "DONT FORGET TO RUN llama-server" echo "build/bin/llama-server -m ~/Downloads/GenAi.Text/gemma-3n-E4B-it-Q8_0.gguf --path tools/server/public_simplechat --jinja" +echo "Note: Remove stream: true line below, if you want one shot instead of streaming response from ai server" +echo "Note: Using different locations below, as the mechanism / url used to fetch will / may need to change" curl http://localhost:8080/v1/chat/completions -d '{ "model": "gpt-3.5-turbo", + "stream": true, "tools": [ { "type":"function", @@ -58,7 +61,7 @@ curl http://localhost:8080/v1/chat/completions -d '{ "messages": [ { "role": "user", - "content": "Add 324 to todays temperature in celcius in kochi. Dont forget to get todays weather info about kochi so that the temperature is valid. Use a free weather info site which doesnt require any api keys to get the info" + "content": "what is your name." } ] }' @@ -67,6 +70,7 @@ curl http://localhost:8080/v1/chat/completions -d '{ exit + "content": "what is your name." "content": "Print a hello world message with python." "content": "Print a hello world message with javascript." "content": "Calculate the sum of 5 and 27." @@ -76,8 +80,9 @@ exit "content": "Who is known as father of the nation in India, Add 10 to double his year of birth and show me the results." "content": "How is the weather today in london." "content": "How is the weather today in london. Add 324 to todays temperature in celcius in london" - "content": "How is the weather today in london. Add 324 to todays temperature in celcius in kochi" + "content": "How is the weather today in bengaluru. Add 324 to todays temperature in celcius in kochi" "content": "Add 324 to todays temperature in celcius in london" + "content": "Add 324 to todays temperature in celcius in delhi" "content": "Add 324 to todays temperature in celcius in delhi. Dont forget to get todays weather info about delhi so that the temperature is valid" - "content": "Add 324 to todays temperature in celcius in mumbai. Dont forget to get todays weather info about mumbai so that the temperature is valid. Use a free weather info site which doesnt require any api keys to get the info" + "content": "Add 324 to todays temperature in celcius in bengaluru. Dont forget to get todays weather info about bengaluru so that the temperature is valid. Use a free weather info site which doesnt require any api keys to get the info" "content": "Can you get the cutoff rank for all the deemed medical universities in India for UGNeet 25" From f1aa0ee778182ee97ee1f41a40f506ae5ba156a2 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 10 Oct 2025 23:31:36 +0530 Subject: [PATCH 004/446] SimpleChatTC: Add skeleton for a javascript interpretor tool call Define the meta that needs to be passed to the GenAi Engine. Define the logic that implements the tool call, if called. Implement the flow/structure such that a single tool calls implementation file can define multiple tool calls. --- tools/server/public_simplechat/tooljs.mjs | 40 +++++++++++++++++++++++ tools/server/public_simplechat/tools.mjs | 7 ++++ 2 files changed, 47 insertions(+) create mode 100644 tools/server/public_simplechat/tooljs.mjs create mode 100644 tools/server/public_simplechat/tools.mjs diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs new file mode 100644 index 0000000000..cfbdc61e56 --- /dev/null +++ b/tools/server/public_simplechat/tooljs.mjs @@ -0,0 +1,40 @@ +//@ts-check +// Helpers to handle tools/functions calling +// by Humans for All +// + + +let metas = [ + { + "type":"function", + "function":{ + "name": "javascript", + "description":"Runs code in an javascript interpreter and returns the result of the execution after 60 seconds.", + "parameters":{ + "type":"object", + "properties":{ + "code":{ + "type":"string", + "description":"The code to run in the javascript interpreter." + } + }, + "required":["code"] + } + } + } +] + + +/** + * Implementation of the javascript interpretor logic. Minimal skeleton for now. + * @param {any} obj + */ +function tool_run(obj) { + let func = new Function(obj["code"]) + func() +} + +let tswitch = { + "javascript": tool_run, +} + diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs new file mode 100644 index 0000000000..6d0b375447 --- /dev/null +++ b/tools/server/public_simplechat/tools.mjs @@ -0,0 +1,7 @@ +//@ts-check +// Helpers to handle tools/functions calling +// by Humans for All +// + + +import * as tjs from './tooljs.mjs' \ No newline at end of file From 46f0304105fefdd5237a57b921d852a1560362b0 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 00:02:32 +0530 Subject: [PATCH 005/446] SimpleChatTC: More generic tooljs, SimpCalc, some main skeleton Make tooljs structure and flow more generic Add a simple_calculator tool/function call logic Add initial skeleton wrt the main tools.mjs file. --- tools/server/public_simplechat/tooljs.mjs | 77 ++++++++++++++++++----- tools/server/public_simplechat/tools.mjs | 26 +++++++- 2 files changed, 84 insertions(+), 19 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index cfbdc61e56..1796bfaa2b 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -1,40 +1,83 @@ //@ts-check -// Helpers to handle tools/functions calling +// DANGER DANGER DANGER - Simple and Stupid - Use from a discardable VM only +// Helpers to handle tools/functions calling wrt +// * javascript interpreter +// * simple arithmatic calculator // by Humans for All // -let metas = [ - { - "type":"function", - "function":{ +let js_meta = { + "type": "function", + "function": { "name": "javascript", - "description":"Runs code in an javascript interpreter and returns the result of the execution after 60 seconds.", - "parameters":{ - "type":"object", - "properties":{ - "code":{ - "type":"string", - "description":"The code to run in the javascript interpreter." + "description": "Runs code in an javascript interpreter and returns the result of the execution after few seconds", + "parameters": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "The code to run in the javascript interpreter." } }, - "required":["code"] + "required": ["code"] } } } -] /** * Implementation of the javascript interpretor logic. Minimal skeleton for now. + * ALERT: Has access to the javascript environment and can mess with it and beyond * @param {any} obj */ -function tool_run(obj) { +function js_run(obj) { let func = new Function(obj["code"]) func() } -let tswitch = { - "javascript": tool_run, + +let calc_meta = { + "type": "function", + "function": { + "name": "simple_calculator", + "description": "Calculates the provided arithmatic expression using javascript interpreter and returns the result of the execution after few seconds", + "parameters": { + "type": "object", + "properties": { + "arithexpr":{ + "type":"string", + "description":"The arithmatic expression that will be calculated using javascript interpreter." + } + }, + "required": ["arithexpr"] + } + } + } + + +/** + * Implementation of the simple calculator logic. Minimal skeleton for now. + * ALERT: Has access to the javascript environment and can mess with it and beyond + * @param {any} obj + */ +function calc_run(obj) { + let func = new Function(obj["arithexpr"]) + func() +} + + +/** + * @type {Object} + */ +export let tc_switch = { + "javascript": { + "handler": js_run, + "meta": js_meta + }, + "simple_calculator": { + "handler": calc_run, + "meta": calc_meta + } } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 6d0b375447..ba80e30a91 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -1,7 +1,29 @@ //@ts-check -// Helpers to handle tools/functions calling +// DANGER DANGER DANGER - Simple and Stupid - Use from a discardable VM only +// Helpers to handle tools/functions calling in a direct and dangerous way // by Humans for All // -import * as tjs from './tooljs.mjs' \ No newline at end of file +import * as tjs from './tooljs.mjs' + + +/** + * @type {Object} + */ +let tc_switch = {} + +export function setup() { + for (const key in tjs.tc_switch) { + tc_switch[key] = tjs.tc_switch[key] + } +} + +export function meta() { + let tools = [] + for (const key in tc_switch) { + tools.push(tc_switch[key]["meta"]) + } + return tools +} + From 6167cdff9fc628b9400899cec5f52581fe8c912c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 00:18:37 +0530 Subject: [PATCH 006/446] SimpleChatTC: Bring in the tools meta into the main flow --- tools/server/public_simplechat/simplechat.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index bed0afbcdf..6f33457e73 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -4,6 +4,8 @@ import * as du from "./datautils.mjs"; import * as ui from "./ui.mjs" +import * as tools from "./tools.mjs" + class Roles { static System = "system"; @@ -228,6 +230,9 @@ class SimpleChat { if (gMe.bStream) { obj["stream"] = true; } + if (gMe.bTools) { + obj["tools"] = tools.meta(); + } return JSON.stringify(obj); } @@ -927,6 +932,8 @@ function startme() { gMe.debug_disable(); document["gMe"] = gMe; document["du"] = du; + document["tools"] = tools; + tools.setup() for (let cid of gMe.defaultChatIds) { gMe.multiChat.new_chat_session(cid); } From 32f5278e8c7504eda4d3ad2066deec0d876811fe Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 01:06:23 +0530 Subject: [PATCH 007/446] SimpleChatTC: use tcpdump to dbg hs; check if ai aware of tools --- tools/server/public_simplechat/test-tools-cmdline.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/test-tools-cmdline.sh b/tools/server/public_simplechat/test-tools-cmdline.sh index e5fb7652f4..adea59cb6c 100644 --- a/tools/server/public_simplechat/test-tools-cmdline.sh +++ b/tools/server/public_simplechat/test-tools-cmdline.sh @@ -2,6 +2,7 @@ echo "DONT FORGET TO RUN llama-server" echo "build/bin/llama-server -m ~/Downloads/GenAi.Text/gemma-3n-E4B-it-Q8_0.gguf --path tools/server/public_simplechat --jinja" echo "Note: Remove stream: true line below, if you want one shot instead of streaming response from ai server" echo "Note: Using different locations below, as the mechanism / url used to fetch will / may need to change" +echo "Note: sudo tcpdump -i lo -s 0 -vvv -A host 127.0.0.1 and port 8080 | tee /tmp/td.log can be used to capture the hs" curl http://localhost:8080/v1/chat/completions -d '{ "model": "gpt-3.5-turbo", "stream": true, @@ -61,7 +62,7 @@ curl http://localhost:8080/v1/chat/completions -d '{ "messages": [ { "role": "user", - "content": "what is your name." + "content": "What and all tools you have access to" } ] }' @@ -71,6 +72,7 @@ exit "content": "what is your name." + "content": "What and all tools you have access to" "content": "Print a hello world message with python." "content": "Print a hello world message with javascript." "content": "Calculate the sum of 5 and 27." From bfe7ef69fa7609779ea9fab8a9b454567acfca7b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 01:17:19 +0530 Subject: [PATCH 008/446] SimpleChatTC: Skeleton to handle diff fields when streaming Changed latestResponse type to an object instead of a string. Inturn it contains entries for content, toolname and toolargs. Added a custom clear logic due to the same and used it to replace the previously simple assigning of empty string to latestResponse. For now in all places where latestReponse is used, I have replaced with latestReponse.content. Next need to handle identifying the field being streamed and inturn append to it. Also need to add logic to call tool, when tool_call triggered by genai. --- tools/server/public_simplechat/simplechat.js | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 6f33457e73..bc3d2f00f5 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -72,7 +72,7 @@ class SimpleChat { */ this.xchat = []; this.iLastSys = -1; - this.latestResponse = ""; + this.latestResponse = { content: "", toolname: "", toolargs: "" }; } clear() { @@ -80,6 +80,10 @@ class SimpleChat { this.iLastSys = -1; } + clear_latestresponse() { + this.latestResponse = { content: "", toolname: "", toolargs: "" }; + } + ods_key() { return `SimpleChat-${this.chatId}` } @@ -151,7 +155,7 @@ class SimpleChat { * @param {string} content */ append_response(content) { - this.latestResponse += content; + this.latestResponse.content += content; } /** @@ -392,7 +396,7 @@ class SimpleChat { } let tdUtf8 = new TextDecoder("utf-8"); let rr = resp.body.getReader(); - this.latestResponse = ""; + this.clear_latestresponse() let xLines = new du.NewLines(); while(true) { let { value: cur, done: done } = await rr.read(); @@ -419,14 +423,14 @@ class SimpleChat { console.debug("DBUG:SC:PART:Json:", curJson); this.append_response(this.response_extract_stream(curJson, apiEP)); } - elP.innerText = this.latestResponse; + elP.innerText = this.latestResponse.content; elP.scrollIntoView(false); if (done) { break; } } - console.debug("DBUG:SC:PART:Full:", this.latestResponse); - return this.latestResponse; + console.debug("DBUG:SC:PART:Full:", this.latestResponse.content); + return this.latestResponse.content; } /** @@ -455,11 +459,11 @@ class SimpleChat { if (gMe.bStream) { try { theResp.assistant = await this.handle_response_multipart(resp, apiEP, elDiv); - this.latestResponse = ""; + this.clear_latestresponse() } catch (error) { - theResp.assistant = this.latestResponse; + theResp.assistant = this.latestResponse.content; this.add(Roles.Assistant, theResp.assistant); - this.latestResponse = ""; + this.clear_latestresponse() throw error; } } else { From 63430dc9f79273d152b78ba9cdbe4eda5f00efb5 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 01:46:23 +0530 Subject: [PATCH 009/446] SimpleChatTC: Extract streamed field - assume only 1f at any time Update response_extract_stream to check for which field is being currently streamed ie is it normal content or tool call func name or tool call func args and then return the field name and extracted value. Previously it was always assumed that only normal content will be returned. Currently it is assumed that the server will only stream one of the 3 supported fields at any time and not more than one of them at the same time. TODO: Have to also add logic to extract the reasoning field later, ie wrt gen ai models which give out their thinking. Have updated append_response to expect both the key and the value wrt the latestResponse object, which it will be manipualted. Previously it was always assumed that content is what will be got and inturn appended. --- tools/server/public_simplechat/simplechat.js | 27 +++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index bc3d2f00f5..5adae09971 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -152,10 +152,10 @@ class SimpleChat { /** * Collate the latest response from the server/ai-model, as it is becoming available. * This is mainly useful for the stream mode. - * @param {string} content + * @param {{key: string, value: string}} resp */ - append_response(content) { - this.latestResponse.content += content; + append_response(resp) { + this.latestResponse[resp.key] += resp.value; } /** @@ -311,10 +311,25 @@ class SimpleChat { * @param {string} apiEP */ response_extract_stream(respBody, apiEP) { + let key = "content" let assistant = ""; if (apiEP == ApiEP.Type.Chat) { - if (respBody["choices"][0]["finish_reason"] !== "stop") { - assistant = respBody["choices"][0]["delta"]["content"]; + if (respBody["choices"][0]["finish_reason"] !== null) { + if (respBody["choices"][0]["delta"]["content"] !== undefined) { + assistant = respBody["choices"][0]["delta"]["content"]; + } else { + if (respBody["choices"][0]["delta"]["tool_calls"] !== undefined) { + if (respBody["choices"][0]["delta"]["tool_calls"][0]["function"]["name"] !== undefined) { + key = "toolname"; + assistant = respBody["choices"][0]["delta"]["tool_calls"][0]["function"]["name"]; + } else { + if (respBody["choices"][0]["delta"]["tool_calls"][0]["function"]["arguments"] !== undefined) { + key = "toolargs"; + assistant = respBody["choices"][0]["delta"]["tool_calls"][0]["function"]["arguments"]; + } + } + } + } } } else { try { @@ -323,7 +338,7 @@ class SimpleChat { assistant = respBody["content"]; } } - return assistant; + return { key: key, value: assistant }; } /** From e73bc4550b629329a9e0a1b7265141a0ced587a7 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 02:35:01 +0530 Subject: [PATCH 010/446] SimpleChatTC: Avoid null content, Fix oversight wrt finish_reason I was wrongly checking for finish_reason to be non null, before trying to extract the genai content/toolcalls, have fixed this oversight with the new flow in progress. I had added few debug logs to identify the above issue, need to remove them later. Note: given that debug logs are disabled by replacing the debug function during this program's initialisation, which I had forgotten about, I didnt get the debug messages and had to scratch my head a bit, before realising this and the other issue ;) Also either when I had originally implemented simplechat 1+ years back, or later due to changes on the server end, the streaming flow sends a initial null wrt the content, where it only sets the role. This was not handled in my flow on the client side, so a null was getting prepended to the chat messages/responses from the server. This has been fixed now in the new generic flow. --- tools/server/public_simplechat/simplechat.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 5adae09971..cd075f37bd 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -155,6 +155,10 @@ class SimpleChat { * @param {{key: string, value: string}} resp */ append_response(resp) { + if (resp.value == null) { + return + } + console.debug(resp.key, resp.value) this.latestResponse[resp.key] += resp.value; } @@ -311,10 +315,11 @@ class SimpleChat { * @param {string} apiEP */ response_extract_stream(respBody, apiEP) { + console.debug(respBody, apiEP) let key = "content" let assistant = ""; if (apiEP == ApiEP.Type.Chat) { - if (respBody["choices"][0]["finish_reason"] !== null) { + if (respBody["choices"][0]["finish_reason"] === null) { if (respBody["choices"][0]["delta"]["content"] !== undefined) { assistant = respBody["choices"][0]["delta"]["content"]; } else { From 5a26831ad20afbdaabcb090bd19f939ec0264322 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 02:58:03 +0530 Subject: [PATCH 011/446] SimpleChatTC: Show toolcall being generated by ai - Temp --- tools/server/public_simplechat/simplechat.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index cd075f37bd..4fefe48ea4 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -443,7 +443,11 @@ class SimpleChat { console.debug("DBUG:SC:PART:Json:", curJson); this.append_response(this.response_extract_stream(curJson, apiEP)); } - elP.innerText = this.latestResponse.content; + if (this.latestResponse.content !== "") { + elP.innerText = this.latestResponse.content; + } else { + elP.innerText = `ToolCall:${this.latestResponse.toolname}:${this.latestResponse.toolargs}`; + } elP.scrollIntoView(false); if (done) { break; From 3f3aa8d043be432125ed02cf817e08abdffbc1b9 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 03:14:27 +0530 Subject: [PATCH 012/446] SimpleChatTC: AssistantResponse class initial go Make latestResponse into a new class based type instance wrt ai assistant response, which is what it represents. Move clearing, appending fields' values and getting assistant's response info (irrespective of a content or toolcall response) into this new class and inturn use the same. --- tools/server/public_simplechat/simplechat.js | 70 ++++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 4fefe48ea4..510749c17c 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -37,6 +37,39 @@ class ApiEP { } +class AssistantResponse { + + constructor() { + this.response = { content: "", toolname: "", toolargs: "" }; + } + + clear() { + this.response = { content: "", toolname: "", toolargs: "" }; + } + + /** + * Helps collate the latest response from the server/ai-model, as it is becoming available. + * This is mainly useful for the stream mode. + * @param {{key: string, value: string}} resp + */ + append_response(resp) { + if (resp.value == null) { + return + } + console.debug(resp.key, resp.value) + this.response[resp.key] += resp.value; + } + + content_equiv() { + if (this.response.content !== "") { + return this.response.content; + } else { + return `ToolCall:${this.response.toolname}:${this.response.toolargs}`; + } + } + +} + let gUsageMsg = `

Usage

@@ -72,7 +105,7 @@ class SimpleChat { */ this.xchat = []; this.iLastSys = -1; - this.latestResponse = { content: "", toolname: "", toolargs: "" }; + this.latestResponse = new AssistantResponse(); } clear() { @@ -80,10 +113,6 @@ class SimpleChat { this.iLastSys = -1; } - clear_latestresponse() { - this.latestResponse = { content: "", toolname: "", toolargs: "" }; - } - ods_key() { return `SimpleChat-${this.chatId}` } @@ -149,19 +178,6 @@ class SimpleChat { return rchat; } - /** - * Collate the latest response from the server/ai-model, as it is becoming available. - * This is mainly useful for the stream mode. - * @param {{key: string, value: string}} resp - */ - append_response(resp) { - if (resp.value == null) { - return - } - console.debug(resp.key, resp.value) - this.latestResponse[resp.key] += resp.value; - } - /** * Add an entry into xchat * @param {string} role @@ -416,7 +432,7 @@ class SimpleChat { } let tdUtf8 = new TextDecoder("utf-8"); let rr = resp.body.getReader(); - this.clear_latestresponse() + this.latestResponse.clear() let xLines = new du.NewLines(); while(true) { let { value: cur, done: done } = await rr.read(); @@ -441,20 +457,16 @@ class SimpleChat { } let curJson = JSON.parse(curLine); console.debug("DBUG:SC:PART:Json:", curJson); - this.append_response(this.response_extract_stream(curJson, apiEP)); - } - if (this.latestResponse.content !== "") { - elP.innerText = this.latestResponse.content; - } else { - elP.innerText = `ToolCall:${this.latestResponse.toolname}:${this.latestResponse.toolargs}`; + this.latestResponse.append_response(this.response_extract_stream(curJson, apiEP)); } + elP.innerText = this.latestResponse.content_equiv() elP.scrollIntoView(false); if (done) { break; } } - console.debug("DBUG:SC:PART:Full:", this.latestResponse.content); - return this.latestResponse.content; + console.debug("DBUG:SC:PART:Full:", this.latestResponse.content_equiv()); + return this.latestResponse; } /** @@ -483,11 +495,11 @@ class SimpleChat { if (gMe.bStream) { try { theResp.assistant = await this.handle_response_multipart(resp, apiEP, elDiv); - this.clear_latestresponse() + this.latestResponse.clear() } catch (error) { theResp.assistant = this.latestResponse.content; this.add(Roles.Assistant, theResp.assistant); - this.clear_latestresponse() + this.latestResponse.clear() throw error; } } else { From 53f85d09befcc5e5507198a7535a70413e6fb7fa Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 03:31:18 +0530 Subject: [PATCH 013/446] SimpleChatTC: AssistantResponse everywhere initial go Switch oneshot handler to use AssistantResponse, inturn currenlty only handle the normal content in the response. TODO: If any tool_calls in the oneshot response, it is currently not handled. Inturn switch the generic/toplevel handle response logic to use AssistantResponse class, given that both oneshot and the multipart/streaming flows use/return it. Inturn add trimmedContent member to AssistantResponse class and make the generic handle response logic to save the trimmed content into this. Update users of trimmed to work with this structure. --- tools/server/public_simplechat/simplechat.js | 39 ++++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 510749c17c..c38c740efe 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -40,11 +40,11 @@ class ApiEP { class AssistantResponse { constructor() { - this.response = { content: "", toolname: "", toolargs: "" }; + this.response = { content: "", toolname: "", toolargs: "", trimmedContent: "" }; } clear() { - this.response = { content: "", toolname: "", toolargs: "" }; + this.response = { content: "", toolname: "", toolargs: "", trimmedContent: "" }; } /** @@ -312,14 +312,14 @@ class SimpleChat { * @param {string} apiEP */ response_extract(respBody, apiEP) { - let assistant = ""; + let assistant = new AssistantResponse(); if (apiEP == ApiEP.Type.Chat) { - assistant = respBody["choices"][0]["message"]["content"]; + assistant.response.content = respBody["choices"][0]["message"]["content"]; } else { try { - assistant = respBody["choices"][0]["text"]; + assistant.response.content = respBody["choices"][0]["text"]; } catch { - assistant = respBody["content"]; + assistant.response.content = respBody["content"]; } } return assistant; @@ -483,34 +483,33 @@ class SimpleChat { /** * Handle the response from the server be it in oneshot or multipart/stream mode. * Also take care of the optional garbage trimming. + * TODO: Need to handle tool calling and related flow, including how to show + * the assistant's request for tool calling and the response from tool. * @param {Response} resp * @param {string} apiEP * @param {HTMLDivElement} elDiv */ async handle_response(resp, apiEP, elDiv) { - let theResp = { - assistant: "", - trimmed: "", - } + let theResp = null if (gMe.bStream) { try { - theResp.assistant = await this.handle_response_multipart(resp, apiEP, elDiv); + theResp = await this.handle_response_multipart(resp, apiEP, elDiv); this.latestResponse.clear() } catch (error) { - theResp.assistant = this.latestResponse.content; - this.add(Roles.Assistant, theResp.assistant); + theResp = this.latestResponse; + this.add(Roles.Assistant, theResp.content_equiv()); this.latestResponse.clear() throw error; } } else { - theResp.assistant = await this.handle_response_oneshot(resp, apiEP); + theResp = await this.handle_response_oneshot(resp, apiEP); } if (gMe.bTrimGarbage) { - let origMsg = theResp.assistant; - theResp.assistant = du.trim_garbage_at_end(origMsg); - theResp.trimmed = origMsg.substring(theResp.assistant.length); + let origMsg = theResp.response.content; + theResp.response.content = du.trim_garbage_at_end(origMsg); + theResp.response.trimmedContent = origMsg.substring(theResp.response.content.length); } - this.add(Roles.Assistant, theResp.assistant); + this.add(Roles.Assistant, theResp.content_equiv()); return theResp; } @@ -678,8 +677,8 @@ class MultiChatUI { let theResp = await chat.handle_response(resp, apiEP, this.elDivChat); if (chatId == this.curChatId) { chat.show(this.elDivChat); - if (theResp.trimmed.length > 0) { - let p = ui.el_create_append_p(`TRIMMED:${theResp.trimmed}`, this.elDivChat); + if (theResp.response.trimmedContent.length > 0) { + let p = ui.el_create_append_p(`TRIMMED:${theResp.response.trimmedContent}`, this.elDivChat); p.className="role-trim"; } } else { From 383c19c99b093bba5d99a962afd05ea1d46b83e3 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 04:03:53 +0530 Subject: [PATCH 014/446] SimpleChatTC: twins wrt streamed response handling As there could be failure wrt getting the response from the ai server some where in between a long response spread over multiple parts, the logic uses the latestResponse to cache the response as it is being received. However once the full response is got, one needs to transfer it to a new instance of AssistantResponse class, so that latestResponse can be cleared, while the new instance can be used in other locations in the flow as needed. Achieve the same now. --- tools/server/public_simplechat/simplechat.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index c38c740efe..7cce5b5f5f 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -39,8 +39,16 @@ class ApiEP { class AssistantResponse { - constructor() { - this.response = { content: "", toolname: "", toolargs: "", trimmedContent: "" }; + constructor(content="", toolname="", toolargs="", trimmedContent="") { + this.response = { content: content, toolname: toolname, toolargs: toolargs, trimmedContent: trimmedContent }; + } + + /** + * Create a new instance from an existing instance + * @param {AssistantResponse} old + */ + static newFrom(old) { + return new AssistantResponse(old.response.content, old.response.toolname, old.response.toolargs, old.response.trimmedContent) } clear() { @@ -466,7 +474,7 @@ class SimpleChat { } } console.debug("DBUG:SC:PART:Full:", this.latestResponse.content_equiv()); - return this.latestResponse; + return AssistantResponse.newFrom(this.latestResponse); } /** From 6d430110034cdb6e506adfb934b1f13df4b83469 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 04:22:15 +0530 Subject: [PATCH 015/446] SimpleChatTC: Saner/Robust AssistantResponse content_equiv Previously if content was empty, it would have always sent the toolcall info related version even if there was no toolcall info in it. Fixed now to return empty string, if both content and toolname are empty. --- tools/server/public_simplechat/simplechat.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 7cce5b5f5f..8c9fa56698 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -71,8 +71,10 @@ class AssistantResponse { content_equiv() { if (this.response.content !== "") { return this.response.content; - } else { + } else if (this.response.toolname !== "") { return `ToolCall:${this.response.toolname}:${this.response.toolargs}`; + } else { + return "" } } From fa63a86c71e2d94da039dde65bec2c8a7d73ce33 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 11 Oct 2025 23:27:08 +0530 Subject: [PATCH 016/446] SimpleChatTC:tooljs: Trap console.log and store in new result key The implementations of javascript and simple_calculator now use provided helpers to trap console.log messages when they execute the code / expression provided by GenAi and inturn store the captured log messages in the newly added result key in tc_switch This should help trap the output generated by the provided code or expression as the case maybe and inturn return the same to the GenAi, for its further processing. --- tools/server/public_simplechat/tooljs.mjs | 48 +++++++++++++++++++++-- tools/server/public_simplechat/tools.mjs | 2 +- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 1796bfaa2b..628563ebd1 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -7,6 +7,40 @@ // +let gConsoleStr = "" +/** + * @type { {(...data: any[]): void} | null} + */ +let gOrigConsoleLog = null + + +/** + * @param {any[]} args + */ +function console_trapped(...args) { + let res = args.map((arg)=>{ + if (typeof arg == 'object') { + return JSON.stringify(arg); + } else { + return String(arg); + } + }).join(' '); + gConsoleStr += res; +} + +function console_redir() { + gOrigConsoleLog = console.log + console.log = console_trapped + gConsoleStr = "" +} + +function console_revert() { + if (gOrigConsoleLog !== null) { + console.log = gOrigConsoleLog + } +} + + let js_meta = { "type": "function", "function": { @@ -32,8 +66,11 @@ let js_meta = { * @param {any} obj */ function js_run(obj) { + console_redir() let func = new Function(obj["code"]) func() + console_revert() + tc_switch["javascript"]["result"] = gConsoleStr } @@ -62,22 +99,27 @@ let calc_meta = { * @param {any} obj */ function calc_run(obj) { + console_redir() let func = new Function(obj["arithexpr"]) func() + console_revert() + tc_switch["simple_calculator"]["result"] = gConsoleStr } /** - * @type {Object} + * @type {Object>} */ export let tc_switch = { "javascript": { "handler": js_run, - "meta": js_meta + "meta": js_meta, + "result": "" }, "simple_calculator": { "handler": calc_run, - "meta": calc_meta + "meta": calc_meta, + "result": "" } } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index ba80e30a91..d249a3f543 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -9,7 +9,7 @@ import * as tjs from './tooljs.mjs' /** - * @type {Object} + * @type {Object>} */ let tc_switch = {} From 301910c3a12761cb533e265c6a22b68a1ce809ac Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 12 Oct 2025 01:20:53 +0530 Subject: [PATCH 017/446] SimpleChatTC: Implement a simple toolcall handling flow Checks for toolname to be defined or not in the GenAi's response If toolname is set, then check if a corresponding tool/func exists, and if so call the same by passing it the GenAi provided toolargs as a object. Inturn the text generated by the tool/func is captured and put into the user input entry text box, with tool_response tag around it. --- tools/server/public_simplechat/simplechat.js | 24 ++++++++++++++++++++ tools/server/public_simplechat/tools.mjs | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 8c9fa56698..f0156e007f 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -190,6 +190,7 @@ class SimpleChat { /** * Add an entry into xchat + * Also update iLastSys system prompt index tracker * @param {string} role * @param {string|undefined|null} content */ @@ -398,6 +399,7 @@ class SimpleChat { /** * Allow setting of system prompt, at any time. + * Updates the system prompt, if one was never set or if the newly passed is different from the last set system prompt. * @param {string} sysPrompt * @param {string} msgTag */ @@ -523,6 +525,24 @@ class SimpleChat { return theResp; } + /** + * Call the requested tool/function and get its response + * @param {AssistantResponse} ar + */ + async handle_toolcall(ar) { + let toolname = ar.response.toolname.trim(); + if (toolname === "") { + return undefined + } + for (const fn in tools.tc_switch) { + if (fn == toolname) { + tools.tc_switch[fn]["handler"](JSON.parse(ar.response.toolargs)) + return tools.tc_switch[fn]["result"] + } + } + return `Unknown Tool/Function Call:${toolname}` + } + } @@ -694,6 +714,10 @@ class MultiChatUI { } else { console.debug(`DBUG:SimpleChat:MCUI:HandleUserSubmit:ChatId has changed:[${chatId}] [${this.curChatId}]`); } + let toolResult = await chat.handle_toolcall(theResp) + if (toolResult !== undefined) { + this.elInUser.value = `${toolResult}` + } this.ui_reset_userinput(); } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index d249a3f543..adf87fbdf4 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -11,7 +11,7 @@ import * as tjs from './tooljs.mjs' /** * @type {Object>} */ -let tc_switch = {} +export let tc_switch = {} export function setup() { for (const key in tjs.tc_switch) { From 17c5daa52c332d4a12c81d7569fbfce1f01222d2 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 12 Oct 2025 01:36:43 +0530 Subject: [PATCH 018/446] SimpleChatTC: Cleanup initial/1st go toolcall flow As output generated by any tool/function call is currently placed into the TextArea provided for End user (for their queries), bcas the GenAi (engine/LLM) may be expecting the tool response to be sent as a user role data with tool_response tag surrounding the results from the tool call. So also now at the end of submit btn click handling, the end user input text area is not cleared, if there was a tool call handled, for above reasons. Also given that running a simple arithmatic expression in itself doesnt generate any output, so wrap them in a console.log, to help capture the result using the console.log trapping flow that is already setup. --- tools/server/public_simplechat/simplechat.js | 11 +++++++---- tools/server/public_simplechat/tooljs.mjs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index f0156e007f..618bb95679 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -586,12 +586,15 @@ class MultiChatUI { /** * Reset user input ui. - * * clear user input + * * clear user input (if requested, default true) * * enable user input * * set focus to user input + * @param {boolean} [bClearElInUser=true] */ - ui_reset_userinput() { - this.elInUser.value = ""; + ui_reset_userinput(bClearElInUser=true) { + if (bClearElInUser) { + this.elInUser.value = ""; + } this.elInUser.disabled = false; this.elInUser.focus(); } @@ -718,7 +721,7 @@ class MultiChatUI { if (toolResult !== undefined) { this.elInUser.value = `${toolResult}` } - this.ui_reset_userinput(); + this.ui_reset_userinput(toolResult === undefined); } /** diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 628563ebd1..ae62fd0176 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -100,7 +100,7 @@ let calc_meta = { */ function calc_run(obj) { console_redir() - let func = new Function(obj["arithexpr"]) + let func = new Function(`console.log(${obj["arithexpr"]})`) func() console_revert() tc_switch["simple_calculator"]["result"] = gConsoleStr From b4776da67097b8faf84357d6e8d7ca575cbb6ab5 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 12 Oct 2025 02:53:08 +0530 Subject: [PATCH 019/446] SimpleChatTC: Trap any exception raised during tool call and inform the GenAi/LLM about the same --- tools/server/public_simplechat/simplechat.js | 8 ++++++-- tools/server/public_simplechat/test-tools-cmdline.sh | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 618bb95679..36cd5a5937 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -536,8 +536,12 @@ class SimpleChat { } for (const fn in tools.tc_switch) { if (fn == toolname) { - tools.tc_switch[fn]["handler"](JSON.parse(ar.response.toolargs)) - return tools.tc_switch[fn]["result"] + try { + tools.tc_switch[fn]["handler"](JSON.parse(ar.response.toolargs)) + return tools.tc_switch[fn]["result"] + } catch (error) { + return `Tool/Function call raised an exception:${error.name}:${error.message}` + } } } return `Unknown Tool/Function Call:${toolname}` diff --git a/tools/server/public_simplechat/test-tools-cmdline.sh b/tools/server/public_simplechat/test-tools-cmdline.sh index adea59cb6c..8fc62d2af9 100644 --- a/tools/server/public_simplechat/test-tools-cmdline.sh +++ b/tools/server/public_simplechat/test-tools-cmdline.sh @@ -73,10 +73,12 @@ exit "content": "what is your name." "content": "What and all tools you have access to" + "content": "do you have access to any tools" "content": "Print a hello world message with python." "content": "Print a hello world message with javascript." "content": "Calculate the sum of 5 and 27." "content": "Can you get me todays date." + "content": "Can you get me a summary of latest news from bbc world" "content": "Can you get todays date. And inturn add 10 to todays date" "content": "Who is known as father of the nation in India, also is there a similar figure for USA as well as UK" "content": "Who is known as father of the nation in India, Add 10 to double his year of birth and show me the results." From a408e5e017c5525368f98ae0b831e490952c7084 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 12 Oct 2025 12:58:37 +0530 Subject: [PATCH 020/446] SimpleChatTC: More clearer description of toolcalls execution env Should hopeful ensure that the GenAi/LLM will generate appropriate code/expression as the argument to pass to these tool calls, to some extent. --- tools/server/public_simplechat/tooljs.mjs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index ae62fd0176..c2bbc0c43c 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -44,14 +44,14 @@ function console_revert() { let js_meta = { "type": "function", "function": { - "name": "javascript", - "description": "Runs code in an javascript interpreter and returns the result of the execution after few seconds", + "name": "run_javascript_function_code", + "description": "Runs given code as a function in a browser's javascript environment and returns the console.log outputs of the execution after few seconds", "parameters": { "type": "object", "properties": { "code": { "type": "string", - "description": "The code to run in the javascript interpreter." + "description": "The code belonging to a function to run in the browser's javascript interpreter." } }, "required": ["code"] @@ -78,13 +78,13 @@ let calc_meta = { "type": "function", "function": { "name": "simple_calculator", - "description": "Calculates the provided arithmatic expression using javascript interpreter and returns the result of the execution after few seconds", + "description": "Calculates the provided arithmatic expression using console.log of a browser's javascript interpreter and returns the output of the execution once it is done in few seconds", "parameters": { "type": "object", "properties": { "arithexpr":{ "type":"string", - "description":"The arithmatic expression that will be calculated using javascript interpreter." + "description":"The arithmatic expression that will be calculated by passing it to console.log of a browser's javascript interpreter." } }, "required": ["arithexpr"] @@ -111,7 +111,7 @@ function calc_run(obj) { * @type {Object>} */ export let tc_switch = { - "javascript": { + "run_javascript_function_code": { "handler": js_run, "meta": js_meta, "result": "" From ef85ed41d44f9011bb3b0c5e7d0c094d7ed151fb Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 12 Oct 2025 13:01:59 +0530 Subject: [PATCH 021/446] SimpleChatTC: Clarify some type definitions to avoid warnings ie in vs code with ts-check --- tools/server/public_simplechat/simplechat.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 36cd5a5937..e39d9245ed 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -18,6 +18,7 @@ class ApiEP { Chat: "chat", Completion: "completion", } + /** @type {Object} */ static UrlSuffix = { 'chat': `/chat/completions`, 'completion': `/completions`, @@ -40,6 +41,7 @@ class ApiEP { class AssistantResponse { constructor(content="", toolname="", toolargs="", trimmedContent="") { + /** @type {Object} */ this.response = { content: content, toolname: toolname, toolargs: toolargs, trimmedContent: trimmedContent }; } @@ -539,7 +541,7 @@ class SimpleChat { try { tools.tc_switch[fn]["handler"](JSON.parse(ar.response.toolargs)) return tools.tc_switch[fn]["result"] - } catch (error) { + } catch (/** @type {any} */error) { return `Tool/Function call raised an exception:${error.name}:${error.message}` } } @@ -824,11 +826,15 @@ class Me { "Last4": 5, }; this.apiEP = ApiEP.Type.Chat; + /** @type {Object} */ this.headers = { "Content-Type": "application/json", "Authorization": "", // Authorization: Bearer OPENAI_API_KEY } - // Add needed fields wrt json object to be sent wrt LLM web services completions endpoint. + /** + * Add needed fields wrt json object to be sent wrt LLM web services completions endpoint. + * @type {Object} + */ this.apiRequestOptions = { "model": "gpt-3.5-turbo", "temperature": 0.7, From f7284a8b89d1108138a5957687799adf956b3c3f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 12 Oct 2025 16:08:11 +0530 Subject: [PATCH 022/446] SimpleChatTC: Move tool calling to tools, try trap async failures Move tool calling logic into tools module. Try trap async promise failures by awaiting results of tool calling and putting full thing in an outer try catch. Have forgotten the nitty gritties of JS flow, this might help, need to check. --- tools/server/public_simplechat/simplechat.js | 14 ++++---------- tools/server/public_simplechat/tools.mjs | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index e39d9245ed..4e243db2f0 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -536,17 +536,11 @@ class SimpleChat { if (toolname === "") { return undefined } - for (const fn in tools.tc_switch) { - if (fn == toolname) { - try { - tools.tc_switch[fn]["handler"](JSON.parse(ar.response.toolargs)) - return tools.tc_switch[fn]["result"] - } catch (/** @type {any} */error) { - return `Tool/Function call raised an exception:${error.name}:${error.message}` - } - } + try { + return await tools.tool_call(toolname, ar.response.toolargs) + } catch (/** @type {any} */error) { + return `Tool/Function call raised an exception:${error.name}:${error.message}` } - return `Unknown Tool/Function Call:${toolname}` } } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index adf87fbdf4..686d47a241 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -27,3 +27,22 @@ export function meta() { return tools } + +/** + * Try call the specified tool/function call and return its response + * @param {string} toolname + * @param {string} toolargs + */ +export async function tool_call(toolname, toolargs) { + for (const fn in tc_switch) { + if (fn == toolname) { + try { + tc_switch[fn]["handler"](JSON.parse(toolargs)) + return tc_switch[fn]["result"] + } catch (/** @type {any} */error) { + return `Tool/Function call raised an exception:${error.name}:${error.message}` + } + } + } + return `Unknown Tool/Function Call:${toolname}` +} From a80da9a652674496a16df32027910f5cc51b28fa Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 12 Oct 2025 17:19:51 +0530 Subject: [PATCH 023/446] SimpleChatTC: Pass toolname to the tool handler So that when tool handler writes the result to the tc_switch, it can make use of the same, to write to the right location. NOTE: This also fixes the issue with I forgetting to rename the key in js_run wrt writing of result. --- tools/server/public_simplechat/tooljs.mjs | 10 ++++++---- tools/server/public_simplechat/tools.mjs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index c2bbc0c43c..dfaab85009 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -63,14 +63,15 @@ let js_meta = { /** * Implementation of the javascript interpretor logic. Minimal skeleton for now. * ALERT: Has access to the javascript environment and can mess with it and beyond + * @param {string} toolname * @param {any} obj */ -function js_run(obj) { +function js_run(toolname, obj) { console_redir() let func = new Function(obj["code"]) func() console_revert() - tc_switch["javascript"]["result"] = gConsoleStr + tc_switch[toolname]["result"] = gConsoleStr } @@ -96,14 +97,15 @@ let calc_meta = { /** * Implementation of the simple calculator logic. Minimal skeleton for now. * ALERT: Has access to the javascript environment and can mess with it and beyond + * @param {string} toolname * @param {any} obj */ -function calc_run(obj) { +function calc_run(toolname, obj) { console_redir() let func = new Function(`console.log(${obj["arithexpr"]})`) func() console_revert() - tc_switch["simple_calculator"]["result"] = gConsoleStr + tc_switch[toolname]["result"] = gConsoleStr } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 686d47a241..d75d3eb7bf 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -37,7 +37,7 @@ export async function tool_call(toolname, toolargs) { for (const fn in tc_switch) { if (fn == toolname) { try { - tc_switch[fn]["handler"](JSON.parse(toolargs)) + tc_switch[fn]["handler"](fn, JSON.parse(toolargs)) return tc_switch[fn]["result"] } catch (/** @type {any} */error) { return `Tool/Function call raised an exception:${error.name}:${error.message}` From 63b5c6d76d494b7baa9903b047d490e7f49163fd Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 12 Oct 2025 21:21:00 +0530 Subject: [PATCH 024/446] SimpleChatTC: Cleanup the function description a bit to better describe how it will be run, so that genai/llm while creating the code to run, will hopefully take care of any naunces required. --- tools/server/public_simplechat/tooljs.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index dfaab85009..b79b79dd82 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -45,13 +45,13 @@ let js_meta = { "type": "function", "function": { "name": "run_javascript_function_code", - "description": "Runs given code as a function in a browser's javascript environment and returns the console.log outputs of the execution after few seconds", + "description": "Runs given code using function constructor mechanism in a browser's javascript environment and returns the console.log outputs of the execution after few seconds", "parameters": { "type": "object", "properties": { "code": { "type": "string", - "description": "The code belonging to a function to run in the browser's javascript interpreter." + "description": "The code belonging to the dynamic function to run in the browser's javascript interpreter environment." } }, "required": ["code"] From 30aa2f4c6b1206600da5cb949aaaf0c586532704 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 12 Oct 2025 22:16:34 +0530 Subject: [PATCH 025/446] SimpleChatTC: Update the readme.md wrt tool calling a bit --- tools/server/public_simplechat/readme.md | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 24e026d455..388202156e 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -78,6 +78,7 @@ Once inside * try trim garbage in response or not * amount of chat history in the context sent to server/ai-model * oneshot or streamed mode. + * use built in tool calling or not * In completion mode * one normally doesnt use a system prompt in completion mode. @@ -116,6 +117,13 @@ Once inside * the user input box will be disabled and a working message will be shown in it. * if trim garbage is enabled, the logic will try to trim repeating text kind of garbage to some extent. +* tool calling flow + * if tool calling is enabled and the user query results in need for one of the builtin tools to be + called, then the response will include request for tool call. + * the SimpleChat client will call the requested tool and inturn place the returned result into user + entry text area with generated result + * if user is ok with the tool response, they can click submit to send the same to the GenAi/LLM. + * just refresh the page, to reset wrt the chat history and or system prompt and start afresh. * Using NewChat one can start independent chat sessions. @@ -158,6 +166,15 @@ It is attached to the document object. Some of these can also be updated using t inturn the machine goes into power saving mode or so, the platform may stop network connection, leading to exception. + bTools - control whether tool calling is enabled or not + + remember to enable this only for GenAi/LLM models which support tool/function calling. + + the builtin tools meta data is sent to the ai model in the requests sent to it. + + inturn if the ai model requests a tool call to be made, the same will be done and the response + sent back to the ai model, under user control. + apiEP - select between /completions and /chat/completions endpoint provided by the server/ai-model. bCompletionFreshChatAlways - whether Completion mode collates complete/sliding-window history when @@ -281,6 +298,27 @@ NOTE: Not tested, as there is no free tier api testing available. However logica work. +### Tool Calling + +Provide a descriptive meta data explaining the tool / function being provided for tool calling. + +Provide a handler which should implement the specified tool / function call. It should place +the result to be sent back to the ai model in the result key of the tc_switch entry for the +corresponding tool. + +Update the tc_switch to include a object entry for the tool, which inturn icnludes +* the meta data as well as +* a reference to the handler and also +* the result key + + +### Debuging the handshake + +When working with llama.cpp server based GenAi/LLM running locally + +sudo tcpdump -i lo -s 0 -vvv -A host 127.0.0.1 and port 8080 | tee /tmp/td.log + + ## At the end Also a thank you to all open source and open model developers, who strive for the common good. From fd662b4b0bca502cd6ec738df6277e0680921ce3 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 12 Oct 2025 23:50:35 +0530 Subject: [PATCH 026/446] SimpleChatTC: ToolCall hs info in normal assistant-user chat flow Also as part of same, wrap the request details in the assistant block using a similar tagging format as the tool_response in user block. --- tools/server/public_simplechat/readme.md | 19 +++++++++++++++++++ tools/server/public_simplechat/simplechat.js | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 388202156e..19e0452069 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -300,6 +300,8 @@ work. ### Tool Calling +#### Extending wiht new tools + Provide a descriptive meta data explaining the tool / function being provided for tool calling. Provide a handler which should implement the specified tool / function call. It should place @@ -311,6 +313,23 @@ Update the tc_switch to include a object entry for the tool, which inturn icnlud * a reference to the handler and also * the result key +#### Mapping tool calls and responses to normal assistant - user chat flow + +Instead of maintaining tool_call request and resultant response in logically seperate parallel +channel used for requesting tool_calls by the assistant and the resulstant tool role response, +the SimpleChatTC pushes it into the normal assistant - user chat flow itself, by including the +tool call and response as a pair of tagged request with details in the assistant block and inturn +tagged response in the subsequent user block. + +This allows the GenAi/LLM to be aware of the tool calls it made as well as the responses it got, +so that it can incorporate the results of the same in the subsequent chat / interactions. + +NOTE: This flow tested to be ok enough with Gemma-3N-E4B-it-Q8_0 LLM ai model for now. + +TODO: Need to think later, whether to continue this simple flow, or atleast use tool role wrt +the tool call responses or even go further and have the logically seperate tool_call request +structures also. + ### Debuging the handshake diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 4e243db2f0..83911fde42 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -74,7 +74,7 @@ class AssistantResponse { if (this.response.content !== "") { return this.response.content; } else if (this.response.toolname !== "") { - return `ToolCall:${this.response.toolname}:${this.response.toolargs}`; + return `\n${this.response.toolname}\n${this.response.toolargs}\n`; } else { return "" } From 1fc44c971d3c741f883522c1e223eac10ce8c0ff Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 13 Oct 2025 00:41:19 +0530 Subject: [PATCH 027/446] SimpleChatTC: Add ui elements for tool call verify and trigger Instead of automatically calling the requested tool with supplied arguments, rather allow user to verify things before triggering the tool. NOTE: User already provided control over tool_response before submitting it to the ai assistant. --- tools/server/public_simplechat/index.html | 9 +++++++++ tools/server/public_simplechat/simplechat.js | 14 +++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/index.html b/tools/server/public_simplechat/index.html index f6413016fc..c9b508eecf 100644 --- a/tools/server/public_simplechat/index.html +++ b/tools/server/public_simplechat/index.html @@ -40,6 +40,15 @@

You need to have javascript enabled.

+
+
+
+ + +
+ +
+
diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 83911fde42..73e1cafc80 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -91,7 +91,11 @@ let gUsageMsg = `
  • Completion mode - no system prompt normally.
  • Use shift+enter for inserting enter/newline.
  • -
  • Enter your query to ai assistant below.
  • +
  • Enter your query to ai assistant in textarea provided below.
  • +
  • If ai assistant requests a tool call, varify same before triggering it.
  • +
      +
    • submit tool response placed into user query textarea
    • +
  • Default ContextWindow = [System, Last Query+Resp, Cur Query].
    • ChatHistInCtxt, MaxTokens, ModelCtxt window to expand
    • @@ -562,6 +566,10 @@ class MultiChatUI { this.elDivHeading = /** @type{HTMLSelectElement} */(document.getElementById("heading")); this.elDivSessions = /** @type{HTMLDivElement} */(document.getElementById("sessions-div")); this.elBtnSettings = /** @type{HTMLButtonElement} */(document.getElementById("settings")); + this.elDivTool = /** @type{HTMLDivElement} */(document.getElementById("tool-div")); + this.elBtnTool = /** @type{HTMLButtonElement} */(document.getElementById("tool-btn")); + this.elInToolName = /** @type{HTMLInputElement} */(document.getElementById("toolname-in")); + this.elInToolArgs = /** @type{HTMLInputElement} */(document.getElementById("toolargs-in")); this.validate_element(this.elInSystem, "system-in"); this.validate_element(this.elDivChat, "chat-div"); @@ -569,6 +577,10 @@ class MultiChatUI { this.validate_element(this.elDivHeading, "heading"); this.validate_element(this.elDivChat, "sessions-div"); this.validate_element(this.elBtnSettings, "settings"); + this.validate_element(this.elDivTool, "tool-div"); + this.validate_element(this.elInToolName, "toolname-in"); + this.validate_element(this.elInToolArgs, "toolargs-in"); + this.validate_element(this.elBtnTool, "tool-btn"); } /** From bfe789706e7a309154d269597d430b80eb9fadc4 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 13 Oct 2025 01:19:37 +0530 Subject: [PATCH 028/446] SimpleChatTC: Let user trigger tool call, instead of automatic Instead of automatically calling any requested tool by the GenAi / llm, that is from the tail end of the handle user submit btn click, Now if the GenAi/LLM has requested any tool to be called, then enable the Tool Run related UI elements and fill them with the tool name and tool args. In turn the user can verify if they are ok with the tool being called and the arguments being passed to it. Rather they can even fix any errors in the tool usage like the arithmatic expr to calculate that is being passed to simple_calculator or the javascript code being passed to run_javascript_function_code If user is ok with the tool call being requested, then trigger the same. The results if any will be automatically placed into the user query text area. User can cross verify if they are ok with the result and or modify it suitabley if required and inturn submit the same to the GenAi/LLM. --- tools/server/public_simplechat/simplechat.js | 55 +++++++++++++++++--- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 73e1cafc80..9b4cc58a15 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -70,10 +70,17 @@ class AssistantResponse { this.response[resp.key] += resp.value; } + has_toolcall() { + if (this.response.toolname.trim() == "") { + return false + } + return true + } + content_equiv() { if (this.response.content !== "") { return this.response.content; - } else if (this.response.toolname !== "") { + } else if (this.has_toolcall()) { return `\n${this.response.toolname}\n${this.response.toolargs}\n`; } else { return "" @@ -533,15 +540,15 @@ class SimpleChat { /** * Call the requested tool/function and get its response - * @param {AssistantResponse} ar + * @param {string} toolname + * @param {string} toolargs */ - async handle_toolcall(ar) { - let toolname = ar.response.toolname.trim(); + async handle_toolcall(toolname, toolargs) { if (toolname === "") { return undefined } try { - return await tools.tool_call(toolname, ar.response.toolargs) + return await tools.tool_call(toolname, toolargs) } catch (/** @type {any} */error) { return `Tool/Function call raised an exception:${error.name}:${error.message}` } @@ -596,6 +603,24 @@ class MultiChatUI { } } + /** + * Reset/Setup Tool Call UI parts as needed + * @param {AssistantResponse} ar + */ + ui_reset_toolcall_as_needed(ar) { + if (ar.has_toolcall()) { + this.elDivTool.hidden = false + this.elInToolName.value = ar.response.toolname + this.elInToolArgs.value = ar.response.toolargs + this.elBtnTool.disabled = false + } else { + this.elDivTool.hidden = true + this.elInToolName.value = "" + this.elInToolArgs.value = "" + this.elBtnTool.disabled = true + } + } + /** * Reset user input ui. * * clear user input (if requested, default true) @@ -641,6 +666,13 @@ class MultiChatUI { }); }); + this.elBtnTool.addEventListener("click", (ev)=>{ + if (this.elDivTool.hidden) { + return; + } + this.handle_tool_run(this.curChatId); + }) + this.elInUser.addEventListener("keyup", (ev)=> { // allow user to insert enter into their message using shift+enter. // while just pressing enter key will lead to submitting. @@ -729,7 +761,18 @@ class MultiChatUI { } else { console.debug(`DBUG:SimpleChat:MCUI:HandleUserSubmit:ChatId has changed:[${chatId}] [${this.curChatId}]`); } - let toolResult = await chat.handle_toolcall(theResp) + this.ui_reset_toolcall_as_needed(theResp); + this.ui_reset_userinput(); + } + + /** + * @param {string} chatId + */ + async handle_tool_run(chatId) { + let chat = this.simpleChats[chatId]; + this.elInUser.value = "toolcall in progress..."; + this.elInUser.disabled = true; + let toolResult = await chat.handle_toolcall(this.elInToolName.value, this.elInToolArgs.value) if (toolResult !== undefined) { this.elInUser.value = `${toolResult}` } From 1e5b638bebc467ab7d01eea34bdda433a1c121c8 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 13 Oct 2025 02:57:17 +0530 Subject: [PATCH 029/446] SimpleChatTC: Update readme with bit more details, Cleaner UI Also avoid showing Tool calling UI elements, when not needed to be shown. --- tools/server/public_simplechat/readme.md | 54 +++++++++++++++++--- tools/server/public_simplechat/simplechat.js | 5 ++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 19e0452069..d2a1e22df6 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -33,6 +33,10 @@ Allows developer/end-user to control some of the behaviour by updating gMe membe console. Parallely some of the directly useful to end-user settings can also be changed using the provided settings ui. +For GenAi/LLM models supporting tool / function calling, allows one to interact with them and explore use of +ai driven augmenting of the knowledge used for generating answers by using the predefined tools/functions. +The end user is provided control over tool calling and response submitting. + NOTE: Current web service api doesnt expose the model context length directly, so client logic doesnt provide any adaptive culling of old messages nor of replacing them with summary of their content etal. However there is a optional sliding window based chat logic, which provides a simple minded culling of old messages from @@ -117,12 +121,15 @@ Once inside * the user input box will be disabled and a working message will be shown in it. * if trim garbage is enabled, the logic will try to trim repeating text kind of garbage to some extent. -* tool calling flow +* tool calling flow when working with ai models which support tool / function calling * if tool calling is enabled and the user query results in need for one of the builtin tools to be - called, then the response will include request for tool call. - * the SimpleChat client will call the requested tool and inturn place the returned result into user - entry text area with generated result + called, then the ai response might include request for tool call. + * the SimpleChat client will show details of the tool call (ie tool name and args passed) requested + and allow the user to trigger it as is or after modifying things as needed. + * inturn returned / generated result is placed into user query entry text area with approriate tags + ie generated result * if user is ok with the tool response, they can click submit to send the same to the GenAi/LLM. + User can even modify the response generated by the tool, if required, before submitting. * just refresh the page, to reset wrt the chat history and or system prompt and start afresh. @@ -170,11 +177,15 @@ It is attached to the document object. Some of these can also be updated using t remember to enable this only for GenAi/LLM models which support tool/function calling. - the builtin tools meta data is sent to the ai model in the requests sent to it. + the builtin tools' meta data is sent to the ai model in the requests sent to it. inturn if the ai model requests a tool call to be made, the same will be done and the response sent back to the ai model, under user control. + as tool calling will involve a bit of back and forth between ai assistant and end user, it is + recommended to set iRecentUserMsgCnt to 5 or more, so that enough context is retained during + chatting with ai models with tool support. + apiEP - select between /completions and /chat/completions endpoint provided by the server/ai-model. bCompletionFreshChatAlways - whether Completion mode collates complete/sliding-window history when @@ -300,9 +311,31 @@ work. ### Tool Calling +ALERT: Currently the way this is implemented, it is dangerous to use this, unless one verifies +all the tool calls requested and the responses generated manually to ensure everything is fine, +during interaction with ai modles with tools support. + +#### Builtin Tools + +The following tools/functions are currently provided by default +* simple_calculator - which can solve simple arithmatic expressions +* run_javascript_function_code - which can be used to run some javascript code in the browser + context. + +Currently the generated code / expression is run through a simple dynamic function mechanism. +May update things, in future, so that a WebWorker is used to avoid exposing browser global scope +to the generated code directly. Either way always remember to cross check the tool requests and +generated responses when using tool calling. + +May add +* web_fetch along with a corresponding simple local proxy server logic that can bypass the + CORS restrictions applied if trying to directly fetch from the browser js runtime environment. + + #### Extending wiht new tools -Provide a descriptive meta data explaining the tool / function being provided for tool calling. +Provide a descriptive meta data explaining the tool / function being provided for tool calling, +as well as its arguments. Provide a handler which should implement the specified tool / function call. It should place the result to be sent back to the ai model in the result key of the tc_switch entry for the @@ -330,6 +363,15 @@ TODO: Need to think later, whether to continue this simple flow, or atleast use the tool call responses or even go further and have the logically seperate tool_call request structures also. +#### ToDo + +Update to use web worker. + +Make the Tool Call related ui elements use up horizontal space properly. + +Try and trap promises based flows to ensure all generated results or errors if any are caught +before responding back to the ai model. + ### Debuging the handshake diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 9b4cc58a15..599b147adc 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -649,6 +649,8 @@ class MultiChatUI { this.handle_session_switch(this.curChatId); } + this.ui_reset_toolcall_as_needed(new AssistantResponse()); + this.elBtnSettings.addEventListener("click", (ev)=>{ this.elDivChat.replaceChildren(); gMe.show_settings(this.elDivChat); @@ -729,6 +731,8 @@ class MultiChatUI { chat.clear(); } + this.ui_reset_toolcall_as_needed(new AssistantResponse()); + chat.add_system_anytime(this.elInSystem.value, chatId); let content = this.elInUser.value; @@ -766,6 +770,7 @@ class MultiChatUI { } /** + * Handle running of specified tool call if any, for the specified chat session. * @param {string} chatId */ async handle_tool_run(chatId) { From a8c8176d09df0654665ab2a30fe343d5567d4978 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 13 Oct 2025 03:18:09 +0530 Subject: [PATCH 030/446] SimpleChatTC: Tool Calling UI elements use up horizontal space --- tools/server/public_simplechat/index.html | 6 +++++- tools/server/public_simplechat/readme.md | 7 ++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/index.html b/tools/server/public_simplechat/index.html index c9b508eecf..3cd840569c 100644 --- a/tools/server/public_simplechat/index.html +++ b/tools/server/public_simplechat/index.html @@ -42,11 +42,15 @@
      +
      - +
      +
      +
      +

      diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index d2a1e22df6..29214369ec 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -328,8 +328,9 @@ to the generated code directly. Either way always remember to cross check the to generated responses when using tool calling. May add -* web_fetch along with a corresponding simple local proxy server logic that can bypass the - CORS restrictions applied if trying to directly fetch from the browser js runtime environment. +* web_fetch along with a corresponding simple local web proxy/caching server logic that can bypass + the CORS restrictions applied if trying to directly fetch from the browser js runtime environment. + Inturn maybe with a white list of allowed sites to access or so. #### Extending wiht new tools @@ -367,7 +368,7 @@ structures also. Update to use web worker. -Make the Tool Call related ui elements use up horizontal space properly. +WebFetch and Local web proxy/caching server Try and trap promises based flows to ensure all generated results or errors if any are caught before responding back to the ai model. From 45d8a00738eba039fbe5b9621b440f5030c11658 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 13 Oct 2025 03:32:58 +0530 Subject: [PATCH 031/446] SimpleChatTC: Update readme wrt --jinja argument and bit more --- tools/server/public_simplechat/readme.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 29214369ec..6f9f986b01 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -7,7 +7,7 @@ by Humans for All. To run from the build dir -bin/llama-server -m path/model.gguf --path ../tools/server/public_simplechat +bin/llama-server -m path/model.gguf --path ../tools/server/public_simplechat --jinja Continue reading for the details. @@ -68,6 +68,16 @@ next run this web front end in tools/server/public_simplechat * cd ../tools/server/public_simplechat * python3 -m http.server PORT +### for tool calling + +remember to + +* pass --jinja to llama-server to enable tool calling support from the server ai engine end. + +* enable bTools in the settings page of the client side gui. + +* use a GenAi/LLM model which supports tool calling. + ### using the front end Open this simple web front end from your local browser From 2701cb3a1e4f8889e6beedc5dccdc0a2401a49b2 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 13 Oct 2025 22:11:25 +0530 Subject: [PATCH 032/446] SimpleChatTC: Move console.log trapping into its own module So that it can be used from different modules, if required. --- tools/server/public_simplechat/tooljs.mjs | 45 +++---------------- .../server/public_simplechat/toolsconsole.mjs | 38 ++++++++++++++++ 2 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 tools/server/public_simplechat/toolsconsole.mjs diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index b79b79dd82..23b340e514 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -7,38 +7,7 @@ // -let gConsoleStr = "" -/** - * @type { {(...data: any[]): void} | null} - */ -let gOrigConsoleLog = null - - -/** - * @param {any[]} args - */ -function console_trapped(...args) { - let res = args.map((arg)=>{ - if (typeof arg == 'object') { - return JSON.stringify(arg); - } else { - return String(arg); - } - }).join(' '); - gConsoleStr += res; -} - -function console_redir() { - gOrigConsoleLog = console.log - console.log = console_trapped - gConsoleStr = "" -} - -function console_revert() { - if (gOrigConsoleLog !== null) { - console.log = gOrigConsoleLog - } -} +import * as tconsole from "./toolsconsole.mjs" let js_meta = { @@ -67,11 +36,11 @@ let js_meta = { * @param {any} obj */ function js_run(toolname, obj) { - console_redir() + tconsole.console_redir() let func = new Function(obj["code"]) func() - console_revert() - tc_switch[toolname]["result"] = gConsoleStr + tconsole.console_revert() + tc_switch[toolname]["result"] = tconsole.gConsoleStr } @@ -101,11 +70,11 @@ let calc_meta = { * @param {any} obj */ function calc_run(toolname, obj) { - console_redir() + tconsole.console_redir() let func = new Function(`console.log(${obj["arithexpr"]})`) func() - console_revert() - tc_switch[toolname]["result"] = gConsoleStr + tconsole.console_revert() + tc_switch[toolname]["result"] = tconsole.gConsoleStr } diff --git a/tools/server/public_simplechat/toolsconsole.mjs b/tools/server/public_simplechat/toolsconsole.mjs new file mode 100644 index 0000000000..0c7d436a0a --- /dev/null +++ b/tools/server/public_simplechat/toolsconsole.mjs @@ -0,0 +1,38 @@ +//@ts-check +// Helpers to handle tools/functions calling wrt console +// by Humans for All +// + + +export let gConsoleStr = "" +/** + * @type { {(...data: any[]): void} | null} + */ +export let gOrigConsoleLog = null + + +/** + * @param {any[]} args + */ +export function console_trapped(...args) { + let res = args.map((arg)=>{ + if (typeof arg == 'object') { + return JSON.stringify(arg); + } else { + return String(arg); + } + }).join(' '); + gConsoleStr += res; +} + +export function console_redir() { + gOrigConsoleLog = console.log + console.log = console_trapped + gConsoleStr = "" +} + +export function console_revert() { + if (gOrigConsoleLog !== null) { + console.log = gOrigConsoleLog + } +} From a6bccf934e4b7aec2d419b18cdbac97da2b2e6be Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 13 Oct 2025 22:31:31 +0530 Subject: [PATCH 033/446] SimpleChatTC:ToolsConsole:Cleanup a bit, add basic set of notes Try ensure as well as verify that original console.log is saved and not overwritten. Throw an exception if things seem off wrt same. Also ensure to add a newline at end of console.log messages --- .../server/public_simplechat/toolsconsole.mjs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/toolsconsole.mjs b/tools/server/public_simplechat/toolsconsole.mjs index 0c7d436a0a..b372dc74ef 100644 --- a/tools/server/public_simplechat/toolsconsole.mjs +++ b/tools/server/public_simplechat/toolsconsole.mjs @@ -4,14 +4,17 @@ // +/** The redirected console.log's capture-data-space */ export let gConsoleStr = "" /** + * Maintain original console.log, when needed * @type { {(...data: any[]): void} | null} */ -export let gOrigConsoleLog = null +let gOrigConsoleLog = null /** + * The trapping console.log * @param {any[]} args */ export function console_trapped(...args) { @@ -22,17 +25,33 @@ export function console_trapped(...args) { return String(arg); } }).join(' '); - gConsoleStr += res; + gConsoleStr += `${res}\n`; } +/** + * Save the original console.log, if needed. + * Setup redir of console.log. + * Clear the redirected console.log's capture-data-space. + */ export function console_redir() { - gOrigConsoleLog = console.log + if (gOrigConsoleLog == null) { + if (console.log == console_trapped) { + throw new Error("ERRR:ToolsConsole:ReDir:Original Console.Log lost???"); + } + gOrigConsoleLog = console.log + } console.log = console_trapped gConsoleStr = "" } +/** + * Revert the redirected console.log to the original console.log, if possible. + */ export function console_revert() { if (gOrigConsoleLog !== null) { + if (gOrigConsoleLog == console_trapped) { + throw new Error("ERRR:ToolsConsole:Revert:Original Console.Log lost???"); + } console.log = gOrigConsoleLog } } From 510c65c7219ba8ad8964339bbf0ea8eb7eb5037a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 13 Oct 2025 23:07:44 +0530 Subject: [PATCH 034/446] SimpleChatTC: Initial skeleton of a simple toolsworker --- .../server/public_simplechat/toolsworker.mjs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tools/server/public_simplechat/toolsworker.mjs diff --git a/tools/server/public_simplechat/toolsworker.mjs b/tools/server/public_simplechat/toolsworker.mjs new file mode 100644 index 0000000000..b17c5bb197 --- /dev/null +++ b/tools/server/public_simplechat/toolsworker.mjs @@ -0,0 +1,19 @@ +//@ts-check +// STILL DANGER DANGER DANGER - Simple and Stupid - Use from a discardable VM only +// Helpers to handle tools/functions calling using web worker +// by Humans for All +// + +import * as tconsole from "./toolsconsole.mjs" + +tconsole.console_redir() + +onmessage = async (ev) => { + try { + eval(ev.data) + } catch (/** @type {any} */error) { + console.log(`\n\nTool/Function call raised an exception:${error.name}:${error.message}\n\n`) + } + tconsole.console_revert() + postMessage(tconsole.gConsoleStr) +} From 14d67f6c3cf13da50c639d34cd48012a77e01ac9 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 00:54:21 +0530 Subject: [PATCH 035/446] SimpleChatTC: Pass around structured objects wrt tool worker The request for code to run as well as the resultant response data both need to follow a structured object convention, so that it is easy to map a request and the corresponding response to some extent. --- tools/server/public_simplechat/toolsworker.mjs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/toolsworker.mjs b/tools/server/public_simplechat/toolsworker.mjs index b17c5bb197..d17b772b4f 100644 --- a/tools/server/public_simplechat/toolsworker.mjs +++ b/tools/server/public_simplechat/toolsworker.mjs @@ -4,16 +4,23 @@ // by Humans for All // +/** + * Expects to get a message with identifier name and code to run + * Posts message with identifier name and data captured from console.log outputs + */ + + import * as tconsole from "./toolsconsole.mjs" + tconsole.console_redir() -onmessage = async (ev) => { +self.onmessage = function (ev) { try { - eval(ev.data) + eval(ev.data.code) } catch (/** @type {any} */error) { - console.log(`\n\nTool/Function call raised an exception:${error.name}:${error.message}\n\n`) + console.log(`\n\nTool/Function call "${ev.data.name}" raised an exception:${error.name}:${error.message}\n\n`) } tconsole.console_revert() - postMessage(tconsole.gConsoleStr) + self.postMessage({ name: ev.data.name, data: tconsole.gConsoleStr}) } From 2a8bd1c9e7e9303c75b65fdbea11955e7f72dd19 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 00:58:23 +0530 Subject: [PATCH 036/446] SimpleChatTC: Actual tool call implementations simplified These no longer need to worry about * setting up the console.log related redirection to capture the generated outputs, nor about * setting up a dynamic function for executing the needed tool call related code The web worker setup to help run tool calls in a relatively isolated environment independent of the main browser env, takes care of these. One needs to only worry about getting the handle to the web worker to use and inturn pass the need code wrt the tool call to it. --- tools/server/public_simplechat/tooljs.mjs | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 23b340e514..5ee8e83004 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -7,7 +7,7 @@ // -import * as tconsole from "./toolsconsole.mjs" +let gToolsWorker = /** @type{Worker} */(/** @type {unknown} */(null)); let js_meta = { @@ -31,16 +31,12 @@ let js_meta = { /** * Implementation of the javascript interpretor logic. Minimal skeleton for now. - * ALERT: Has access to the javascript environment and can mess with it and beyond + * ALERT: Has access to the javascript web worker environment and can mess with it and beyond * @param {string} toolname * @param {any} obj */ function js_run(toolname, obj) { - tconsole.console_redir() - let func = new Function(obj["code"]) - func() - tconsole.console_revert() - tc_switch[toolname]["result"] = tconsole.gConsoleStr + gToolsWorker.postMessage({ name: toolname, code: obj["code"]}) } @@ -65,16 +61,12 @@ let calc_meta = { /** * Implementation of the simple calculator logic. Minimal skeleton for now. - * ALERT: Has access to the javascript environment and can mess with it and beyond + * ALERT: Has access to the javascript web worker environment and can mess with it and beyond * @param {string} toolname * @param {any} obj */ function calc_run(toolname, obj) { - tconsole.console_redir() - let func = new Function(`console.log(${obj["arithexpr"]})`) - func() - tconsole.console_revert() - tc_switch[toolname]["result"] = tconsole.gConsoleStr + gToolsWorker.postMessage({ name: toolname, code: `console.log(${obj["arithexpr"]})`}) } @@ -94,3 +86,11 @@ export let tc_switch = { } } + +/** + * Used to get hold of the web worker to use for running tool/function call related code + * @param {Worker} toolsWorker + */ +export function init(toolsWorker) { + gToolsWorker = toolsWorker +} From 148ec1c41abfbdb80bf6d063f8c79edfb1cb1a54 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 01:06:00 +0530 Subject: [PATCH 037/446] SimpleChatTC: Get ready for decoupled tool call response tools manager/module * setup the web worker that will help execute the tool call related codes in a js environment that is isolated from the browsers main js environment * pass the web worker to the tool call providers, for them to use * dont wait for the result from the tool call, as it will be got later asynchronously through a message * allow users of the tools manager to register a call back, which will be called when ever a message is got from the web worker containing response wrt previously requested tool call execution. simplechat * decouple toolcall response handling and toolcall requesting logic * setup a timeout to take back control if tool call takes up too much time. Inturn help alert the ai model, that the tool call took up too much time and so was aborted, by placing a approriate tagged tool response into user query area. * register a call back that will be called when response is got asynchronously wrt anye requested tool calls. In turn take care of updating the user query area with response got wrt the tool call, along with tool response tag around it. --- tools/server/public_simplechat/simplechat.js | 24 ++++++++++++++++---- tools/server/public_simplechat/tools.mjs | 21 ++++++++++++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 599b147adc..925a181dbf 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -539,13 +539,15 @@ class SimpleChat { } /** - * Call the requested tool/function and get its response + * Call the requested tool/function. + * Returns undefined, if the call was placed successfully + * Else some appropriate error message will be returned. * @param {string} toolname * @param {string} toolargs */ async handle_toolcall(toolname, toolargs) { if (toolname === "") { - return undefined + return "Tool/Function call name not specified" } try { return await tools.tool_call(toolname, toolargs) @@ -675,6 +677,11 @@ class MultiChatUI { this.handle_tool_run(this.curChatId); }) + tools.setup((name, data)=>{ + this.elInUser.value = `${data}` + this.ui_reset_userinput(false) + }) + this.elInUser.addEventListener("keyup", (ev)=> { // allow user to insert enter into their message using shift+enter. // while just pressing enter key will lead to submitting. @@ -771,17 +778,24 @@ class MultiChatUI { /** * Handle running of specified tool call if any, for the specified chat session. + * Also sets up a timeout, so that user gets control back to interact with the ai model. * @param {string} chatId */ async handle_tool_run(chatId) { let chat = this.simpleChats[chatId]; this.elInUser.value = "toolcall in progress..."; this.elInUser.disabled = true; - let toolResult = await chat.handle_toolcall(this.elInToolName.value, this.elInToolArgs.value) + let toolname = this.elInToolName.value.trim() + let toolResult = await chat.handle_toolcall(toolname, this.elInToolArgs.value) if (toolResult !== undefined) { this.elInUser.value = `${toolResult}` + this.ui_reset_userinput(false) + } else { + setTimeout(() => { + this.elInUser.value = `Tool/Function call ${toolname} taking too much time, aborting...` + this.ui_reset_userinput(false) + }, 10000) } - this.ui_reset_userinput(toolResult === undefined); } /** @@ -1073,7 +1087,7 @@ function startme() { document["gMe"] = gMe; document["du"] = du; document["tools"] = tools; - tools.setup() + tools.init() for (let cid of gMe.defaultChatIds) { gMe.multiChat.new_chat_session(cid); } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index d75d3eb7bf..4ece70ae56 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -8,12 +8,14 @@ import * as tjs from './tooljs.mjs' +let gToolsWorker = new Worker('./toolsworker.mjs'); /** * @type {Object>} */ export let tc_switch = {} -export function setup() { +export function init() { + tjs.init(gToolsWorker) for (const key in tjs.tc_switch) { tc_switch[key] = tjs.tc_switch[key] } @@ -27,9 +29,22 @@ export function meta() { return tools } +/** + * Setup the callback that will be called when ever message + * is recieved from the Tools Web Worker. + * @param {(name: string, data: string) => void} cb + */ +export function setup(cb) { + gToolsWorker.onmessage = function (ev) { + cb(ev.data.name, ev.data.data) + } +} + /** - * Try call the specified tool/function call and return its response + * Try call the specified tool/function call. + * Returns undefined, if the call was placed successfully + * Else some appropriate error message will be returned. * @param {string} toolname * @param {string} toolargs */ @@ -38,7 +53,7 @@ export async function tool_call(toolname, toolargs) { if (fn == toolname) { try { tc_switch[fn]["handler"](fn, JSON.parse(toolargs)) - return tc_switch[fn]["result"] + return undefined } catch (/** @type {any} */error) { return `Tool/Function call raised an exception:${error.name}:${error.message}` } From a0f6762fda379be100c029d6e3051a69f52585e2 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 01:34:22 +0530 Subject: [PATCH 038/446] SimpleChatTC: Web worker flow initial go cleanup Had forgotten to specify type as module wrt web worker, in order to allow it to import the toolsconsole module. Had forgotten to maintain the id of the timeout handler, which is needed to clear/stop the timeout handler from triggering, if tool call response is got well in time. As I am currently reverting the console redirection at end of handling a tool call code in the web worker message handler, I need to setup the redirection each time. Also I had forgotten to clear the console.log capture data space, before a new tool call code is executed, this is also fixed by this change. TODO: Need to abort the tool call code execution in the web worker if possible in future, if the client / browser side times out waiting for tool call response, ie if the tool call code is taking up too much time. --- tools/server/public_simplechat/simplechat.js | 3 ++- tools/server/public_simplechat/tools.mjs | 2 +- tools/server/public_simplechat/toolsworker.mjs | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 925a181dbf..6b897448c2 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -678,6 +678,7 @@ class MultiChatUI { }) tools.setup((name, data)=>{ + clearTimeout(this.idTimeOut) this.elInUser.value = `${data}` this.ui_reset_userinput(false) }) @@ -791,7 +792,7 @@ class MultiChatUI { this.elInUser.value = `${toolResult}` this.ui_reset_userinput(false) } else { - setTimeout(() => { + this.idTimeOut = setTimeout(() => { this.elInUser.value = `Tool/Function call ${toolname} taking too much time, aborting...` this.ui_reset_userinput(false) }, 10000) diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 4ece70ae56..75fe56e4f4 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -8,7 +8,7 @@ import * as tjs from './tooljs.mjs' -let gToolsWorker = new Worker('./toolsworker.mjs'); +let gToolsWorker = new Worker('./toolsworker.mjs', { type: 'module' }); /** * @type {Object>} */ diff --git a/tools/server/public_simplechat/toolsworker.mjs b/tools/server/public_simplechat/toolsworker.mjs index d17b772b4f..e370fd0a9d 100644 --- a/tools/server/public_simplechat/toolsworker.mjs +++ b/tools/server/public_simplechat/toolsworker.mjs @@ -13,9 +13,8 @@ import * as tconsole from "./toolsconsole.mjs" -tconsole.console_redir() - self.onmessage = function (ev) { + tconsole.console_redir() try { eval(ev.data.code) } catch (/** @type {any} */error) { From 1789f5f1e23e34292f8378e194ea6f42d95464e5 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 01:56:34 +0530 Subject: [PATCH 039/446] SimpleChatTC: Increase the sliding window context to Last4 QA As the tool calling, if enabled, will need access to last few user query and ai assistant responses (which will also include in them the tool call requests and the corresponding results), so that the model can build answers based on its tool call reqs and got responses, and also given that most of the models these days have sufficiently large context windows, so the sliding window context implemented by SimpleChat logic has been increased by default to include last 4 query and their responses roughlty. --- tools/server/public_simplechat/simplechat.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 6b897448c2..4804c88ea0 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -103,7 +103,7 @@ let gUsageMsg = `
      • submit tool response placed into user query textarea
      -
    • Default ContextWindow = [System, Last Query+Resp, Cur Query].
    • +
    • Default ContextWindow = [System, Last4 Query+Resp, Cur Query].
      • ChatHistInCtxt, MaxTokens, ModelCtxt window to expand
      @@ -886,7 +886,7 @@ class Me { this.bCompletionFreshChatAlways = true; this.bCompletionInsertStandardRolePrefix = false; this.bTrimGarbage = true; - this.iRecentUserMsgCnt = 2; + this.iRecentUserMsgCnt = 5; this.sRecentUserMsgCnt = { "Full": -1, "Last0": 1, From c2112618c03efdba801d7b35b2c3951a2c0f4973 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 02:28:42 +0530 Subject: [PATCH 040/446] SimpleChatTC: Update readme.md wrt latest updates. 2k maxtokens --- tools/server/public_simplechat/readme.md | 51 +++++++++++--------- tools/server/public_simplechat/simplechat.js | 4 +- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 6f9f986b01..c8cb786c3c 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -239,10 +239,10 @@ It is attached to the document object. Some of these can also be updated using t be set if needed using the settings ui. iRecentUserMsgCnt - a simple minded SlidingWindow to limit context window load at Ai Model end. - This is disabled by default. However if enabled, then in addition to latest system message, only - the last/latest iRecentUserMsgCnt user messages after the latest system prompt and its responses - from the ai model will be sent to the ai-model, when querying for a new response. IE if enabled, - only user messages after the latest system message/prompt will be considered. + This is set to 5 by default. So in addition to latest system message, last/latest iRecentUserMsgCnt + user messages after the latest system prompt and its responses from the ai model will be sent + to the ai-model, when querying for a new response. Note that if enabled, only user messages after + the latest system message/prompt will be considered. This specified sliding window user message count also includes the latest user query. <0 : Send entire chat history to server @@ -282,9 +282,11 @@ full chat history. This way if there is any response with garbage/repeatation, i mess with things beyond the next question/request/query, in some ways. The trim garbage option also tries to help avoid issues with garbage in the context to an extent. -Set max_tokens to 1024, so that a relatively large previous reponse doesnt eat up the space -available wrt next query-response. However dont forget that the server when started should -also be started with a model context size of 1k or more, to be on safe side. +Set max_tokens to 2048, so that a relatively large previous reponse doesnt eat up the space +available wrt next query-response. While parallely allowing a good enough context size for +some amount of the chat history in the current session to influence future answers. However +dont forget that the server when started should also be started with a model context size of +2k or more, to be on safe side. The /completions endpoint of tools/server doesnt take max_tokens, instead it takes the internal n_predict, for now add the same here on the client side, maybe later add max_tokens @@ -321,9 +323,9 @@ work. ### Tool Calling -ALERT: Currently the way this is implemented, it is dangerous to use this, unless one verifies -all the tool calls requested and the responses generated manually to ensure everything is fine, -during interaction with ai modles with tools support. +ALERT: The simple minded way in which this is implemented, it can be dangerous in the worst case, +Always remember to verify all the tool calls requested and the responses generated manually to +ensure everything is fine, during interaction with ai modles with tools support. #### Builtin Tools @@ -332,10 +334,10 @@ The following tools/functions are currently provided by default * run_javascript_function_code - which can be used to run some javascript code in the browser context. -Currently the generated code / expression is run through a simple dynamic function mechanism. -May update things, in future, so that a WebWorker is used to avoid exposing browser global scope -to the generated code directly. Either way always remember to cross check the tool requests and -generated responses when using tool calling. +Currently the generated code / expression is run through a simple minded eval inside a web worker +mechanism. Use of WebWorker helps avoid exposing browser global scope to the generated code directly. +However any shared web worker scope isnt isolated. Either way always remember to cross check the tool +requests and generated responses when using tool calling. May add * web_fetch along with a corresponding simple local web proxy/caching server logic that can bypass @@ -343,19 +345,20 @@ May add Inturn maybe with a white list of allowed sites to access or so. -#### Extending wiht new tools +#### Extending with new tools Provide a descriptive meta data explaining the tool / function being provided for tool calling, as well as its arguments. -Provide a handler which should implement the specified tool / function call. It should place -the result to be sent back to the ai model in the result key of the tc_switch entry for the -corresponding tool. +Provide a handler which should implement the specified tool / function call or rather constructs +the code to be run to get the tool / function call job done, and inturn pass the same to the +provided web worker to get it executed. Remember to use console.log while generating any response +that should be sent back to the ai model, in your constructed code. -Update the tc_switch to include a object entry for the tool, which inturn icnludes +Update the tc_switch to include a object entry for the tool, which inturn includes * the meta data as well as * a reference to the handler and also -* the result key +* the result key (was used previously, may use in future, but for now left as is) #### Mapping tool calls and responses to normal assistant - user chat flow @@ -368,16 +371,16 @@ tagged response in the subsequent user block. This allows the GenAi/LLM to be aware of the tool calls it made as well as the responses it got, so that it can incorporate the results of the same in the subsequent chat / interactions. -NOTE: This flow tested to be ok enough with Gemma-3N-E4B-it-Q8_0 LLM ai model for now. +NOTE: This flow tested to be ok enough with Gemma-3N-E4B-it-Q8_0 LLM ai model for now. Logically +given the way current ai models work, most of them should understand things as needed, but need +to test this with other ai models later. TODO: Need to think later, whether to continue this simple flow, or atleast use tool role wrt -the tool call responses or even go further and have the logically seperate tool_call request +the tool call responses or even go further and have the logically seperate tool_calls request structures also. #### ToDo -Update to use web worker. - WebFetch and Local web proxy/caching server Try and trap promises based flows to ensure all generated results or errors if any are caught diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 4804c88ea0..9c79122218 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -907,8 +907,8 @@ class Me { this.apiRequestOptions = { "model": "gpt-3.5-turbo", "temperature": 0.7, - "max_tokens": 1024, - "n_predict": 1024, + "max_tokens": 2048, + "n_predict": 2048, "cache_prompt": false, //"frequency_penalty": 1.2, //"presence_penalty": 1.2, From 37faf8611a886a0464a568aa94c2803ad9f18529 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 02:35:24 +0530 Subject: [PATCH 041/446] SimpleChatTC: update descs to indicate use of web workers ie wrt the tool calls provided. --- tools/server/public_simplechat/tooljs.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 5ee8e83004..6aea9a5ee4 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -14,13 +14,13 @@ let js_meta = { "type": "function", "function": { "name": "run_javascript_function_code", - "description": "Runs given code using function constructor mechanism in a browser's javascript environment and returns the console.log outputs of the execution after few seconds", + "description": "Runs given code using eval within a web worker context in a browser's javascript environment and returns the console.log outputs of the execution after few seconds", "parameters": { "type": "object", "properties": { "code": { "type": "string", - "description": "The code belonging to the dynamic function to run in the browser's javascript interpreter environment." + "description": "The code that will be run using eval within a web worker in the browser's javascript interpreter environment." } }, "required": ["code"] @@ -44,7 +44,7 @@ let calc_meta = { "type": "function", "function": { "name": "simple_calculator", - "description": "Calculates the provided arithmatic expression using console.log of a browser's javascript interpreter and returns the output of the execution once it is done in few seconds", + "description": "Calculates the provided arithmatic expression using console.log within a web worker of a browser's javascript interpreter environment and returns the output of the execution once it is done in few seconds", "parameters": { "type": "object", "properties": { From 2c29c2d589f78e8a35bdeae886b4dcbaeccd4b19 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 16:48:40 +0530 Subject: [PATCH 042/446] SimpleChatTC:ChatMessage: AssistantResponse into chat message class Modify the constructor, newFrom and clear towards this goal. --- tools/server/public_simplechat/simplechat.js | 27 ++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 9c79122218..4183ca87a2 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -38,23 +38,34 @@ class ApiEP { } -class AssistantResponse { +class ChatMessage { - constructor(content="", toolname="", toolargs="", trimmedContent="") { - /** @type {Object} */ - this.response = { content: content, toolname: toolname, toolargs: toolargs, trimmedContent: trimmedContent }; + /** + * Represent a Message in the Chat + * @param {string} role + * @param {string} content + * @param {Array} tool_calls + * @param {string} trimmedContent + */ + constructor(role = "", content="", tool_calls=[], trimmedContent="") { + /** @type {Object} */ + this.ns = { role: role, content: content, tool_calls: tool_calls } + this.trimmedContent = trimmedContent; } /** * Create a new instance from an existing instance - * @param {AssistantResponse} old + * @param {ChatMessage} old */ static newFrom(old) { - return new AssistantResponse(old.response.content, old.response.toolname, old.response.toolargs, old.response.trimmedContent) + return new ChatMessage(old.ns.role, old.ns.content, old.ns.tool_calls, old.trimmedContent) } clear() { - this.response = { content: "", toolname: "", toolargs: "", trimmedContent: "" }; + this.ns.role = ""; + this.ns.content = ""; + this.ns.tool_calls = []; + this.trimmedContent = ""; } /** @@ -71,7 +82,7 @@ class AssistantResponse { } has_toolcall() { - if (this.response.toolname.trim() == "") { + if (this.ns.tool_calls.trim() == "") { return false } return true From aa229a1f99c5ca86fc91ce0e5e3a2428578d8dce Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 17:59:06 +0530 Subject: [PATCH 043/446] SimpleChatTC:ChatMessageEx: UpdateStream logic Rename ChatMessage to ChatMessageEx. Add typedefs for NSToolCall and NSChatMessage, they represent the way the corresponding data is structured in network hs. Add logic to build the ChatMessageEx from data got over network in streaming mode. --- tools/server/public_simplechat/simplechat.js | 69 ++++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 4183ca87a2..608479c862 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -38,7 +38,15 @@ class ApiEP { } -class ChatMessage { +/** + * @typedef {{id: string, type: string, function: {name: string, arguments: string}}} NSToolCalls + */ + +/** + * @typedef {{role: string, content: string, tool_calls: Array}} NSChatMessage + */ + +class ChatMessageEx { /** * Represent a Message in the Chat @@ -48,17 +56,17 @@ class ChatMessage { * @param {string} trimmedContent */ constructor(role = "", content="", tool_calls=[], trimmedContent="") { - /** @type {Object} */ + /** @type {NSChatMessage} */ this.ns = { role: role, content: content, tool_calls: tool_calls } this.trimmedContent = trimmedContent; } /** * Create a new instance from an existing instance - * @param {ChatMessage} old + * @param {ChatMessageEx} old */ static newFrom(old) { - return new ChatMessage(old.ns.role, old.ns.content, old.ns.tool_calls, old.trimmedContent) + return new ChatMessageEx(old.ns.role, old.ns.content, old.ns.tool_calls, old.trimmedContent) } clear() { @@ -69,16 +77,53 @@ class ChatMessage { } /** - * Helps collate the latest response from the server/ai-model, as it is becoming available. - * This is mainly useful for the stream mode. - * @param {{key: string, value: string}} resp + * Update based on the drip by drip data got from network in streaming mode + * @param {any} nwo + * @param {string} apiEP */ - append_response(resp) { - if (resp.value == null) { - return + update_stream(nwo, apiEP) { + if (apiEP == ApiEP.Type.Chat) { + if (nwo["choices"][0]["finish_reason"] === null) { + let content = nwo["choices"][0]["delta"]["content"]; + if (content !== undefined) { + if (content !== null) { + this.ns.content += content; + } + } else { + let toolCalls = nwo["choices"][0]["delta"]["tool_calls"]; + if ( toolCalls !== undefined) { + if (toolCalls[0]["function"]["name"] !== undefined) { + this.ns.tool_calls.push(toolCalls[0]) + /* + this.ns.tool_calls[0].function.name = toolCalls[0]["function"]["name"]; + this.ns.tool_calls[0].id = toolCalls[0]["id"]; + this.ns.tool_calls[0].type = toolCalls[0]["type"]; + this.ns.tool_calls[0].function.arguments = toolCalls[0]["function"]["arguments"] + */ + } else { + if (toolCalls[0]["function"]["arguments"] !== undefined) { + this.ns.tool_calls[0].function.arguments += toolCalls[0]["function"]["arguments"]; + } + } + } + } + } + } else { + try { + this.ns.content += nwo["choices"][0]["text"]; + } catch { + this.ns.content += nwo["content"]; + } } - console.debug(resp.key, resp.value) - this.response[resp.key] += resp.value; + } + + /** + * Update based on the data got from network in oneshot mode + * @param {any} nwo + * @param {string} apiEP + */ + update_oneshot(nwo, apiEP) { + } has_toolcall() { From 32dd63ee1d695e13abae5535c0a27113a61533dd Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 18:28:49 +0530 Subject: [PATCH 044/446] SimpleChatTC:ChatMessageEx:cleanup, HasToolCalls, ContentEquiv Update HasToolCalls and ContentEquiv to work with new structure --- tools/server/public_simplechat/simplechat.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 608479c862..993094fe42 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -77,7 +77,8 @@ class ChatMessageEx { } /** - * Update based on the drip by drip data got from network in streaming mode + * Update based on the drip by drip data got from network in streaming mode. + * Tries to support both Chat and Completion endpoints * @param {any} nwo * @param {string} apiEP */ @@ -88,12 +89,14 @@ class ChatMessageEx { if (content !== undefined) { if (content !== null) { this.ns.content += content; + } else { + this.ns.role = nwo["choices"][0]["delta"]["role"]; } } else { let toolCalls = nwo["choices"][0]["delta"]["tool_calls"]; - if ( toolCalls !== undefined) { + if (toolCalls !== undefined) { if (toolCalls[0]["function"]["name"] !== undefined) { - this.ns.tool_calls.push(toolCalls[0]) + this.ns.tool_calls.push(toolCalls[0]); /* this.ns.tool_calls[0].function.name = toolCalls[0]["function"]["name"]; this.ns.tool_calls[0].id = toolCalls[0]["id"]; @@ -127,17 +130,17 @@ class ChatMessageEx { } has_toolcall() { - if (this.ns.tool_calls.trim() == "") { + if (this.ns.tool_calls.length == 0) { return false } return true } content_equiv() { - if (this.response.content !== "") { - return this.response.content; + if (this.ns.content !== "") { + return this.ns.content; } else if (this.has_toolcall()) { - return `\n${this.response.toolname}\n${this.response.toolargs}\n`; + return `\n${this.ns.tool_calls[0].function.name}\n${this.ns.tool_calls[0].function.arguments}\n`; } else { return "" } @@ -184,7 +187,7 @@ class SimpleChat { */ this.xchat = []; this.iLastSys = -1; - this.latestResponse = new AssistantResponse(); + this.latestResponse = new ChatMessageEx(); } clear() { From 361f6968d12b3887896dcac1f690a733f2689652 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 18:36:25 +0530 Subject: [PATCH 045/446] SimpleChatTC:ChatMessage: remove ResponseExtractStream Use the equivalent update_stream directly added to ChatMessageEx. update_stream is also more generic to some extent and also directly implemented by the ChatMessageEx class. --- tools/server/public_simplechat/simplechat.js | 42 ++------------------ 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 993094fe42..33c4e4b3f7 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -83,6 +83,7 @@ class ChatMessageEx { * @param {string} apiEP */ update_stream(nwo, apiEP) { + console.debug(nwo, apiEP) if (apiEP == ApiEP.Type.Chat) { if (nwo["choices"][0]["finish_reason"] === null) { let content = nwo["choices"][0]["delta"]["content"]; @@ -408,43 +409,6 @@ class SimpleChat { return assistant; } - /** - * Extract the ai-model/assistant's response from the http response got in streaming mode. - * @param {any} respBody - * @param {string} apiEP - */ - response_extract_stream(respBody, apiEP) { - console.debug(respBody, apiEP) - let key = "content" - let assistant = ""; - if (apiEP == ApiEP.Type.Chat) { - if (respBody["choices"][0]["finish_reason"] === null) { - if (respBody["choices"][0]["delta"]["content"] !== undefined) { - assistant = respBody["choices"][0]["delta"]["content"]; - } else { - if (respBody["choices"][0]["delta"]["tool_calls"] !== undefined) { - if (respBody["choices"][0]["delta"]["tool_calls"][0]["function"]["name"] !== undefined) { - key = "toolname"; - assistant = respBody["choices"][0]["delta"]["tool_calls"][0]["function"]["name"]; - } else { - if (respBody["choices"][0]["delta"]["tool_calls"][0]["function"]["arguments"] !== undefined) { - key = "toolargs"; - assistant = respBody["choices"][0]["delta"]["tool_calls"][0]["function"]["arguments"]; - } - } - } - } - } - } else { - try { - assistant = respBody["choices"][0]["text"]; - } catch { - assistant = respBody["content"]; - } - } - return { key: key, value: assistant }; - } - /** * Allow setting of system prompt, but only at begining. * @param {string} sysPrompt @@ -541,7 +505,7 @@ class SimpleChat { } let curJson = JSON.parse(curLine); console.debug("DBUG:SC:PART:Json:", curJson); - this.latestResponse.append_response(this.response_extract_stream(curJson, apiEP)); + this.latestResponse.update_stream(curJson, apiEP); } elP.innerText = this.latestResponse.content_equiv() elP.scrollIntoView(false); @@ -550,7 +514,7 @@ class SimpleChat { } } console.debug("DBUG:SC:PART:Full:", this.latestResponse.content_equiv()); - return AssistantResponse.newFrom(this.latestResponse); + return ChatMessageEx.newFrom(this.latestResponse); } /** From abbf92755741047d1cef899dc3412aa79678b37e Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 18:52:18 +0530 Subject: [PATCH 046/446] SimpleChatTC:ChatMessageEx: add update_oneshot response_extract logic moved directly into ChatMessageEx as update oneshot, with suitable adjustments. Inturn use the same directly. --- tools/server/public_simplechat/simplechat.js | 39 ++++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 33c4e4b3f7..9b29081a77 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -127,7 +127,15 @@ class ChatMessageEx { * @param {string} apiEP */ update_oneshot(nwo, apiEP) { - + if (apiEP == ApiEP.Type.Chat) { + this.ns.content = nwo["choices"][0]["message"]["content"]; + } else { + try { + this.ns.content = nwo["choices"][0]["text"]; + } catch { + this.ns.content = nwo["content"]; + } + } } has_toolcall() { @@ -389,25 +397,6 @@ class SimpleChat { } } - /** - * Extract the ai-model/assistant's response from the http response got. - * Optionally trim the message wrt any garbage at the end. - * @param {any} respBody - * @param {string} apiEP - */ - response_extract(respBody, apiEP) { - let assistant = new AssistantResponse(); - if (apiEP == ApiEP.Type.Chat) { - assistant.response.content = respBody["choices"][0]["message"]["content"]; - } else { - try { - assistant.response.content = respBody["choices"][0]["text"]; - } catch { - assistant.response.content = respBody["content"]; - } - } - return assistant; - } /** * Allow setting of system prompt, but only at begining. @@ -525,7 +514,9 @@ class SimpleChat { async handle_response_oneshot(resp, apiEP) { let respBody = await resp.json(); console.debug(`DBUG:SimpleChat:SC:${this.chatId}:HandleUserSubmit:RespBody:${JSON.stringify(respBody)}`); - return this.response_extract(respBody, apiEP); + let cm = new ChatMessageEx() + cm.update_oneshot(respBody, apiEP) + return cm } /** @@ -553,9 +544,9 @@ class SimpleChat { theResp = await this.handle_response_oneshot(resp, apiEP); } if (gMe.bTrimGarbage) { - let origMsg = theResp.response.content; - theResp.response.content = du.trim_garbage_at_end(origMsg); - theResp.response.trimmedContent = origMsg.substring(theResp.response.content.length); + let origMsg = theResp.ns.content; + theResp.ns.content = du.trim_garbage_at_end(origMsg); + theResp.trimmedContent = origMsg.substring(theResp.ns.content.length); } this.add(Roles.Assistant, theResp.content_equiv()); return theResp; From 343d414dd3f94b9ffc3cd00ed3b6dde9b8eacefd Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 19:03:48 +0530 Subject: [PATCH 047/446] SimpleChatTC:ChatMessageEx: ods load, system prompt related these have been updated to work with ChatMessageEx to an extent --- tools/server/public_simplechat/simplechat.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 9b29081a77..b7262369bc 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -179,7 +179,7 @@ let gUsageMsg = ` `; -/** @typedef {{role: string, content: string}[]} ChatMessages */ +/** @typedef {ChatMessageEx[]} ChatMessages */ /** @typedef {{iLastSys: number, xchat: ChatMessages}} SimpleChatODS */ @@ -222,7 +222,11 @@ class SimpleChat { /** @type {SimpleChatODS} */ let ods = JSON.parse(sods); this.iLastSys = ods.iLastSys; - this.xchat = ods.xchat; + this.xchat = []; + for (const cur of ods.xchat) { + // TODO: May have to account for missing fields + this.xchat.push(new ChatMessageEx(cur.ns.role, cur.ns.content, cur.ns.tool_calls, cur.trimmedContent)) + } } /** @@ -410,10 +414,10 @@ class SimpleChat { } } else { if (sysPrompt.length > 0) { - if (this.xchat[0].role !== Roles.System) { + if (this.xchat[0].ns.role !== Roles.System) { console.error(`ERRR:SimpleChat:SC:${msgTag}:You need to specify system prompt before any user query, ignoring...`); } else { - if (this.xchat[0].content !== sysPrompt) { + if (this.xchat[0].ns.content !== sysPrompt) { console.error(`ERRR:SimpleChat:SC:${msgTag}:You cant change system prompt, mid way through, ignoring...`); } } @@ -437,7 +441,7 @@ class SimpleChat { return this.add(Roles.System, sysPrompt); } - let lastSys = this.xchat[this.iLastSys].content; + let lastSys = this.xchat[this.iLastSys].ns.content; if (lastSys !== sysPrompt) { return this.add(Roles.System, sysPrompt); } @@ -451,7 +455,7 @@ class SimpleChat { if (this.iLastSys == -1) { return ""; } - let sysPrompt = this.xchat[this.iLastSys].content; + let sysPrompt = this.xchat[this.iLastSys].ns.content; return sysPrompt; } From c65c1d5f0fce3e2cefdeccb708c9a895812c6389 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 19:33:24 +0530 Subject: [PATCH 048/446] SimpleChatTC:ChatMessageEx: RecentChat, GetSystemLatest GetSystemLatest and its users updated wrt ChatMessageEx. RecentChat updated wrt ChatMessageEx. Also now irrespective of whether full history is being retrieved or only a subset, both cases refer to the ChatMessageEx instances in SimpleChat.xchat without creating new instances of anything. --- tools/server/public_simplechat/simplechat.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index b7262369bc..d2d8be73a0 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -248,8 +248,8 @@ class SimpleChat { /** @type{ChatMessages} */ let rchat = []; let sysMsg = this.get_system_latest(); - if (sysMsg.length != 0) { - rchat.push({role: Roles.System, content: sysMsg}); + if (sysMsg.ns.content.length != 0) { + rchat.push(sysMsg) } let iUserCnt = 0; let iStart = this.xchat.length; @@ -258,17 +258,17 @@ class SimpleChat { break; } let msg = this.xchat[i]; - if (msg.role == Roles.User) { + if (msg.ns.role == Roles.User) { iStart = i; iUserCnt += 1; } } for(let i = iStart; i < this.xchat.length; i++) { let msg = this.xchat[i]; - if (msg.role == Roles.System) { + if (msg.ns.role == Roles.System) { continue; } - rchat.push({role: msg.role, content: msg.content}); + rchat.push(msg) } return rchat; } @@ -453,10 +453,9 @@ class SimpleChat { */ get_system_latest() { if (this.iLastSys == -1) { - return ""; + return new ChatMessageEx(Roles.System); } - let sysPrompt = this.xchat[this.iLastSys].ns.content; - return sysPrompt; + return this.xchat[this.iLastSys]; } @@ -882,7 +881,7 @@ class MultiChatUI { console.error(`ERRR:SimpleChat:MCUI:HandleSessionSwitch:${chatId} missing...`); return; } - this.elInSystem.value = chat.get_system_latest(); + this.elInSystem.value = chat.get_system_latest().ns.content; this.elInUser.value = ""; chat.show(this.elDivChat); this.elInUser.focus(); @@ -959,7 +958,7 @@ class Me { chat.load(); queueMicrotask(()=>{ chat.show(div); - this.multiChat.elInSystem.value = chat.get_system_latest(); + this.multiChat.elInSystem.value = chat.get_system_latest().ns.content; }); }); div.appendChild(btn); From 4d9e3d156689e44ea55989cf702629cf1e5d9466 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 21:02:22 +0530 Subject: [PATCH 049/446] SimpleChatTC:ChatMessageEx: Upd Add, rm sysPromptAtBeginOnly hlpr Simplify Add semantic by expecting any validation of stuff before adding to be done by the callers of Add and not by add itself. Also update it to expect ChatMessageEx object Update all users of add to follow the new syntax and semantic. Remove the old and ununsed AddSysPromptOnlyAtBegin helper --- tools/server/public_simplechat/simplechat.js | 61 ++++++-------------- 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index d2d8be73a0..3c50d4f626 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -274,17 +274,14 @@ class SimpleChat { } /** - * Add an entry into xchat + * Add an entry into xchat. + * NOTE: A new copy is created and added into xchat. * Also update iLastSys system prompt index tracker - * @param {string} role - * @param {string|undefined|null} content + * @param {ChatMessageEx} chatMsg */ - add(role, content) { - if ((content == undefined) || (content == null) || (content == "")) { - return false; - } - this.xchat.push( {role: role, content: content} ); - if (role == Roles.System) { + add(chatMsg) { + this.xchat.push(ChatMessageEx.newFrom(chatMsg)); + if (chatMsg.ns.role == Roles.System) { this.iLastSys = this.xchat.length - 1; } this.save(); @@ -402,30 +399,6 @@ class SimpleChat { } - /** - * Allow setting of system prompt, but only at begining. - * @param {string} sysPrompt - * @param {string} msgTag - */ - add_system_begin(sysPrompt, msgTag) { - if (this.xchat.length == 0) { - if (sysPrompt.length > 0) { - return this.add(Roles.System, sysPrompt); - } - } else { - if (sysPrompt.length > 0) { - if (this.xchat[0].ns.role !== Roles.System) { - console.error(`ERRR:SimpleChat:SC:${msgTag}:You need to specify system prompt before any user query, ignoring...`); - } else { - if (this.xchat[0].ns.content !== sysPrompt) { - console.error(`ERRR:SimpleChat:SC:${msgTag}:You cant change system prompt, mid way through, ignoring...`); - } - } - } - } - return false; - } - /** * Allow setting of system prompt, at any time. * Updates the system prompt, if one was never set or if the newly passed is different from the last set system prompt. @@ -438,12 +411,12 @@ class SimpleChat { } if (this.iLastSys < 0) { - return this.add(Roles.System, sysPrompt); + return this.add(new ChatMessageEx(Roles.System, sysPrompt)); } let lastSys = this.xchat[this.iLastSys].ns.content; if (lastSys !== sysPrompt) { - return this.add(Roles.System, sysPrompt); + return this.add(new ChatMessageEx(Roles.System, sysPrompt)); } return false; } @@ -473,6 +446,7 @@ class SimpleChat { let tdUtf8 = new TextDecoder("utf-8"); let rr = resp.body.getReader(); this.latestResponse.clear() + this.latestResponse.ns.role = Roles.Assistant let xLines = new du.NewLines(); while(true) { let { value: cur, done: done } = await rr.read(); @@ -517,7 +491,7 @@ class SimpleChat { async handle_response_oneshot(resp, apiEP) { let respBody = await resp.json(); console.debug(`DBUG:SimpleChat:SC:${this.chatId}:HandleUserSubmit:RespBody:${JSON.stringify(respBody)}`); - let cm = new ChatMessageEx() + let cm = new ChatMessageEx(Roles.Assistant) cm.update_oneshot(respBody, apiEP) return cm } @@ -532,15 +506,16 @@ class SimpleChat { * @param {HTMLDivElement} elDiv */ async handle_response(resp, apiEP, elDiv) { - let theResp = null + let theResp = null; if (gMe.bStream) { try { theResp = await this.handle_response_multipart(resp, apiEP, elDiv); - this.latestResponse.clear() + this.latestResponse.clear(); } catch (error) { theResp = this.latestResponse; - this.add(Roles.Assistant, theResp.content_equiv()); - this.latestResponse.clear() + theResp.ns.role = Roles.Assistant; + this.add(theResp); + this.latestResponse.clear(); throw error; } } else { @@ -551,7 +526,8 @@ class SimpleChat { theResp.ns.content = du.trim_garbage_at_end(origMsg); theResp.trimmedContent = origMsg.substring(theResp.ns.content.length); } - this.add(Roles.Assistant, theResp.content_equiv()); + theResp.ns.role = Roles.Assistant; + this.add(theResp); return theResp; } @@ -761,10 +737,11 @@ class MultiChatUI { chat.add_system_anytime(this.elInSystem.value, chatId); let content = this.elInUser.value; - if (!chat.add(Roles.User, content)) { + if (content.trim() == "") { console.debug(`WARN:SimpleChat:MCUI:${chatId}:HandleUserSubmit:Ignoring empty user input...`); return; } + chat.add(new ChatMessageEx(Roles.User, content)) chat.show(this.elDivChat); let theUrl = ApiEP.Url(gMe.baseURL, apiEP); From 963b9f4661034fd080e093eeb22e361487253af6 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 21:36:11 +0530 Subject: [PATCH 050/446] SimpleChatTC:ChatMessageEx: Recent chat users upd Users of recent_chat updated to work with ChatMessageEx As part of same recent_chat_ns also added, for the case where the array of chat messages can be passed as is ie in the chat mode, provided it has only the network handshake representation of the messages. --- tools/server/public_simplechat/simplechat.js | 29 +++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 3c50d4f626..8bd8a285db 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -273,6 +273,21 @@ class SimpleChat { return rchat; } + + /** + * Return recent chat messages in the format, + * which can be directly sent to the ai server. + * @param {number} iRecentUserMsgCnt - look at recent_chat for semantic + */ + recent_chat_ns(iRecentUserMsgCnt) { + let xchat = this.recent_chat(iRecentUserMsgCnt); + let chat = [] + for (const msg of xchat) { + chat.push(msg.ns) + } + return chat + } + /** * Add an entry into xchat. * NOTE: A new copy is created and added into xchat. @@ -299,8 +314,8 @@ class SimpleChat { } let last = undefined; for(const x of this.recent_chat(gMe.iRecentUserMsgCnt)) { - let entry = ui.el_create_append_p(`${x.role}: ${x.content}`, div); - entry.className = `role-${x.role}`; + let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, div); + entry.className = `role-${x.ns.role}`; last = entry; } if (last !== undefined) { @@ -338,7 +353,7 @@ class SimpleChat { * The needed fields/options are picked from a global object. * Add optional stream flag, if required. * Convert the json into string. - * @param {Object} obj + * @param {Object} obj */ request_jsonstr_extend(obj) { for(let k in gMe.apiRequestOptions) { @@ -358,7 +373,7 @@ class SimpleChat { */ request_messages_jsonstr() { let req = { - messages: this.recent_chat(gMe.iRecentUserMsgCnt), + messages: this.recent_chat_ns(gMe.iRecentUserMsgCnt), } return this.request_jsonstr_extend(req); } @@ -370,15 +385,15 @@ class SimpleChat { request_prompt_jsonstr(bInsertStandardRolePrefix) { let prompt = ""; let iCnt = 0; - for(const chat of this.recent_chat(gMe.iRecentUserMsgCnt)) { + for(const msg of this.recent_chat(gMe.iRecentUserMsgCnt)) { iCnt += 1; if (iCnt > 1) { prompt += "\n"; } if (bInsertStandardRolePrefix) { - prompt += `${chat.role}: `; + prompt += `${msg.ns.role}: `; } - prompt += `${chat.content}`; + prompt += `${msg.ns.content}`; } let req = { prompt: prompt, From 475858a4b3bb853ff245772c9166422d8a6ad4c5 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 22:26:48 +0530 Subject: [PATCH 051/446] SimpleChatTC:ChatMessageEx: Cleanup remaining stuff wrt ChatMessageEx related required flow as well as avoid warnings --- tools/server/public_simplechat/simplechat.js | 27 ++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 8bd8a285db..0d338ebaa1 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -437,7 +437,7 @@ class SimpleChat { } /** - * Retrieve the latest system prompt. + * Retrieve the latest system prompt related chat message entry. */ get_system_latest() { if (this.iLastSys == -1) { @@ -609,19 +609,20 @@ class MultiChatUI { if (el == null) { throw Error(`ERRR:SimpleChat:MCUI:${msgTag} element missing in html...`); } else { + // @ts-ignore console.debug(`INFO:SimpleChat:MCUI:${msgTag} Id[${el.id}] Name[${el["name"]}]`); } } /** * Reset/Setup Tool Call UI parts as needed - * @param {AssistantResponse} ar + * @param {ChatMessageEx} ar */ ui_reset_toolcall_as_needed(ar) { if (ar.has_toolcall()) { this.elDivTool.hidden = false - this.elInToolName.value = ar.response.toolname - this.elInToolArgs.value = ar.response.toolargs + this.elInToolName.value = ar.ns.tool_calls[0].function.name + this.elInToolArgs.value = ar.ns.tool_calls[0].function.arguments this.elBtnTool.disabled = false } else { this.elDivTool.hidden = true @@ -659,7 +660,7 @@ class MultiChatUI { this.handle_session_switch(this.curChatId); } - this.ui_reset_toolcall_as_needed(new AssistantResponse()); + this.ui_reset_toolcall_as_needed(new ChatMessageEx()); this.elBtnSettings.addEventListener("click", (ev)=>{ this.elDivChat.replaceChildren(); @@ -747,7 +748,7 @@ class MultiChatUI { chat.clear(); } - this.ui_reset_toolcall_as_needed(new AssistantResponse()); + this.ui_reset_toolcall_as_needed(new ChatMessageEx()); chat.add_system_anytime(this.elInSystem.value, chatId); @@ -775,8 +776,8 @@ class MultiChatUI { let theResp = await chat.handle_response(resp, apiEP, this.elDivChat); if (chatId == this.curChatId) { chat.show(this.elDivChat); - if (theResp.response.trimmedContent.length > 0) { - let p = ui.el_create_append_p(`TRIMMED:${theResp.response.trimmedContent}`, this.elDivChat); + if (theResp.trimmedContent.length > 0) { + let p = ui.el_create_append_p(`TRIMMED:${theResp.trimmedContent}`, this.elDivChat); p.className="role-trim"; } } else { @@ -847,6 +848,11 @@ class MultiChatUI { } } + /** + * Create session button and append to specified Div element. + * @param {HTMLDivElement} elDiv + * @param {string} cid + */ create_session_btn(elDiv, cid) { let btn = ui.el_create_button(cid, (ev)=>{ let target = /** @type{HTMLButtonElement} */(ev.target); @@ -896,6 +902,7 @@ class Me { this.bCompletionInsertStandardRolePrefix = false; this.bTrimGarbage = true; this.iRecentUserMsgCnt = 5; + /** @type {Object} */ this.sRecentUserMsgCnt = { "Full": -1, "Last0": 1, @@ -1063,6 +1070,7 @@ class Me { this.show_settings_apirequestoptions(elDiv); let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ + // @ts-ignore this.apiEP = ApiEP.Type[val]; }); elDiv.appendChild(sel.div); @@ -1094,8 +1102,11 @@ function startme() { console.log("INFO:SimpleChat:StartMe:Starting..."); gMe = new Me(); gMe.debug_disable(); + // @ts-ignore document["gMe"] = gMe; + // @ts-ignore document["du"] = du; + // @ts-ignore document["tools"] = tools; tools.init() for (let cid of gMe.defaultChatIds) { From 2ef201ff8dd4d955de7605be0ab16818316ac194 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 14 Oct 2025 23:27:03 +0530 Subject: [PATCH 052/446] SimpleChatTC:Load allows old and new ChatMessage(Ex) formats --- tools/server/public_simplechat/simplechat.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 0d338ebaa1..ce09e764dc 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -224,8 +224,13 @@ class SimpleChat { this.iLastSys = ods.iLastSys; this.xchat = []; for (const cur of ods.xchat) { - // TODO: May have to account for missing fields - this.xchat.push(new ChatMessageEx(cur.ns.role, cur.ns.content, cur.ns.tool_calls, cur.trimmedContent)) + if (cur.ns == undefined) { + /** @typedef {{role: string, content: string}} OldChatMessage */ + let tcur = /** @type {OldChatMessage} */(/** @type {unknown} */(cur)); + this.xchat.push(new ChatMessageEx(tcur.role, tcur.content)) + } else { + this.xchat.push(new ChatMessageEx(cur.ns.role, cur.ns.content, cur.ns.tool_calls, cur.trimmedContent)) + } } } From 2bb3d747e6f52363ccfaa3928cbbdaf52fa8e203 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 00:04:29 +0530 Subject: [PATCH 053/446] SimpleChatTC:ChatMessageEx: send tool_calls, only if needed --- tools/server/public_simplechat/simplechat.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index ce09e764dc..2b82a4648d 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -11,6 +11,7 @@ class Roles { static System = "system"; static User = "user"; static Assistant = "assistant"; + static Tool = "tool"; } class ApiEP { @@ -286,9 +287,14 @@ class SimpleChat { */ recent_chat_ns(iRecentUserMsgCnt) { let xchat = this.recent_chat(iRecentUserMsgCnt); - let chat = [] + let chat = []; for (const msg of xchat) { - chat.push(msg.ns) + let tmsg = ChatMessageEx.newFrom(msg); + if (!tmsg.has_toolcall()) { + // @ts-ignore + delete(tmsg.ns.tool_calls) + } + chat.push(tmsg.ns); } return chat } From ebc7f88b5337b271c68047b834ccf306b7e27e60 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 01:20:27 +0530 Subject: [PATCH 054/446] SimpleChatTC:Propogate toolcall id through tool call chain Use HTMLElement's dataset to maintain tool call id along with the element which maintains the toolname. Pass it along to the tools manager and inturn the actual tool calls and through them to the web worker handling the tool call related code and inturn returning it back as part of the obj which is used to return the tool call result. Embed the tool call id, function name and function result into the content field of chat message in terms of a xml structure Also make use of tool role to send back the tool call result. Do note that currently the id, name and content are all embedded into the content field of the tool role message sent to the ai engine on the server. NOTE: Use the user query entry area for showing tool call result in the above mentioned xml form, as well as for user to enter their own queries. Based on presence of the xml format data at beginning the logic will treat it has a tool result and if not then as a normal user query. The css has been updated to help show tool results/msgs in a lightyellow background --- tools/server/public_simplechat/simplechat.css | 3 ++ tools/server/public_simplechat/simplechat.js | 46 +++++++++++++++---- tools/server/public_simplechat/tooljs.mjs | 10 ++-- tools/server/public_simplechat/tools.mjs | 9 ++-- .../server/public_simplechat/toolsworker.mjs | 2 +- 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.css b/tools/server/public_simplechat/simplechat.css index 13bfb80b48..d4755074b7 100644 --- a/tools/server/public_simplechat/simplechat.css +++ b/tools/server/public_simplechat/simplechat.css @@ -21,6 +21,9 @@ .role-user { background-color: lightgray; } +.role-tool { + background-color: lightyellow; +} .role-trim { background-color: lightpink; } diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 2b82a4648d..281b2c15b9 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -77,6 +77,17 @@ class ChatMessageEx { this.trimmedContent = ""; } + /** + * Create a all in one tool call result string + * @param {string} toolCallId + * @param {string} toolName + * @param {string} toolResult + */ + static createToolCallResultAllInOne(toolCallId, toolName, toolResult) { + return ` ${toolCallId} ${toolName} ${toolResult} `; + } + + /** * Update based on the drip by drip data got from network in streaming mode. * Tries to support both Chat and Completion endpoints @@ -561,15 +572,16 @@ class SimpleChat { * Call the requested tool/function. * Returns undefined, if the call was placed successfully * Else some appropriate error message will be returned. + * @param {string} toolcallid * @param {string} toolname * @param {string} toolargs */ - async handle_toolcall(toolname, toolargs) { + async handle_toolcall(toolcallid, toolname, toolargs) { if (toolname === "") { return "Tool/Function call name not specified" } try { - return await tools.tool_call(toolname, toolargs) + return await tools.tool_call(toolcallid, toolname, toolargs) } catch (/** @type {any} */error) { return `Tool/Function call raised an exception:${error.name}:${error.message}` } @@ -633,11 +645,13 @@ class MultiChatUI { if (ar.has_toolcall()) { this.elDivTool.hidden = false this.elInToolName.value = ar.ns.tool_calls[0].function.name + this.elInToolName.dataset.tool_call_id = ar.ns.tool_calls[0].id this.elInToolArgs.value = ar.ns.tool_calls[0].function.arguments this.elBtnTool.disabled = false } else { this.elDivTool.hidden = true this.elInToolName.value = "" + this.elInToolName.dataset.tool_call_id = "" this.elInToolArgs.value = "" this.elBtnTool.disabled = true } @@ -697,9 +711,9 @@ class MultiChatUI { this.handle_tool_run(this.curChatId); }) - tools.setup((name, data)=>{ + tools.setup((id, name, data)=>{ clearTimeout(this.idTimeOut) - this.elInUser.value = `${data}` + this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(id, name, data); this.ui_reset_userinput(false) }) @@ -744,6 +758,14 @@ class MultiChatUI { /** * Handle user query submit request, wrt specified chat session. + * NOTE: Currently the user query entry area is used for + * * showing and allowing edits by user wrt tool call results + * in a predfined simple xml format, + * ie before they submit tool result to ai engine on server + * * as well as for user to enter their own queries. + * Based on presence of the predefined xml format data at beginning + * the logic will treat it has a tool result and if not then as a + * normal user query. * @param {string} chatId * @param {string} apiEP */ @@ -768,7 +790,11 @@ class MultiChatUI { console.debug(`WARN:SimpleChat:MCUI:${chatId}:HandleUserSubmit:Ignoring empty user input...`); return; } - chat.add(new ChatMessageEx(Roles.User, content)) + if (content.startsWith("")) { + chat.add(new ChatMessageEx(Roles.Tool, content)) + } else { + chat.add(new ChatMessageEx(Roles.User, content)) + } chat.show(this.elDivChat); let theUrl = ApiEP.Url(gMe.baseURL, apiEP); @@ -808,13 +834,17 @@ class MultiChatUI { this.elInUser.value = "toolcall in progress..."; this.elInUser.disabled = true; let toolname = this.elInToolName.value.trim() - let toolResult = await chat.handle_toolcall(toolname, this.elInToolArgs.value) + let toolCallId = this.elInToolName.dataset.tool_call_id; + if (toolCallId === undefined) { + toolCallId = "??? ToolCallId Missing ???" + } + let toolResult = await chat.handle_toolcall(toolCallId, toolname, this.elInToolArgs.value) if (toolResult !== undefined) { - this.elInUser.value = `${toolResult}` + this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, toolResult); this.ui_reset_userinput(false) } else { this.idTimeOut = setTimeout(() => { - this.elInUser.value = `Tool/Function call ${toolname} taking too much time, aborting...` + this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, `Tool/Function call ${toolname} taking too much time, aborting...`); this.ui_reset_userinput(false) }, 10000) } diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 6aea9a5ee4..a44333ca1b 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -32,11 +32,12 @@ let js_meta = { /** * Implementation of the javascript interpretor logic. Minimal skeleton for now. * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function js_run(toolname, obj) { - gToolsWorker.postMessage({ name: toolname, code: obj["code"]}) +function js_run(toolcallid, toolname, obj) { + gToolsWorker.postMessage({ id: toolcallid, name: toolname, code: obj["code"]}) } @@ -62,11 +63,12 @@ let calc_meta = { /** * Implementation of the simple calculator logic. Minimal skeleton for now. * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function calc_run(toolname, obj) { - gToolsWorker.postMessage({ name: toolname, code: `console.log(${obj["arithexpr"]})`}) +function calc_run(toolcallid, toolname, obj) { + gToolsWorker.postMessage({ id: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 75fe56e4f4..8c89e96525 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -32,11 +32,11 @@ export function meta() { /** * Setup the callback that will be called when ever message * is recieved from the Tools Web Worker. - * @param {(name: string, data: string) => void} cb + * @param {(id: string, name: string, data: string) => void} cb */ export function setup(cb) { gToolsWorker.onmessage = function (ev) { - cb(ev.data.name, ev.data.data) + cb(ev.data.id, ev.data.name, ev.data.data) } } @@ -45,14 +45,15 @@ export function setup(cb) { * Try call the specified tool/function call. * Returns undefined, if the call was placed successfully * Else some appropriate error message will be returned. + * @param {string} toolcallid * @param {string} toolname * @param {string} toolargs */ -export async function tool_call(toolname, toolargs) { +export async function tool_call(toolcallid, toolname, toolargs) { for (const fn in tc_switch) { if (fn == toolname) { try { - tc_switch[fn]["handler"](fn, JSON.parse(toolargs)) + tc_switch[fn]["handler"](toolcallid, fn, JSON.parse(toolargs)) return undefined } catch (/** @type {any} */error) { return `Tool/Function call raised an exception:${error.name}:${error.message}` diff --git a/tools/server/public_simplechat/toolsworker.mjs b/tools/server/public_simplechat/toolsworker.mjs index e370fd0a9d..590c45234b 100644 --- a/tools/server/public_simplechat/toolsworker.mjs +++ b/tools/server/public_simplechat/toolsworker.mjs @@ -21,5 +21,5 @@ self.onmessage = function (ev) { console.log(`\n\nTool/Function call "${ev.data.name}" raised an exception:${error.name}:${error.message}\n\n`) } tconsole.console_revert() - self.postMessage({ name: ev.data.name, data: tconsole.gConsoleStr}) + self.postMessage({ id: ev.data.id, name: ev.data.name, data: tconsole.gConsoleStr}) } From cc65a2f7a3e98e25ce932b16e1b345e6011b4a7f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 03:28:48 +0530 Subject: [PATCH 055/446] SimpleChatTC:ChatMessageEx: Build tool role result fully Expand the xml format id, name and content in content field of tool result into apropriate fields in the tool result message sent to the genai/llm engine on the server. --- tools/server/public_simplechat/simplechat.js | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 281b2c15b9..847115cb8e 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -87,6 +87,33 @@ class ChatMessageEx { return ` ${toolCallId} ${toolName} ${toolResult} `; } + /** + * Extract the elements of the all in one tool call result string + * @param {string} allInOne + */ + static extractToolCallResultAllInOne(allInOne) { + const regex = /\s*(.*?)<\/id>\s*(.*?)<\/name>\s*([\s\S]*?)<\/content>\s*<\/tool_response>/si; + const caught = allInOne.match(regex) + let data = { tool_call_id: "Error", name: "Error", content: "Error" } + if (caught) { + data = { + tool_call_id: caught[1].trim(), + name: caught[2].trim(), + content: caught[3].trim() + } + } + return data + } + + /** + * Set extra members into the ns object + * @param {string | number} key + * @param {any} value + */ + ns_set_extra(key, value) { + // @ts-ignore + this.ns[key] = value + } /** * Update based on the drip by drip data got from network in streaming mode. @@ -305,6 +332,12 @@ class SimpleChat { // @ts-ignore delete(tmsg.ns.tool_calls) } + if (tmsg.ns.role == Roles.Tool) { + let res = ChatMessageEx.extractToolCallResultAllInOne(tmsg.ns.content) + tmsg.ns.content = res.content + tmsg.ns_set_extra("tool_call_id", res.tool_call_id) + tmsg.ns_set_extra("name", res.name) + } chat.push(tmsg.ns); } return chat From 152deb5d5aa08ced28c0a43bb18a4e99705c251a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 03:35:55 +0530 Subject: [PATCH 056/446] SimpleChatTC:ChatMessageEx:While at it also ns_delete these common helpers avoid needing ignore tagging to ts-check, in places where valid constructs have been used which go beyond strict structured js handling that is tried to be achieved using it, but are still valid and legal. --- tools/server/public_simplechat/simplechat.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 847115cb8e..bc44035162 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -115,6 +115,15 @@ class ChatMessageEx { this.ns[key] = value } + /** + * Remove specified key and its value from ns object + * @param {string | number} key + */ + ns_delete(key) { + // @ts-ignore + delete(this.ns[key]) + } + /** * Update based on the drip by drip data got from network in streaming mode. * Tries to support both Chat and Completion endpoints @@ -329,8 +338,7 @@ class SimpleChat { for (const msg of xchat) { let tmsg = ChatMessageEx.newFrom(msg); if (!tmsg.has_toolcall()) { - // @ts-ignore - delete(tmsg.ns.tool_calls) + tmsg.ns_delete("tool_calls") } if (tmsg.ns.role == Roles.Tool) { let res = ChatMessageEx.extractToolCallResultAllInOne(tmsg.ns.content) From 61b70bfa5df8b0b464fcb6b6ae1be441c35fa579 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 04:13:34 +0530 Subject: [PATCH 057/446] SimpleChatTC:Readme: Updated wrt new relativelyProper toolCallsHS Also update the sliding window context size to last 9 chat messages so that there is a sufficiently large context for multi turn tool calls based adjusting by ai and user, without needing to go full hog, which has the issue of overflowing the currently set context window wrt the loaded ai model. --- tools/server/public_simplechat/readme.md | 20 ++++++++++++++------ tools/server/public_simplechat/simplechat.js | 5 +++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index c8cb786c3c..d50588cce5 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -136,8 +136,9 @@ Once inside called, then the ai response might include request for tool call. * the SimpleChat client will show details of the tool call (ie tool name and args passed) requested and allow the user to trigger it as is or after modifying things as needed. + NOTE: Tool sees the original tool call only, for now * inturn returned / generated result is placed into user query entry text area with approriate tags - ie generated result + ie generated result with meta data * if user is ok with the tool response, they can click submit to send the same to the GenAi/LLM. User can even modify the response generated by the tool, if required, before submitting. @@ -193,7 +194,7 @@ It is attached to the document object. Some of these can also be updated using t sent back to the ai model, under user control. as tool calling will involve a bit of back and forth between ai assistant and end user, it is - recommended to set iRecentUserMsgCnt to 5 or more, so that enough context is retained during + recommended to set iRecentUserMsgCnt to 10 or more, so that enough context is retained during chatting with ai models with tool support. apiEP - select between /completions and /chat/completions endpoint provided by the server/ai-model. @@ -239,7 +240,7 @@ It is attached to the document object. Some of these can also be updated using t be set if needed using the settings ui. iRecentUserMsgCnt - a simple minded SlidingWindow to limit context window load at Ai Model end. - This is set to 5 by default. So in addition to latest system message, last/latest iRecentUserMsgCnt + This is set to 10 by default. So in addition to latest system message, last/latest iRecentUserMsgCnt user messages after the latest system prompt and its responses from the ai model will be sent to the ai-model, when querying for a new response. Note that if enabled, only user messages after the latest system message/prompt will be considered. @@ -325,7 +326,7 @@ work. ALERT: The simple minded way in which this is implemented, it can be dangerous in the worst case, Always remember to verify all the tool calls requested and the responses generated manually to -ensure everything is fine, during interaction with ai modles with tools support. +ensure everything is fine, during interaction with ai models with tools support. #### Builtin Tools @@ -358,9 +359,11 @@ that should be sent back to the ai model, in your constructed code. Update the tc_switch to include a object entry for the tool, which inturn includes * the meta data as well as * a reference to the handler and also + the handler should take toolCallId, toolName and toolArgs and pass these along to + web worker as needed. * the result key (was used previously, may use in future, but for now left as is) -#### Mapping tool calls and responses to normal assistant - user chat flow +#### OLD: Mapping tool calls and responses to normal assistant - user chat flow Instead of maintaining tool_call request and resultant response in logically seperate parallel channel used for requesting tool_calls by the assistant and the resulstant tool role response, @@ -375,10 +378,14 @@ NOTE: This flow tested to be ok enough with Gemma-3N-E4B-it-Q8_0 LLM ai model fo given the way current ai models work, most of them should understand things as needed, but need to test this with other ai models later. -TODO: Need to think later, whether to continue this simple flow, or atleast use tool role wrt +TODO:OLD: Need to think later, whether to continue this simple flow, or atleast use tool role wrt the tool call responses or even go further and have the logically seperate tool_calls request structures also. +DONE: rather both tool_calls structure wrt assistant messages and tool role based tool call +result messages are generated as needed. + + #### ToDo WebFetch and Local web proxy/caching server @@ -386,6 +393,7 @@ WebFetch and Local web proxy/caching server Try and trap promises based flows to ensure all generated results or errors if any are caught before responding back to the ai model. +Trap error responses. ### Debuging the handshake diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index bc44035162..9363063123 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -219,7 +219,7 @@ let gUsageMsg = `
      • submit tool response placed into user query textarea
      -
    • Default ContextWindow = [System, Last4 Query+Resp, Cur Query].
    • +
    • Default ContextWindow = [System, Last9 Query+Resp, Cur Query].
      • ChatHistInCtxt, MaxTokens, ModelCtxt window to expand
      @@ -983,7 +983,7 @@ class Me { this.bCompletionFreshChatAlways = true; this.bCompletionInsertStandardRolePrefix = false; this.bTrimGarbage = true; - this.iRecentUserMsgCnt = 5; + this.iRecentUserMsgCnt = 10; /** @type {Object} */ this.sRecentUserMsgCnt = { "Full": -1, @@ -991,6 +991,7 @@ class Me { "Last1": 2, "Last2": 3, "Last4": 5, + "Last9": 10, }; this.apiEP = ApiEP.Type.Chat; /** @type {Object} */ From 7dbbc46390adb6f0a1ff461f2464e80f5d3c1b36 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 05:31:08 +0530 Subject: [PATCH 058/446] SimpleChatTC:ChatMessageEx: Better tool result extractor --- tools/server/public_simplechat/simplechat.js | 25 +++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 9363063123..c7259fb950 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -91,7 +91,7 @@ class ChatMessageEx { * Extract the elements of the all in one tool call result string * @param {string} allInOne */ - static extractToolCallResultAllInOne(allInOne) { + static extractToolCallResultAllInOneSimpleMinded(allInOne) { const regex = /\s*(.*?)<\/id>\s*(.*?)<\/name>\s*([\s\S]*?)<\/content>\s*<\/tool_response>/si; const caught = allInOne.match(regex) let data = { tool_call_id: "Error", name: "Error", content: "Error" } @@ -105,6 +105,29 @@ class ChatMessageEx { return data } + /** + * Extract the elements of the all in one tool call result string + * This should potentially account for content tag having xml content within to an extent. + * @param {string} allInOne + */ + static extractToolCallResultAllInOne(allInOne) { + const dParser = new DOMParser(); + const got = dParser.parseFromString(allInOne, 'text/xml'); + const parseErrors = got.querySelector('parseerror') + if (parseErrors) { + console.debug("WARN:ChatMessageEx:ExtractToolCallResultAllInOne:", parseErrors.textContent.trim()) + } + const id = got.querySelector('id')?.textContent.trim(); + const name = got.querySelector('name')?.textContent.trim(); + const content = got.querySelector('content')?.textContent.trim(); + let data = { + tool_call_id: id? id : "Error", + name: name? name : "Error", + content: content? content : "Error" + } + return data + } + /** * Set extra members into the ns object * @param {string | number} key From 3d661793ef5b3f1d979a57a6675d9adb63cd2914 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 18:20:20 +0530 Subject: [PATCH 059/446] SimpleChatTC:ChatMessageEx: 1st go at trying to track promises --- .../server/public_simplechat/toolsworker.mjs | 9 ++-- tools/server/public_simplechat/xpromise.mjs | 46 +++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 tools/server/public_simplechat/xpromise.mjs diff --git a/tools/server/public_simplechat/toolsworker.mjs b/tools/server/public_simplechat/toolsworker.mjs index 590c45234b..d1c7a2e42b 100644 --- a/tools/server/public_simplechat/toolsworker.mjs +++ b/tools/server/public_simplechat/toolsworker.mjs @@ -5,18 +5,19 @@ // /** - * Expects to get a message with identifier name and code to run - * Posts message with identifier name and data captured from console.log outputs + * Expects to get a message with id, name and code to run + * Posts message with id, name and data captured from console.log outputs */ import * as tconsole from "./toolsconsole.mjs" +import * as xpromise from "./xpromise.mjs" -self.onmessage = function (ev) { +self.onmessage = async function (ev) { tconsole.console_redir() try { - eval(ev.data.code) + await xpromise.evalWithPromiseTracking(ev.data.code); } catch (/** @type {any} */error) { console.log(`\n\nTool/Function call "${ev.data.name}" raised an exception:${error.name}:${error.message}\n\n`) } diff --git a/tools/server/public_simplechat/xpromise.mjs b/tools/server/public_simplechat/xpromise.mjs new file mode 100644 index 0000000000..de3612d764 --- /dev/null +++ b/tools/server/public_simplechat/xpromise.mjs @@ -0,0 +1,46 @@ +//@ts-check +// Helpers for a tracked promise land +// by Humans for All +// + + +/** + * @typedef {(resolve: (value: any) => void, reject: (reason?: any) => void) => void} PromiseExecutor + */ + + +/** + * Eval which allows promises generated by the evald code to be tracked. + * @param {string} codeToEval + */ +export function evalWithPromiseTracking(codeToEval) { + const _Promise = globalThis.Promise; + /** @type {any[]} */ + const trackedPromises = []; + + const Promise = function ( /** @type {PromiseExecutor} */ executor) { + const promise = new _Promise(executor); + trackedPromises.push(promise); + + promise.then = function (...args) { + const newPromise = _Promise.prototype.then.apply(this, args); + trackedPromises.push(newPromise); + return newPromise; + }; + + promise.catch = function (...args) { + const newPromise = _Promise.prototype.catch.apply(this, args); + trackedPromises.push(newPromise); + return newPromise; + }; + + return promise; + }; + + Promise.prototype = _Promise.prototype; + Object.assign(Promise, _Promise); + + eval(codeToEval); + + return _Promise.all(trackedPromises); +} From 0241b7b46946b3d93f94cfe4cef6eed44d1268f8 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 21:48:18 +0530 Subject: [PATCH 060/446] SimpleChatTC:TrapPromise: log the trapping also possible refinement wrt trapping, if needed, added as comment all or allSettled to use or not is the question. whether to wait for a round trip through the related event loop or not is also a question. --- tools/server/public_simplechat/toolsworker.mjs | 2 ++ tools/server/public_simplechat/xpromise.mjs | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/toolsworker.mjs b/tools/server/public_simplechat/toolsworker.mjs index d1c7a2e42b..b85b83b33b 100644 --- a/tools/server/public_simplechat/toolsworker.mjs +++ b/tools/server/public_simplechat/toolsworker.mjs @@ -15,6 +15,7 @@ import * as xpromise from "./xpromise.mjs" self.onmessage = async function (ev) { + console.info("DBUG:WW:OnMessage started...") tconsole.console_redir() try { await xpromise.evalWithPromiseTracking(ev.data.code); @@ -23,4 +24,5 @@ self.onmessage = async function (ev) { } tconsole.console_revert() self.postMessage({ id: ev.data.id, name: ev.data.name, data: tconsole.gConsoleStr}) + console.info("DBUG:WW:OnMessage done") } diff --git a/tools/server/public_simplechat/xpromise.mjs b/tools/server/public_simplechat/xpromise.mjs index de3612d764..7134293c0b 100644 --- a/tools/server/public_simplechat/xpromise.mjs +++ b/tools/server/public_simplechat/xpromise.mjs @@ -13,22 +13,25 @@ * Eval which allows promises generated by the evald code to be tracked. * @param {string} codeToEval */ -export function evalWithPromiseTracking(codeToEval) { +export async function evalWithPromiseTracking(codeToEval) { const _Promise = globalThis.Promise; /** @type {any[]} */ const trackedPromises = []; const Promise = function ( /** @type {PromiseExecutor} */ executor) { + console.info("WW:PT:Promise") const promise = new _Promise(executor); trackedPromises.push(promise); promise.then = function (...args) { + console.info("WW:PT:Then") const newPromise = _Promise.prototype.then.apply(this, args); trackedPromises.push(newPromise); return newPromise; }; promise.catch = function (...args) { + console.info("WW:PT:Catch") const newPromise = _Promise.prototype.catch.apply(this, args); trackedPromises.push(newPromise); return newPromise; @@ -42,5 +45,7 @@ export function evalWithPromiseTracking(codeToEval) { eval(codeToEval); + //await Promise(resolve=>setTimeout(resolve, 0)); + //return _Promise.allSettled(trackedPromises); return _Promise.all(trackedPromises); } From 92e5b2133e4328d3f655f0b8ba7892d427f858e0 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 22:10:27 +0530 Subject: [PATCH 061/446] SimpleChatTC:Promises: trap normal fetch (dont care await or not) --- tools/server/public_simplechat/xpromise.mjs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/server/public_simplechat/xpromise.mjs b/tools/server/public_simplechat/xpromise.mjs index 7134293c0b..94d33ac353 100644 --- a/tools/server/public_simplechat/xpromise.mjs +++ b/tools/server/public_simplechat/xpromise.mjs @@ -15,6 +15,8 @@ */ export async function evalWithPromiseTracking(codeToEval) { const _Promise = globalThis.Promise; + const _fetch = globalThis.fetch + /** @type {any[]} */ const trackedPromises = []; @@ -43,6 +45,13 @@ export async function evalWithPromiseTracking(codeToEval) { Promise.prototype = _Promise.prototype; Object.assign(Promise, _Promise); + const fetch = function(/** @type {any[]} */ ...args) { + console.info("WW:PT:Fetch") + const fpromise = _fetch(args); + trackedPromises.push(fpromise) + return fpromise; + } + eval(codeToEval); //await Promise(resolve=>setTimeout(resolve, 0)); From 77d3e43cb49b80266348179263a17f78ec66e971 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 22:52:07 +0530 Subject: [PATCH 062/446] SimpleChatTC: Allow await in generated code that will be evald --- tools/server/public_simplechat/xpromise.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/xpromise.mjs b/tools/server/public_simplechat/xpromise.mjs index 94d33ac353..801ef7adc6 100644 --- a/tools/server/public_simplechat/xpromise.mjs +++ b/tools/server/public_simplechat/xpromise.mjs @@ -25,6 +25,7 @@ export async function evalWithPromiseTracking(codeToEval) { const promise = new _Promise(executor); trackedPromises.push(promise); + // @ts-ignore promise.then = function (...args) { console.info("WW:PT:Then") const newPromise = _Promise.prototype.then.apply(this, args); @@ -47,12 +48,15 @@ export async function evalWithPromiseTracking(codeToEval) { const fetch = function(/** @type {any[]} */ ...args) { console.info("WW:PT:Fetch") + // @ts-ignore const fpromise = _fetch(args); trackedPromises.push(fpromise) return fpromise; } - eval(codeToEval); + //let tf = new Function(codeToEval); + //await tf() + await eval(`(async () => { ${codeToEval} })()`); //await Promise(resolve=>setTimeout(resolve, 0)); //return _Promise.allSettled(trackedPromises); From f0a3886d1e88e39eca4cfd8969d5201964eb745c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 23:13:02 +0530 Subject: [PATCH 063/446] SimpleChatTC:Ensure fetch's promise chain is also trapped Dont forget to map members of got entity from fetch to things from saved original promise, bcas remember what is got is a promise. also add some comments around certain decisions and needed exploration --- tools/server/public_simplechat/xpromise.mjs | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/xpromise.mjs b/tools/server/public_simplechat/xpromise.mjs index 801ef7adc6..6f001ef9de 100644 --- a/tools/server/public_simplechat/xpromise.mjs +++ b/tools/server/public_simplechat/xpromise.mjs @@ -1,5 +1,6 @@ //@ts-check // Helpers for a tracked promise land +// Traps regular promise as well as promise by fetch // by Humans for All // @@ -51,14 +52,36 @@ export async function evalWithPromiseTracking(codeToEval) { // @ts-ignore const fpromise = _fetch(args); trackedPromises.push(fpromise) + + // @ts-ignore + fpromise.then = function (...args) { + console.info("WW:PT:FThen") + const newPromise = _Promise.prototype.then.apply(this, args); + trackedPromises.push(newPromise); + return newPromise; + }; + + fpromise.catch = function (...args) { + console.info("WW:PT:FCatch") + const newPromise = _Promise.prototype.catch.apply(this, args); + trackedPromises.push(newPromise); + return newPromise; + }; + return fpromise; } + fetch.prototype = _fetch.prototype; + Object.assign(fetch, _fetch); + //let tf = new Function(codeToEval); //await tf() await eval(`(async () => { ${codeToEval} })()`); + // Should I allow things to go back to related event loop once //await Promise(resolve=>setTimeout(resolve, 0)); - //return _Promise.allSettled(trackedPromises); + + // Need and prefer promise failures to be trapped using reject/catch logic + // so using all instead of allSettled. return _Promise.all(trackedPromises); } From 09ce19a95a8f7530cdfe77427793bd6e65dbf4aa Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 15 Oct 2025 23:31:07 +0530 Subject: [PATCH 064/446] SimpleChatTC: update readme wrt promise related trapping --- tools/server/public_simplechat/readme.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index d50588cce5..0ae62a3a7b 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -385,16 +385,22 @@ structures also. DONE: rather both tool_calls structure wrt assistant messages and tool role based tool call result messages are generated as needed. +#### Related stuff + +Promise as well as users of promise (for now fetch) have been trapped wrt their then and catch flow, +so that any scheduled asynchronous code or related async error handling using promise mechanism also +gets executed, before tool calling returns and thus data / error generated by those async code also +get incorporated in result sent to ai engine on the server side. #### ToDo WebFetch and Local web proxy/caching server -Try and trap promises based flows to ensure all generated results or errors if any are caught -before responding back to the ai model. +Is the promise land trap deep enough, need to think through and explore around this once later. Trap error responses. + ### Debuging the handshake When working with llama.cpp server based GenAi/LLM running locally From 8fc74ef92333f6271e4c9251246972f39d880c26 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 16 Oct 2025 11:52:21 +0530 Subject: [PATCH 065/446] SimpleChatTC:WebFetchThroughProxy:Initial go creating request --- tools/server/public_simplechat/tooljs.mjs | 47 +++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index a44333ca1b..9591aeef4b 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -72,6 +72,48 @@ function calc_run(toolcallid, toolname, obj) { } +let weburlfetch_meta = { + "type": "function", + "function": { + "name": "web_url_fetch", + "description": "Fetch the requested web url through a proxy server in few seconds", + "parameters": { + "type": "object", + "properties": { + "url":{ + "type":"string", + "description":"the url of the page / content to fetch from the internet" + } + }, + "required": ["url"] + } + } + } + + +/** + * Implementation of the web url logic. Dumb initial go. + * Expects a simple minded proxy server to be running locally + * * listening on port 3128 + * * expecting http requests + * * with a query token named path which gives the actual url to fetch + * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function weburlfetch_run(toolcallid, toolname, obj) { + if (gToolsWorker.onmessage != null) { + let newUrl = `http://127.0.0.1:3128/?path=${obj.url}` + fetch(newUrl).then(resp=>resp.text()).then(data => { + gToolsWorker.onmessage(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) + }).catch((err)=>{ + gToolsWorker.onmessage(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) + }) + } +} + + /** * @type {Object>} */ @@ -85,6 +127,11 @@ export let tc_switch = { "handler": calc_run, "meta": calc_meta, "result": "" + }, + "web_url_fetch": { + "handler": weburlfetch_run, + "meta": weburlfetch_meta, + "result": "" } } From 05c0ade8beb073a5226a2dde4f6b8b98ade94025 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 16 Oct 2025 17:45:33 +0530 Subject: [PATCH 066/446] SimpleChatTC:SimpleProxy:Process args --port --- .../local.tools/simpleproxy.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tools/server/public_simplechat/local.tools/simpleproxy.py diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py new file mode 100644 index 0000000000..85940e60d8 --- /dev/null +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -0,0 +1,37 @@ +# A simple proxy server +# by Humans for All +# +# Listens on the specified port (defaults to squids 3128) +# * if a url query is got (http://localhost:3128/?url=http://site.of.interest/path/of/interest) +# fetches the contents of the specified url and returns the same to the requester +# + +import sys + +gMe = {} + +def process_args(args: list[str]): + global gMe + gMe['INTERNAL.ProcessArgs.Malformed'] = [] + gMe['INTERNAL.ProcessArgs.Unknown'] = [] + iArg = 1 + while iArg < len(args): + cArg = args[iArg] + if (not cArg.startswith("--")): + gMe['INTERNAL.ProcessArgs.Malformed'].append(cArg) + print(f"WARN:ProcessArgs:{iArg}:IgnoringMalformedCommandOr???:{cArg}") + iArg += 1 + continue + match cArg: + case '--port': + iArg += 1 + gMe[cArg] = args[iArg] + iArg += 1 + case _: + gMe['INTERNAL.ProcessArgs.Unknown'].append(cArg) + print(f"WARN:ProcessArgs:{iArg}:IgnoringUnknownCommand:{cArg}") + iArg += 1 + + +if __name__ == "__main__": + process_args(sys.argv) From 80fd065993b4649b2dfdba2b3f4a0f6ab6460af7 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 16 Oct 2025 18:06:56 +0530 Subject: [PATCH 067/446] SimpleChatTC:SimpleProxy: Start server, Show requested path --- .../local.tools/simpleproxy.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 85940e60d8..c9d7264b7a 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -6,10 +6,20 @@ # fetches the contents of the specified url and returns the same to the requester # + import sys +import http.server + gMe = {} + +class ProxyHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + print(self.path) + + + def process_args(args: list[str]): global gMe gMe['INTERNAL.ProcessArgs.Malformed'] = [] @@ -33,5 +43,15 @@ def process_args(args: list[str]): iArg += 1 +def run(): + gMe['server'] = http.server.HTTPServer(('',gMe['--port']), ProxyHandler) + try: + gMe['server'].serve_forever() + except KeyboardInterrupt: + print("INFO:Run:Shuting down...") + gMe['server'].server_close() + sys.exit(0) + if __name__ == "__main__": process_args(sys.argv) + run() From c99788e29059102a15b6e8365a7cfc317a387e0a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 16 Oct 2025 18:18:45 +0530 Subject: [PATCH 068/446] SimpleChatTC:SimpleProxy: Cleanup for basic run --- .../local.tools/simpleproxy.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index c9d7264b7a..ce8c0c5718 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -11,7 +11,10 @@ import sys import http.server -gMe = {} +gMe = { + '--port': 3128, + 'server': None +} class ProxyHandler(http.server.BaseHTTPRequestHandler): @@ -35,7 +38,7 @@ def process_args(args: list[str]): match cArg: case '--port': iArg += 1 - gMe[cArg] = args[iArg] + gMe[cArg] = int(args[iArg]) iArg += 1 case _: gMe['INTERNAL.ProcessArgs.Unknown'].append(cArg) @@ -44,13 +47,20 @@ def process_args(args: list[str]): def run(): - gMe['server'] = http.server.HTTPServer(('',gMe['--port']), ProxyHandler) try: + gMe['server'] = http.server.HTTPServer(('',gMe['--port']), ProxyHandler) gMe['server'].serve_forever() except KeyboardInterrupt: print("INFO:Run:Shuting down...") - gMe['server'].server_close() + if (gMe['server']): + gMe['server'].server_close() sys.exit(0) + except Exception as exc: + print(f"ERRR:Run:Exception:{exc}") + if (gMe['server']): + gMe['server'].server_close() + sys.exit(1) + if __name__ == "__main__": process_args(sys.argv) From 73054a583267118d70202318d0dfa58cbfebd738 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 16 Oct 2025 18:39:02 +0530 Subject: [PATCH 069/446] SimpleChatTC:SimpleProxy: Extract and check path, route to handlers --- .../local.tools/simpleproxy.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index ce8c0c5718..105ba794db 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -9,6 +9,7 @@ import sys import http.server +import urllib.parse gMe = { @@ -19,9 +20,30 @@ gMe = { class ProxyHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): - print(self.path) + print(f"DBUG:ProxyHandler:{self.path}") + pr = urllib.parse.urlparse(self.path) + print(f"DBUG:ProxyHandler:{pr}") + match pr.path: + case '/urlraw': + handle_urlraw(self, pr) + case '/urltext': + handle_urltext(self, pr) + case _: + print(f"WARN:ProxyHandler:UnknownPath{pr.path}") + self.send_response(400) + self.send_header('Content-Type', 'plain/text') + self.end_headers() + self.wfile.write(f"WARN:UnknownPath:{pr.path}") +def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): + print(f"DBUG:HandleUrlRaw:{pr}") + queryParams = urllib.parse.parse_qs(pr.query) + + +def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): + print(f"DBUG:HandleUrlText:{pr}") + def process_args(args: list[str]): global gMe From 73ef9f7d468ac05dab92319d7a8142612c6c3486 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 16 Oct 2025 18:54:20 +0530 Subject: [PATCH 070/446] SimpleChatTC:SimpleProxy:implement handle_urlraw A basic go at it --- .../local.tools/simpleproxy.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 105ba794db..5b41b37f0c 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -19,6 +19,7 @@ gMe = { class ProxyHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): print(f"DBUG:ProxyHandler:{self.path}") pr = urllib.parse.urlparse(self.path) @@ -30,19 +31,36 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): handle_urltext(self, pr) case _: print(f"WARN:ProxyHandler:UnknownPath{pr.path}") - self.send_response(400) - self.send_header('Content-Type', 'plain/text') - self.end_headers() - self.wfile.write(f"WARN:UnknownPath:{pr.path}") + self.send_error(400, f"WARN:UnknownPath:{pr.path}") def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): print(f"DBUG:HandleUrlRaw:{pr}") queryParams = urllib.parse.parse_qs(pr.query) + url = queryParams['url'] + if (not url) or (len(url) == 0): + ph.send_error(400, "WARN:UrlRaw:MissingUrl") + return + try: + # Get requested url + with urllib.request.urlopen(url, timeout=10) as response: + contentData = response.read() + statusCode = response.status or 200 + contentType = response.getheader('Content-Type') or 'text/html' + # Send back to client + ph.send_response(statusCode) + ph.send_header('Content-Type', contentType) + # Add CORS for browser fetch, just inc ase + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + ph.wfile.write(contentData) + except Exception as exc: + ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): print(f"DBUG:HandleUrlText:{pr}") + ph.send_error(400, "WARN:UrlText:Not implemented") def process_args(args: list[str]): From 3bab4de0e8f2f413baac3c94001a37fd35156ad3 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 16 Oct 2025 18:58:06 +0530 Subject: [PATCH 071/446] SimpleChatTC:SimpleProxy:UrlRaw: Fixup basic oversight wrt 1st go --- tools/server/public_simplechat/local.tools/simpleproxy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 5b41b37f0c..d1b229f5cf 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -10,6 +10,7 @@ import sys import http.server import urllib.parse +import urllib.request gMe = { @@ -38,6 +39,8 @@ def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): print(f"DBUG:HandleUrlRaw:{pr}") queryParams = urllib.parse.parse_qs(pr.query) url = queryParams['url'] + print(f"DBUG:HandleUrlRaw:Url:{url}") + url = url[0] if (not url) or (len(url) == 0): ph.send_error(400, "WARN:UrlRaw:MissingUrl") return From c25b1968cdb60595310159596a850789356d19be Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 16 Oct 2025 19:02:55 +0530 Subject: [PATCH 072/446] SimpleChatTC:WebFetch: Update to use internal SimpleProxy.py --- tools/server/public_simplechat/tooljs.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 9591aeef4b..9fb306ccb6 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -104,7 +104,7 @@ let weburlfetch_meta = { */ function weburlfetch_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { - let newUrl = `http://127.0.0.1:3128/?path=${obj.url}` + let newUrl = `http://127.0.0.1:3128/urlraw?url=${obj.url}` fetch(newUrl).then(resp=>resp.text()).then(data => { gToolsWorker.onmessage(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) }).catch((err)=>{ From e600e62e86f768cac0c09e0a937ce9eee32ed11b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 16 Oct 2025 22:47:21 +0530 Subject: [PATCH 073/446] SimpleChatTC:SimpleProxy: Cleanup few messages --- tools/server/public_simplechat/local.tools/simpleproxy.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index d1b229f5cf..f1322913d7 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -53,7 +53,7 @@ def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): # Send back to client ph.send_response(statusCode) ph.send_header('Content-Type', contentType) - # Add CORS for browser fetch, just inc ase + # Add CORS for browser fetch, just in case ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() ph.wfile.write(contentData) @@ -91,7 +91,9 @@ def process_args(args: list[str]): def run(): try: - gMe['server'] = http.server.HTTPServer(('',gMe['--port']), ProxyHandler) + gMe['serverAddr'] = ('', gMe['--port']) + gMe['server'] = http.server.HTTPServer(gMe['serverAddr'], ProxyHandler) + print(f"INFO:Run:Starting on {gMe['serverAddr']}") gMe['server'].serve_forever() except KeyboardInterrupt: print("INFO:Run:Shuting down...") @@ -99,7 +101,7 @@ def run(): gMe['server'].server_close() sys.exit(0) except Exception as exc: - print(f"ERRR:Run:Exception:{exc}") + print(f"ERRR:Run:Exiting:Exception:{exc}") if (gMe['server']): gMe['server'].server_close() sys.exit(1) From 6537559360240f78fba9f4b138ac105d9bc3457f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 00:11:01 +0530 Subject: [PATCH 074/446] SimpleChatTC:SimpleProxy:Common UrlReq helper for UrlRaw & UrlText Declare the result of UrlReq as a DataClass, so that one doesnt goof up wrt updating and accessing members. Duplicate UrlRaw into UrlText, need to add Text extracting from html next for UrlText --- .../local.tools/simpleproxy.py | 55 +++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index f1322913d7..d1f4cb3ec5 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -11,6 +11,7 @@ import sys import http.server import urllib.parse import urllib.request +from dataclasses import dataclass gMe = { @@ -35,35 +36,69 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): self.send_error(400, f"WARN:UnknownPath:{pr.path}") -def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): - print(f"DBUG:HandleUrlRaw:{pr}") +@dataclass(frozen=True) +class UrlReqResp: + callOk: bool + httpStatus: int + httpStatusMsg: str = "" + contentType: str = "" + contentData: urllib.request._UrlopenRet = "" + + +def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): + print(f"DBUG:{tag}:{pr}") queryParams = urllib.parse.parse_qs(pr.query) url = queryParams['url'] - print(f"DBUG:HandleUrlRaw:Url:{url}") + print(f"DBUG:{tag}:Url:{url}") url = url[0] if (not url) or (len(url) == 0): - ph.send_error(400, "WARN:UrlRaw:MissingUrl") - return + return UrlReqResp(False, 400, f"WARN:{tag}:MissingUrl") try: # Get requested url with urllib.request.urlopen(url, timeout=10) as response: contentData = response.read() statusCode = response.status or 200 contentType = response.getheader('Content-Type') or 'text/html' + return UrlReqResp(True, statusCode, "", contentType, contentData) + except Exception as exc: + return UrlReqResp(False, 502, f"WARN:UrlFetchFailed:{exc}") + + +def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): + try: + # Get requested url + got = handle_urlreq(pr, "HandleUrlRaw") + if not got.callOk: + ph.send_error(got.httpStatus, got.httpStatusMsg) + return # Send back to client - ph.send_response(statusCode) - ph.send_header('Content-Type', contentType) + ph.send_response(got.httpStatus) + ph.send_header('Content-Type', got.contentType) # Add CORS for browser fetch, just in case ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() - ph.wfile.write(contentData) + ph.wfile.write(got.contentData) except Exception as exc: ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): - print(f"DBUG:HandleUrlText:{pr}") - ph.send_error(400, "WARN:UrlText:Not implemented") + try: + # Get requested url + got = handle_urlreq(pr, "HandleUrlText") + if not got.callOk: + ph.send_error(got.httpStatus, got.httpStatusMsg) + return + # Extract Text + # Send back to client + ph.send_response(got.httpStatus) + ph.send_header('Content-Type', got.contentType) + # Add CORS for browser fetch, just in case + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + ph.wfile.write(got.contentData) + except Exception as exc: + ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") def process_args(args: list[str]): From d5f4183f7c28b62a6ae52a49bb7fedfde67dcf00 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 00:42:04 +0530 Subject: [PATCH 075/446] SimpleChatTC:SimpleProxy: ElementTree, No _UrlopenRet As _UrlopenRet not exposed for use outside urllib, so decode and encode the data. Add skeleton to try get the html/xml tree top elements --- .../public_simplechat/local.tools/simpleproxy.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index d1f4cb3ec5..0014b0219b 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -12,6 +12,7 @@ import http.server import urllib.parse import urllib.request from dataclasses import dataclass +import xml.etree.ElementTree as xmlET gMe = { @@ -42,7 +43,7 @@ class UrlReqResp: httpStatus: int httpStatusMsg: str = "" contentType: str = "" - contentData: urllib.request._UrlopenRet = "" + contentData: str = "" def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): @@ -56,7 +57,7 @@ def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): try: # Get requested url with urllib.request.urlopen(url, timeout=10) as response: - contentData = response.read() + contentData = response.read().decode('utf-8') statusCode = response.status or 200 contentType = response.getheader('Content-Type') or 'text/html' return UrlReqResp(True, statusCode, "", contentType, contentData) @@ -77,7 +78,7 @@ def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): # Add CORS for browser fetch, just in case ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() - ph.wfile.write(got.contentData) + ph.wfile.write(got.contentData.encode('utf-8')) except Exception as exc: ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") @@ -90,13 +91,16 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.send_error(got.httpStatus, got.httpStatusMsg) return # Extract Text + html = xmlET.fromstring(got.contentData) + for el in html.iter(): + print(el) # Send back to client ph.send_response(got.httpStatus) ph.send_header('Content-Type', got.contentType) # Add CORS for browser fetch, just in case ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() - ph.wfile.write(got.contentData) + ph.wfile.write(got.contentData.encode('utf-8')) except Exception as exc: ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") From 45b05df21bb8e9dfe37f43cac07284a73b0c8c72 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 01:30:46 +0530 Subject: [PATCH 076/446] SimpleChatTC:SimpleProxy: Switch to html.parser As html can be malformed, xml ElementTree XMLParser cant handle the same properly, so switch to the HtmlParser helper class that is provided by python and try extend it. Currently a minimal skeleton to just start it out, which captures only the body contents. --- .../local.tools/simpleproxy.py | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 0014b0219b..4ac26b6b22 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -12,7 +12,7 @@ import http.server import urllib.parse import urllib.request from dataclasses import dataclass -import xml.etree.ElementTree as xmlET +import html.parser gMe = { @@ -83,6 +83,26 @@ def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") +class TextHtmlParser(html.parser.HTMLParser): + + def __init__(self): + super().__init__() + self.bBody = False + self.text = "" + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]): + if tag == 'body': + self.bBody = True + + def handle_endtag(self, tag: str): + if tag == 'body': + self.bBody = False + + def handle_data(self, data: str): + if self.bBody: + self.text += f"{data}\n" + + def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): try: # Get requested url @@ -91,16 +111,15 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.send_error(got.httpStatus, got.httpStatusMsg) return # Extract Text - html = xmlET.fromstring(got.contentData) - for el in html.iter(): - print(el) + textHtml = TextHtmlParser() + textHtml.feed(got.contentData) # Send back to client ph.send_response(got.httpStatus) ph.send_header('Content-Type', got.contentType) # Add CORS for browser fetch, just in case ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() - ph.wfile.write(got.contentData.encode('utf-8')) + ph.wfile.write(textHtml.text.encode('utf-8')) except Exception as exc: ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") From f493e1af59524c6eb1d2545928958021f7377fc5 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 01:39:35 +0530 Subject: [PATCH 077/446] SimpleChatTC:SimpleProxy:UrlText: Capture body except for scripts --- .../server/public_simplechat/local.tools/simpleproxy.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 4ac26b6b22..242e644648 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -88,18 +88,25 @@ class TextHtmlParser(html.parser.HTMLParser): def __init__(self): super().__init__() self.bBody = False + self.bCapture = False self.text = "" def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]): if tag == 'body': self.bBody = True + self.bCapture = True + if tag == 'script': + self.bCapture = False def handle_endtag(self, tag: str): if tag == 'body': self.bBody = False + if tag == 'script': + if self.bBody: + self.bCapture = True def handle_data(self, data: str): - if self.bBody: + if self.bCapture: self.text += f"{data}\n" From b46bbc542ad7344e3a3530babf32a2dc6f021a66 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 01:50:26 +0530 Subject: [PATCH 078/446] SimpleChatTC:SimpleProxy:UrlText: Avoid style blocks also --- tools/server/public_simplechat/local.tools/simpleproxy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 242e644648..3076c0d4ae 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -97,11 +97,13 @@ class TextHtmlParser(html.parser.HTMLParser): self.bCapture = True if tag == 'script': self.bCapture = False + if tag == 'style': + self.bCapture = False def handle_endtag(self, tag: str): if tag == 'body': self.bBody = False - if tag == 'script': + if tag == 'script' or tag == 'style': if self.bBody: self.bCapture = True From 82ab08ec1ac60c7aa8dfce779f6728fb28d93e21 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 02:08:38 +0530 Subject: [PATCH 079/446] SimpleChatTC:WebUrl FetchStrip through simple proxy --- tools/server/public_simplechat/tooljs.mjs | 67 +++++++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 9fb306ccb6..e9dc3c6f90 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -72,6 +72,16 @@ function calc_run(toolcallid, toolname, obj) { } +/** + * Send a message to Tools WebWorker's monitor in main thread directly + * @param {MessageEvent} mev + */ +function message_toolsworker(mev) { + // @ts-ignore + gToolsWorker.onmessage(mev) +} + + let weburlfetch_meta = { "type": "function", "function": { @@ -96,8 +106,8 @@ let weburlfetch_meta = { * Expects a simple minded proxy server to be running locally * * listening on port 3128 * * expecting http requests - * * with a query token named path which gives the actual url to fetch - * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * * with a query token named url which gives the actual url to fetch + * ALERT: Accesses a external web proxy/caching server be aware and careful * @param {string} toolcallid * @param {string} toolname * @param {any} obj @@ -106,9 +116,53 @@ function weburlfetch_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { let newUrl = `http://127.0.0.1:3128/urlraw?url=${obj.url}` fetch(newUrl).then(resp=>resp.text()).then(data => { - gToolsWorker.onmessage(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) }).catch((err)=>{ - gToolsWorker.onmessage(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) + }) + } +} + + +let weburlfetchstrip_meta = { + "type": "function", + "function": { + "name": "web_url_fetch_strip", + "description": "Fetch the requested web url through a proxy server and strip away head, script, styles blocks before sending remaining body in few seconds", + "parameters": { + "type": "object", + "properties": { + "url":{ + "type":"string", + "description":"the url of the page that will be fetched from the internet and inturn contents stripped to some extent" + } + }, + "required": ["url"] + } + } + } + + +/** + * Implementation of the web url logic. Dumb initial go. + * Expects a simple minded proxy server to be running locally + * * listening on port 3128 + * * expecting http requests + * * with a query token named url which gives the actual url to fetch + * * strips out head as well as any script and style blocks in body + * before returning remaining body contents. + * ALERT: Accesses a external web proxy/caching server be aware and careful + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function weburlfetchstrip_run(toolcallid, toolname, obj) { + if (gToolsWorker.onmessage != null) { + let newUrl = `http://127.0.0.1:3128/urltext?url=${obj.url}` + fetch(newUrl).then(resp=>resp.text()).then(data => { + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) + }).catch((err)=>{ + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) }) } } @@ -132,6 +186,11 @@ export let tc_switch = { "handler": weburlfetch_run, "meta": weburlfetch_meta, "result": "" + }, + "web_url_fetch_strip": { + "handler": weburlfetchstrip_run, + "meta": weburlfetchstrip_meta, + "result": "" } } From 266e825c686220dc4ea55eab055fbfe5e8ac8bcc Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 02:52:37 +0530 Subject: [PATCH 080/446] SimpleChatTC:SimpleProxy:UrlText: Try strip empty lines some what --- .../public_simplechat/local.tools/simpleproxy.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 3076c0d4ae..ad21cb3dc4 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -111,6 +111,16 @@ class TextHtmlParser(html.parser.HTMLParser): if self.bCapture: self.text += f"{data}\n" + def get_stripped_text(self): + oldLen = -99 + newLen = len(self.text) + aStripped = self.text; + while oldLen != newLen: + oldLen = newLen + aStripped = aStripped.replace("\n\n\n","\n") + newLen = len(aStripped) + return aStripped + def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): try: @@ -128,7 +138,7 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): # Add CORS for browser fetch, just in case ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() - ph.wfile.write(textHtml.text.encode('utf-8')) + ph.wfile.write(textHtml.get_stripped_text().encode('utf-8')) except Exception as exc: ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") From bf63b8f45ad6e475906dc5a14b4626ec7ecd3d3b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 03:02:53 +0530 Subject: [PATCH 081/446] SimpleChatTC:SimpleProxy:UrlText: Slightly better trimming First identify lines which have only whitespace and replace them with lines with only newline char in them. Next strip out adjacent lines, if they have only newlines --- .../local.tools/simpleproxy.py | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index ad21cb3dc4..ad85b1b809 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -90,6 +90,7 @@ class TextHtmlParser(html.parser.HTMLParser): self.bBody = False self.bCapture = False self.text = "" + self.textStripped = "" def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]): if tag == 'body': @@ -111,15 +112,33 @@ class TextHtmlParser(html.parser.HTMLParser): if self.bCapture: self.text += f"{data}\n" - def get_stripped_text(self): + def syncup(self): + self.textStripped = self.text + + def strip_adjacent_newlines(self): oldLen = -99 - newLen = len(self.text) - aStripped = self.text; + newLen = len(self.textStripped) + aStripped = self.textStripped; while oldLen != newLen: oldLen = newLen aStripped = aStripped.replace("\n\n\n","\n") newLen = len(aStripped) - return aStripped + self.textStripped = aStripped + + def strip_whitespace_lines(self): + aLines = self.textStripped.splitlines() + self.textStripped = "" + for line in aLines: + if (len(line.strip())==0): + self.textStripped += "\n" + continue + self.textStripped += f"{line}\n" + + def get_stripped_text(self): + self.syncup() + self.strip_whitespace_lines() + self.strip_adjacent_newlines() + return self.textStripped def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): From 9c7d6cc0e45982adc8726ecb84b4306487473785 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 03:56:35 +0530 Subject: [PATCH 082/446] SimpleChatTC:WebUrlText:Update name and desc to see if prefered --- tools/server/public_simplechat/tooljs.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index e9dc3c6f90..cb75dc019c 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -127,14 +127,14 @@ function weburlfetch_run(toolcallid, toolname, obj) { let weburlfetchstrip_meta = { "type": "function", "function": { - "name": "web_url_fetch_strip", - "description": "Fetch the requested web url through a proxy server and strip away head, script, styles blocks before sending remaining body in few seconds", + "name": "web_url_fetch_strip_htmltags_and_some_useless", + "description": "Fetch the requested web url through a proxy server and strip away html tags as well as head, script, styles blocks before returning the remaining body in few seconds", "parameters": { "type": "object", "properties": { "url":{ "type":"string", - "description":"the url of the page that will be fetched from the internet and inturn contents stripped to some extent" + "description":"the url of the page that will be fetched from the internet and inturn unwanted stuff stripped from its contents to some extent" } }, "required": ["url"] @@ -187,7 +187,7 @@ export let tc_switch = { "meta": weburlfetch_meta, "result": "" }, - "web_url_fetch_strip": { + "web_url_fetch_strip_htmltags_and_some_useless": { "handler": weburlfetchstrip_run, "meta": weburlfetchstrip_meta, "result": "" From 9ff2c596ee823e8cf9ae5c092422f49cc814593e Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 04:13:46 +0530 Subject: [PATCH 083/446] SimpleChatTC:SimpleProxy:Options just in case --- .../public_simplechat/local.tools/simpleproxy.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index ad85b1b809..4e69c1cf61 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -23,19 +23,29 @@ gMe = { class ProxyHandler(http.server.BaseHTTPRequestHandler): + # Handle GET requests def do_GET(self): - print(f"DBUG:ProxyHandler:{self.path}") + print(f"DBUG:ProxyHandler:GET:{self.path}") pr = urllib.parse.urlparse(self.path) - print(f"DBUG:ProxyHandler:{pr}") + print(f"DBUG:ProxyHandler:GET:{pr}") match pr.path: case '/urlraw': handle_urlraw(self, pr) case '/urltext': handle_urltext(self, pr) case _: - print(f"WARN:ProxyHandler:UnknownPath{pr.path}") + print(f"WARN:ProxyHandler:GET:UnknownPath{pr.path}") self.send_error(400, f"WARN:UnknownPath:{pr.path}") + # Handle OPTIONS for CORS preflights (just in case from browser) + def do_OPTIONS(self): + print(f"DBUG:ProxyHandler:OPTIONS:{self.path}") + self.send_response(200) + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') + self.send_header('Access-Control-Allow-Headers', '*') + self.end_headers() + @dataclass(frozen=True) class UrlReqResp: From 8b950fd348cbab9d847c291c59fc2cbf9b08dbd1 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 05:22:13 +0530 Subject: [PATCH 084/446] SimpleChatTC:WebFetch:UrlEnc url2fetch b4Passing toProxy asQuery Ensures that if the url being requested as any query strings in them then things dont get messed up, when the url to get inc its query is extracted from the proxy request's query string --- tools/server/public_simplechat/tooljs.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index cb75dc019c..5b7979f0ab 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -114,7 +114,7 @@ let weburlfetch_meta = { */ function weburlfetch_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { - let newUrl = `http://127.0.0.1:3128/urlraw?url=${obj.url}` + let newUrl = `http://127.0.0.1:3128/urlraw?url=${encodeURIComponent(obj.url)}` fetch(newUrl).then(resp=>resp.text()).then(data => { message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) }).catch((err)=>{ @@ -158,7 +158,7 @@ let weburlfetchstrip_meta = { */ function weburlfetchstrip_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { - let newUrl = `http://127.0.0.1:3128/urltext?url=${obj.url}` + let newUrl = `http://127.0.0.1:3128/urltext?url=${encodeURIComponent(obj.url)}` fetch(newUrl).then(resp=>resp.text()).then(data => { message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) }).catch((err)=>{ From cd226e8dae08911e287a04a825c405a739dcc137 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 05:36:43 +0530 Subject: [PATCH 085/446] SimpleChatTC: Update readme wrt web fetch and related simple proxy --- tools/server/public_simplechat/readme.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 0ae62a3a7b..d90261ed38 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -334,16 +334,21 @@ The following tools/functions are currently provided by default * simple_calculator - which can solve simple arithmatic expressions * run_javascript_function_code - which can be used to run some javascript code in the browser context. +* web_url_fetch - fetch requested url through a proxy server +* web_url_fetch_strip_htmltags_and_some_useless - fetch requested url through a proxy server + and also try strip the html respose of html tags and also head, script & style blocks. Currently the generated code / expression is run through a simple minded eval inside a web worker mechanism. Use of WebWorker helps avoid exposing browser global scope to the generated code directly. However any shared web worker scope isnt isolated. Either way always remember to cross check the tool requests and generated responses when using tool calling. -May add -* web_fetch along with a corresponding simple local web proxy/caching server logic that can bypass - the CORS restrictions applied if trying to directly fetch from the browser js runtime environment. - Inturn maybe with a white list of allowed sites to access or so. +web_url_fetch and family works along with a corresponding simple local web proxy/caching server logic +that can bypass the CORS restrictions applied if trying to directly fetch from the browser js runtime +environment. Depending on the path specified on the proxy server, if urltext, it additionally tries +to convert html content into equivalent text to some extent. May add support for white list of allowed +sites to access or so. +* tools/server/public_simplechat/local.tools/simpleproxy.py #### Extending with new tools @@ -394,8 +399,6 @@ get incorporated in result sent to ai engine on the server side. #### ToDo -WebFetch and Local web proxy/caching server - Is the promise land trap deep enough, need to think through and explore around this once later. Trap error responses. From 73a144c44d6a672a01eaec6d93736bfdb68f8b3c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 18:01:25 +0530 Subject: [PATCH 086/446] SimpleChatTC:SimpleProxy:HtmlParser more generic and flexible also now track header, footer and nav so that they arent captured --- .../local.tools/simpleproxy.py | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 4e69c1cf61..78cf6d6767 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -97,29 +97,34 @@ class TextHtmlParser(html.parser.HTMLParser): def __init__(self): super().__init__() - self.bBody = False + self.inside = { + 'body': False, + 'script': False, + 'style': False, + 'header': False, + 'footer': False, + 'nav': False + } + self.monitored = [ 'body', 'script', 'style', 'header', 'footer', 'nav' ] self.bCapture = False self.text = "" self.textStripped = "" + def do_capture(self): + if self.inside['body'] and not (self.inside['script'] or self.inside['style'] or self.inside['header'] or self.inside['footer'] or self.inside['nav']): + return True + return False + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]): - if tag == 'body': - self.bBody = True - self.bCapture = True - if tag == 'script': - self.bCapture = False - if tag == 'style': - self.bCapture = False + if tag in self.monitored: + self.inside[tag] = True def handle_endtag(self, tag: str): - if tag == 'body': - self.bBody = False - if tag == 'script' or tag == 'style': - if self.bBody: - self.bCapture = True + if tag in self.monitored: + self.inside[tag] = False def handle_data(self, data: str): - if self.bCapture: + if self.do_capture(): self.text += f"{data}\n" def syncup(self): From c2fb0cd2418760ae426d6d5be1388bc263518053 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 18:25:50 +0530 Subject: [PATCH 087/446] SimpleChatTC:WebFetch: Cleanup the names and descriptions a bit --- tools/server/public_simplechat/readme.md | 11 +++--- tools/server/public_simplechat/tooljs.mjs | 48 ++++++++++++----------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index d90261ed38..6c1d55132b 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -343,11 +343,12 @@ mechanism. Use of WebWorker helps avoid exposing browser global scope to the gen However any shared web worker scope isnt isolated. Either way always remember to cross check the tool requests and generated responses when using tool calling. -web_url_fetch and family works along with a corresponding simple local web proxy/caching server logic -that can bypass the CORS restrictions applied if trying to directly fetch from the browser js runtime -environment. Depending on the path specified on the proxy server, if urltext, it additionally tries -to convert html content into equivalent text to some extent. May add support for white list of allowed -sites to access or so. +fetch_web_url_raw/text and family works along with a corresponding simple local web proxy/caching +server logic, this helps bypass the CORS restrictions applied if trying to directly fetch from the +browser js runtime environment. Depending on the path specified wrt the proxy server, if urltext +(and not urlraw), it additionally tries to convert html content into equivalent text to some extent +in a simple minded manner by dropping head block as well as all scripts/styles/footers/headers/nav. +May add support for white list of allowed sites to access or so. The simple proxy can be found at * tools/server/public_simplechat/local.tools/simpleproxy.py diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 5b7979f0ab..da09d9013a 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -82,17 +82,17 @@ function message_toolsworker(mev) { } -let weburlfetch_meta = { +let fetchweburlraw_meta = { "type": "function", "function": { - "name": "web_url_fetch", - "description": "Fetch the requested web url through a proxy server in few seconds", + "name": "fetch_web_url_raw", + "description": "Fetch the requested web url through a proxy server and return the got content as is, in few seconds", "parameters": { "type": "object", "properties": { "url":{ "type":"string", - "description":"the url of the page / content to fetch from the internet" + "description":"url of the web page to fetch from the internet" } }, "required": ["url"] @@ -102,17 +102,18 @@ let weburlfetch_meta = { /** - * Implementation of the web url logic. Dumb initial go. + * Implementation of the fetch web url raw logic. Dumb initial go. * Expects a simple minded proxy server to be running locally * * listening on port 3128 * * expecting http requests - * * with a query token named url which gives the actual url to fetch - * ALERT: Accesses a external web proxy/caching server be aware and careful + * * with a query token named url wrt the path urlraw + * which gives the actual url to fetch + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function weburlfetch_run(toolcallid, toolname, obj) { +function fetchweburlraw_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { let newUrl = `http://127.0.0.1:3128/urlraw?url=${encodeURIComponent(obj.url)}` fetch(newUrl).then(resp=>resp.text()).then(data => { @@ -124,17 +125,17 @@ function weburlfetch_run(toolcallid, toolname, obj) { } -let weburlfetchstrip_meta = { +let fetchweburltext_meta = { "type": "function", "function": { - "name": "web_url_fetch_strip_htmltags_and_some_useless", - "description": "Fetch the requested web url through a proxy server and strip away html tags as well as head, script, styles blocks before returning the remaining body in few seconds", + "name": "fetch_web_url_text", + "description": "Fetch the requested web url through a proxy server and return its text content after stripping away the html tags as well as head, script, style, header, footer, nav blocks, in few seconds", "parameters": { "type": "object", "properties": { "url":{ "type":"string", - "description":"the url of the page that will be fetched from the internet and inturn unwanted stuff stripped from its contents to some extent" + "description":"url of the page that will be fetched from the internet and inturn unwanted stuff stripped from its contents to some extent" } }, "required": ["url"] @@ -144,19 +145,20 @@ let weburlfetchstrip_meta = { /** - * Implementation of the web url logic. Dumb initial go. + * Implementation of the fetch web url text logic. Dumb initial go. * Expects a simple minded proxy server to be running locally * * listening on port 3128 * * expecting http requests - * * with a query token named url which gives the actual url to fetch - * * strips out head as well as any script and style blocks in body + * * with a query token named url wrt urltext path, + * which gives the actual url to fetch + * * strips out head as well as any script, style, header, footer, nav and so blocks in body * before returning remaining body contents. - * ALERT: Accesses a external web proxy/caching server be aware and careful + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function weburlfetchstrip_run(toolcallid, toolname, obj) { +function fetchweburltext_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { let newUrl = `http://127.0.0.1:3128/urltext?url=${encodeURIComponent(obj.url)}` fetch(newUrl).then(resp=>resp.text()).then(data => { @@ -182,14 +184,14 @@ export let tc_switch = { "meta": calc_meta, "result": "" }, - "web_url_fetch": { - "handler": weburlfetch_run, - "meta": weburlfetch_meta, + "fetch_web_url_raw": { + "handler": fetchweburlraw_run, + "meta": fetchweburlraw_meta, "result": "" }, - "web_url_fetch_strip_htmltags_and_some_useless": { - "handler": weburlfetchstrip_run, - "meta": weburlfetchstrip_meta, + "fetch_web_url_text": { + "handler": fetchweburltext_run, + "meta": fetchweburltext_meta, "result": "" } } From d04c8cd38d542f42a6f2e69270f81e43d8b25c6b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 22:16:30 +0530 Subject: [PATCH 088/446] SimpleChatTC:SimpleProxy: Ensure CORS related headers sent always Add a new send headers common helper and use the same wrt the overridden send_error as well as do_OPTIONS This ensures that if there is any error during proxy opertions, the send_error propogates to the fetch from any browser properly without browser intercepting it with a CORS error --- .../local.tools/simpleproxy.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 78cf6d6767..ce638dfa15 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -23,6 +23,20 @@ gMe = { class ProxyHandler(http.server.BaseHTTPRequestHandler): + # Common headers to include in responses from this server + def send_headers_common(self): + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') + self.send_header('Access-Control-Allow-Headers', '*') + self.end_headers() + + # overrides the SendError helper + # so that the common headers mentioned above can get added to them + # else CORS failure will be triggered by the browser on fetch from browser. + def send_error(self, code: int, message: str | None = None, explain: str | None = None) -> None: + self.send_response(code, message) + self.send_headers_common() + # Handle GET requests def do_GET(self): print(f"DBUG:ProxyHandler:GET:{self.path}") @@ -41,10 +55,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): def do_OPTIONS(self): print(f"DBUG:ProxyHandler:OPTIONS:{self.path}") self.send_response(200) - self.send_header('Access-Control-Allow-Origin', '*') - self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') - self.send_header('Access-Control-Allow-Headers', '*') - self.end_headers() + self.send_headers_common() @dataclass(frozen=True) From 42f91df26110c8ce1446fc4594faf1f9b5c63644 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 17 Oct 2025 22:46:08 +0530 Subject: [PATCH 089/446] SimpleChatTC:WebFetch:Trap Non Ok status and raise error So that the same error path is used for logical error wrt http req also, without needing a different path for it. Dont forget to return the resp text/json/..., so that the contents are passed along the promise then chain --- tools/server/public_simplechat/tooljs.mjs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index da09d9013a..a60f283e7f 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -116,7 +116,12 @@ let fetchweburlraw_meta = { function fetchweburlraw_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { let newUrl = `http://127.0.0.1:3128/urlraw?url=${encodeURIComponent(obj.url)}` - fetch(newUrl).then(resp=>resp.text()).then(data => { + fetch(newUrl).then(resp => { + if (!resp.ok) { + throw new Error(`${resp.status}:${resp.statusText}`); + } + return resp.text() + }).then(data => { message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) }).catch((err)=>{ message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) @@ -161,7 +166,12 @@ let fetchweburltext_meta = { function fetchweburltext_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { let newUrl = `http://127.0.0.1:3128/urltext?url=${encodeURIComponent(obj.url)}` - fetch(newUrl).then(resp=>resp.text()).then(data => { + fetch(newUrl).then(resp => { + if (!resp.ok) { + throw new Error(`${resp.status}:${resp.statusText}`); + } + return resp.text() + }).then(data => { message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) }).catch((err)=>{ message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) From 9b55775e8aca2d2f1b77d939e462e7f1a93efb60 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 18 Oct 2025 01:27:53 +0530 Subject: [PATCH 090/446] SimpleChatTC:WebFetch: Update readme to reflect the new names --- tools/server/public_simplechat/readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 6c1d55132b..79f1097606 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -334,9 +334,9 @@ The following tools/functions are currently provided by default * simple_calculator - which can solve simple arithmatic expressions * run_javascript_function_code - which can be used to run some javascript code in the browser context. -* web_url_fetch - fetch requested url through a proxy server -* web_url_fetch_strip_htmltags_and_some_useless - fetch requested url through a proxy server - and also try strip the html respose of html tags and also head, script & style blocks. +* fetch_web_url_raw - fetch requested url through a proxy server +* fetch_web_url_text - fetch requested url through a proxy server + and also try strip the html respose of html tags and also head, script, style, header,footer,... blocks. Currently the generated code / expression is run through a simple minded eval inside a web worker mechanism. Use of WebWorker helps avoid exposing browser global scope to the generated code directly. From 04644761e6147f4e6240964e9dec92ad15538a7c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 18 Oct 2025 18:40:09 +0530 Subject: [PATCH 091/446] SimpleChatTC:Tools: Pick proxy server address from document[gMe] --- tools/server/public_simplechat/simplechat.js | 1 + tools/server/public_simplechat/tooljs.mjs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index c7259fb950..893c91d68f 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1035,6 +1035,7 @@ class Me { //"frequency_penalty": 1.2, //"presence_penalty": 1.2, }; + this.proxyUrl = "http://127.0.0.1:3128" } /** diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index a60f283e7f..ffaab5de2e 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -115,7 +115,8 @@ let fetchweburlraw_meta = { */ function fetchweburlraw_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { - let newUrl = `http://127.0.0.1:3128/urlraw?url=${encodeURIComponent(obj.url)}` + // @ts-ignore + let newUrl = `${document['gMe'].proxyUrl}/urlraw?url=${encodeURIComponent(obj.url)}` fetch(newUrl).then(resp => { if (!resp.ok) { throw new Error(`${resp.status}:${resp.statusText}`); @@ -165,7 +166,8 @@ let fetchweburltext_meta = { */ function fetchweburltext_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { - let newUrl = `http://127.0.0.1:3128/urltext?url=${encodeURIComponent(obj.url)}` + // @ts-ignore + let newUrl = `${document['gMe'].proxyUrl}/urltext?url=${encodeURIComponent(obj.url)}` fetch(newUrl).then(resp => { if (!resp.ok) { throw new Error(`${resp.status}:${resp.statusText}`); From 6e5b5323135334d48ed544d4f4ac4114f267872c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 00:41:14 +0530 Subject: [PATCH 092/446] SimpleChatTC:UI: el_get/el_set to avoid warnings --- tools/server/public_simplechat/ui.mjs | 37 +++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index b2d5b9aeab..eb0ce88875 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -4,6 +4,27 @@ // +/** + * Insert key-value pairs into passed element object. + * @param {HTMLElement} el + * @param {string} key + * @param {any} value + */ +function el_set(el, key, value) { + // @ts-ignore + el[key] = value +} + +/** + * Retrieve the value corresponding to given key from passed element object. + * @param {HTMLElement} el + * @param {string} key + */ +function el_get(el, key) { + // @ts-ignore + return el[key] +} + /** * Set the class of the children, based on whether it is the idSelected or not. * @param {HTMLDivElement} elBase @@ -72,16 +93,16 @@ export function el_create_append_p(text, elParent=undefined, id=undefined) { */ export function el_create_boolbutton(id, texts, defaultValue, cb) { let el = document.createElement("button"); - el["xbool"] = defaultValue; - el["xtexts"] = structuredClone(texts); - el.innerText = el["xtexts"][String(defaultValue)]; + el_set(el, "xbool", defaultValue) + el_set(el, "xtexts", structuredClone(texts)) + el.innerText = el_get(el, "xtexts")[String(defaultValue)]; if (id) { el.id = id; } el.addEventListener('click', (ev)=>{ - el["xbool"] = !el["xbool"]; - el.innerText = el["xtexts"][String(el["xbool"])]; - cb(el["xbool"]); + el_set(el, "xbool", !el_get(el, "xbool")); + el.innerText = el_get(el, "xtexts")[String(el_get(el, "xbool"))]; + cb(el_get(el, "xbool")); }) return el; } @@ -121,8 +142,8 @@ export function el_creatediv_boolbutton(id, label, texts, defaultValue, cb, clas */ export function el_create_select(id, options, defaultOption, cb) { let el = document.createElement("select"); - el["xselected"] = defaultOption; - el["xoptions"] = structuredClone(options); + el_set(el, "xselected", defaultOption); + el_set(el, "xoptions", structuredClone(options)); for(let cur of Object.keys(options)) { let op = document.createElement("option"); op.value = cur; From b771e42dc13f6ef355c8f7b2cde160e2330fd9c6 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 13:58:27 +0530 Subject: [PATCH 093/446] SimpleChatTC:UI:Common helper to edit obj members of few types Make the previously relatively generic flow wrt apiRequestOptions settings into a fully generic reusable by others flow. Rather had stopped short of it, when previously moved onto other things at that time. --- tools/server/public_simplechat/simplechat.js | 2 +- tools/server/public_simplechat/ui.mjs | 39 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 893c91d68f..073d95f89b 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1174,7 +1174,7 @@ class Me { }); elDiv.appendChild(bb.div); - this.show_settings_apirequestoptions(elDiv); + ui.ui_show_obj_props_edit(elDiv, this.apiRequestOptions, Object.keys(this.apiRequestOptions), "ApiRequestOptions") let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ // @ts-ignore diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index eb0ce88875..dcf1fba66d 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -230,3 +230,42 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= div.appendChild(el); return { div: div, el: el }; } + + +/** + * Auto create ui input elements for fields in apiRequestOptions + * Currently supports text and number field types. + * @param {HTMLDivElement} elDiv + * @param {any} oObj + * @param {Array} lProps + * @param {string} sLegend + */ +export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend) { + let typeDict = { + "string": "text", + "number": "number", + }; + let fs = document.createElement("fieldset"); + let legend = document.createElement("legend"); + legend.innerText = sLegend; + fs.appendChild(legend); + elDiv.appendChild(fs); + for(const k in lProps) { + let val = oObj[k]; + let type = typeof(val); + if (((type == "string") || (type == "number"))) { + let inp = el_creatediv_input(`Set${k}`, k, typeDict[type], oObj[k], (val)=>{ + if (type == "number") { + val = Number(val); + } + oObj[k] = val; + }); + fs.appendChild(inp.div); + } else if (type == "boolean") { + let bbtn = el_creatediv_boolbutton(`Set{k}`, k, {true: "true", false: "false"}, val, (userVal)=>{ + oObj[k] = userVal; + }); + fs.appendChild(bbtn.div); + } + } +} From 756b128539ce37dc4120af40de3b75231aefb557 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 14:55:13 +0530 Subject: [PATCH 094/446] SimpleChatTC:UI:ObjPropEdits handle objects, use for gMe --- tools/server/public_simplechat/simplechat.js | 2 +- tools/server/public_simplechat/ui.mjs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 073d95f89b..4ce17316ef 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1174,7 +1174,7 @@ class Me { }); elDiv.appendChild(bb.div); - ui.ui_show_obj_props_edit(elDiv, this.apiRequestOptions, Object.keys(this.apiRequestOptions), "ApiRequestOptions") + ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "bTools", "apiRequestOptions", "iRecentUserMsgCnt"], "Settings") let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ // @ts-ignore diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index dcf1fba66d..6d9cc9a1ad 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -235,7 +235,7 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= /** * Auto create ui input elements for fields in apiRequestOptions * Currently supports text and number field types. - * @param {HTMLDivElement} elDiv + * @param {HTMLDivElement|HTMLFieldSetElement} elDiv * @param {any} oObj * @param {Array} lProps * @param {string} sLegend @@ -250,7 +250,7 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend) { legend.innerText = sLegend; fs.appendChild(legend); elDiv.appendChild(fs); - for(const k in lProps) { + for(const k of lProps) { let val = oObj[k]; let type = typeof(val); if (((type == "string") || (type == "number"))) { @@ -266,6 +266,8 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend) { oObj[k] = userVal; }); fs.appendChild(bbtn.div); + } else if (type == "object") { + ui_show_obj_props_edit(fs, val, Object.keys(val), k) } } } From 3718a39c0605a353572ecf5127c300164bcd375c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 15:05:55 +0530 Subject: [PATCH 095/446] SimpleChatTC:Use generic obj props edit for settings in general Bring more user controllable properties into this new settings ui --- tools/server/public_simplechat/simplechat.js | 38 +------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 4ce17316ef..23bb561287 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1148,33 +1148,7 @@ class Me { */ show_settings(elDiv) { - let inp = ui.el_creatediv_input("SetBaseURL", "BaseURL", "text", this.baseURL, (val)=>{ - this.baseURL = val; - }); - elDiv.appendChild(inp.div); - - inp = ui.el_creatediv_input("SetAuthorization", "Authorization", "text", this.headers["Authorization"], (val)=>{ - this.headers["Authorization"] = val; - }); - inp.el.placeholder = "Bearer OPENAI_API_KEY"; - elDiv.appendChild(inp.div); - - let bb = ui.el_creatediv_boolbutton("SetStream", "Stream", {true: "[+] yes stream", false: "[-] do oneshot"}, this.bStream, (val)=>{ - this.bStream = val; - }); - elDiv.appendChild(bb.div); - - bb = ui.el_creatediv_boolbutton("SetTools", "Tools", {true: "[+] yes tools", false: "[-] no tools"}, this.bTools, (val)=>{ - this.bTools = val; - }); - elDiv.appendChild(bb.div); - - bb = ui.el_creatediv_boolbutton("SetTrimGarbage", "TrimGarbage", {true: "[+] yes trim", false: "[-] dont trim"}, this.bTrimGarbage, (val)=>{ - this.bTrimGarbage = val; - }); - elDiv.appendChild(bb.div); - - ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "bTools", "apiRequestOptions", "iRecentUserMsgCnt"], "Settings") + ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "bTools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings") let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ // @ts-ignore @@ -1187,16 +1161,6 @@ class Me { }); elDiv.appendChild(sel.div); - bb = ui.el_creatediv_boolbutton("SetCompletionFreshChatAlways", "CompletionFreshChatAlways", {true: "[+] yes fresh", false: "[-] no, with history"}, this.bCompletionFreshChatAlways, (val)=>{ - this.bCompletionFreshChatAlways = val; - }); - elDiv.appendChild(bb.div); - - bb = ui.el_creatediv_boolbutton("SetCompletionInsertStandardRolePrefix", "CompletionInsertStandardRolePrefix", {true: "[+] yes insert", false: "[-] dont insert"}, this.bCompletionInsertStandardRolePrefix, (val)=>{ - this.bCompletionInsertStandardRolePrefix = val; - }); - elDiv.appendChild(bb.div); - } } From 6253c717b3f986ca0f0b51e4ca3fa9a4752b07f8 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 15:35:34 +0530 Subject: [PATCH 096/446] SimpleChatTC:Trappable UiShowObjPropsEdit for custom handling Use it to handle apiEP and iRecentUserMsgCnt in more user friendly way, where they get a selection to choose from. --- tools/server/public_simplechat/simplechat.js | 63 +++++--------------- tools/server/public_simplechat/ui.mjs | 21 ++++++- 2 files changed, 33 insertions(+), 51 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 23bb561287..637cff74bf 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1107,60 +1107,27 @@ class Me { } - /** - * Auto create ui input elements for fields in apiRequestOptions - * Currently supports text and number field types. - * @param {HTMLDivElement} elDiv - */ - show_settings_apirequestoptions(elDiv) { - let typeDict = { - "string": "text", - "number": "number", - }; - let fs = document.createElement("fieldset"); - let legend = document.createElement("legend"); - legend.innerText = "ApiRequestOptions"; - fs.appendChild(legend); - elDiv.appendChild(fs); - for(const k in this.apiRequestOptions) { - let val = this.apiRequestOptions[k]; - let type = typeof(val); - if (((type == "string") || (type == "number"))) { - let inp = ui.el_creatediv_input(`Set${k}`, k, typeDict[type], this.apiRequestOptions[k], (val)=>{ - if (type == "number") { - val = Number(val); - } - this.apiRequestOptions[k] = val; - }); - fs.appendChild(inp.div); - } else if (type == "boolean") { - let bbtn = ui.el_creatediv_boolbutton(`Set{k}`, k, {true: "true", false: "false"}, val, (userVal)=>{ - this.apiRequestOptions[k] = userVal; - }); - fs.appendChild(bbtn.div); - } - } - } - /** * Show settings ui for configurable parameters, in the passed Div element. * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "bTools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings") - - let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ - // @ts-ignore - this.apiEP = ApiEP.Type[val]; - }); - elDiv.appendChild(sel.div); - - sel = ui.el_creatediv_select("SetChatHistoryInCtxt", "ChatHistoryInCtxt", this.sRecentUserMsgCnt, this.iRecentUserMsgCnt, (val)=>{ - this.iRecentUserMsgCnt = this.sRecentUserMsgCnt[val]; - }); - elDiv.appendChild(sel.div); - + ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "bTools", "apiRequestOptions", "TRAPME-apiEP", "TRAPME-iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", "TRAPME-", (tag, elParent)=>{ + if (tag == "TRAPME-apiEP") { + let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ + // @ts-ignore + this.apiEP = ApiEP.Type[val]; + }); + elParent.appendChild(sel.div); + } + if (tag == "TRAPME-iRecentUserMsgCnt") { + let sel = ui.el_creatediv_select("SetChatHistoryInCtxt", "ChatHistoryInCtxt", this.sRecentUserMsgCnt, this.iRecentUserMsgCnt, (val)=>{ + this.iRecentUserMsgCnt = this.sRecentUserMsgCnt[val]; + }); + elParent.appendChild(sel.div); + } + }) } } diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 6d9cc9a1ad..352d4895e0 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -233,14 +233,21 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= /** - * Auto create ui input elements for fields in apiRequestOptions - * Currently supports text and number field types. + * Auto create ui input elements for specified fields/properties in given object + * Currently supports text, number, boolean field types. + * Also supports recursing if a object type field is found. + * For some reason if caller wants to handle certain properties on their own + * * prefix the prop name in lProps with sTrapTag + * * fTrapper will be called with the parent ui element + * into which the new ui elements created for editting the prop, if any, should be attached * @param {HTMLDivElement|HTMLFieldSetElement} elDiv * @param {any} oObj * @param {Array} lProps * @param {string} sLegend + * @param {string | undefined} sTrapTag + * @param {((tagPlusProp: string, elParent: HTMLFieldSetElement)=>void) | undefined} fTrapper */ -export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend) { +export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, sTrapTag=undefined, fTrapper=undefined) { let typeDict = { "string": "text", "number": "number", @@ -251,6 +258,14 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend) { fs.appendChild(legend); elDiv.appendChild(fs); for(const k of lProps) { + if (sTrapTag) { + if (k.startsWith(sTrapTag)) { + if (fTrapper) { + fTrapper(k, fs) + } + continue + } + } let val = oObj[k]; let type = typeof(val); if (((type == "string") || (type == "number"))) { From f874c6998330f5392608d2621fa1dfd7adc7f61e Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 15:48:58 +0530 Subject: [PATCH 097/446] SimpleChatTC:UiShowObjPropsEdit allow refining --- tools/server/public_simplechat/ui.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 352d4895e0..295b14b9b0 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -244,10 +244,11 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= * @param {any} oObj * @param {Array} lProps * @param {string} sLegend + * @param {((prop:string, elUI: HTMLElement)=>void)| undefined} fRefiner * @param {string | undefined} sTrapTag * @param {((tagPlusProp: string, elParent: HTMLFieldSetElement)=>void) | undefined} fTrapper */ -export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, sTrapTag=undefined, fTrapper=undefined) { +export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, fRefiner=undefined, sTrapTag=undefined, fTrapper=undefined) { let typeDict = { "string": "text", "number": "number", @@ -275,11 +276,17 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, sTrapTag=un } oObj[k] = val; }); + if (fRefiner) { + fRefiner(k, inp.el) + } fs.appendChild(inp.div); } else if (type == "boolean") { let bbtn = el_creatediv_boolbutton(`Set{k}`, k, {true: "true", false: "false"}, val, (userVal)=>{ oObj[k] = userVal; }); + if (fRefiner) { + fRefiner(k, bbtn.el) + } fs.appendChild(bbtn.div); } else if (type == "object") { ui_show_obj_props_edit(fs, val, Object.keys(val), k) From 3e0cf2a2df27429ece1c645eefacab0efbec38c6 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 16:08:19 +0530 Subject: [PATCH 098/446] SimpleChatTC:ObjPropsEdit: Obj within Obj aware fRefiner Use same to set a placeholder for Authorization entry in headers --- tools/server/public_simplechat/simplechat.js | 8 ++++++-- tools/server/public_simplechat/ui.mjs | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 637cff74bf..d1692c3844 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1112,8 +1112,12 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - - ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "bTools", "apiRequestOptions", "TRAPME-apiEP", "TRAPME-iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", "TRAPME-", (tag, elParent)=>{ + ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "bTools", "apiRequestOptions", "TRAPME-apiEP", "TRAPME-iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ + if (prop == "headers:Authorization") { + // @ts-ignore + elProp.placeholder = "Bearer OPENAI_API_KEY"; + } + }, "TRAPME-", (tag, elParent)=>{ if (tag == "TRAPME-apiEP") { let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ // @ts-ignore diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 295b14b9b0..b16a19ba77 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -236,6 +236,11 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= * Auto create ui input elements for specified fields/properties in given object * Currently supports text, number, boolean field types. * Also supports recursing if a object type field is found. + * + * If for any reason the caller wants to refine the created ui element for a specific prop, + * they can define a fRefiner callback, which will be called back with prop name and ui element. + * The fRefiner callback even helps work with Obj with-in Obj scenarios. + * * For some reason if caller wants to handle certain properties on their own * * prefix the prop name in lProps with sTrapTag * * fTrapper will be called with the parent ui element @@ -244,7 +249,7 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= * @param {any} oObj * @param {Array} lProps * @param {string} sLegend - * @param {((prop:string, elUI: HTMLElement)=>void)| undefined} fRefiner + * @param {((prop:string, elProp: HTMLElement)=>void)| undefined} fRefiner * @param {string | undefined} sTrapTag * @param {((tagPlusProp: string, elParent: HTMLFieldSetElement)=>void) | undefined} fTrapper */ @@ -289,7 +294,12 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, fRefiner=un } fs.appendChild(bbtn.div); } else if (type == "object") { - ui_show_obj_props_edit(fs, val, Object.keys(val), k) + ui_show_obj_props_edit(fs, val, Object.keys(val), k, (prop, elProp)=>{ + if (fRefiner) { + let theProp = `${k}:${prop}` + fRefiner(theProp, elProp) + } + }) } } } From 8ca77e455a0ae45a3bf0c2ef53ba5d71b68d4d3c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 16:59:32 +0530 Subject: [PATCH 099/446] SimpleChatTC:NonStreaming: Update oneshot mode wrt tool calls Take care of the possibility of content not being there as well as take care of retrieving the tool calls for further processing. With this tool calls should work in non streaming mode also --- tools/server/public_simplechat/simplechat.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index d1692c3844..eaea16223f 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -199,7 +199,16 @@ class ChatMessageEx { */ update_oneshot(nwo, apiEP) { if (apiEP == ApiEP.Type.Chat) { - this.ns.content = nwo["choices"][0]["message"]["content"]; + let curContent = nwo["choices"][0]["message"]["content"]; + if (curContent != undefined) { + if (curContent != null) { + this.ns.content = curContent; + } + } + let curTCs = nwo["choices"][0]["message"]["tool_calls"]; + if (curTCs != undefined) { + this.ns.tool_calls = curTCs; + } } else { try { this.ns.content = nwo["choices"][0]["text"]; From fa0a6919cbb0055f123561eb10d6344a3cd5f7b7 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 17:28:46 +0530 Subject: [PATCH 100/446] SimpleChatTC: Update/Cleanup readme --- tools/server/public_simplechat/readme.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 79f1097606..a856b21b82 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -94,7 +94,7 @@ Once inside * oneshot or streamed mode. * use built in tool calling or not -* In completion mode +* In completion mode >> note: most recent work has been in chat mode << * one normally doesnt use a system prompt in completion mode. * logic by default doesnt insert any role specific "ROLE: " prefix wrt each role's message. If the model requires any prefix wrt user role messages, then the end user has to @@ -263,9 +263,9 @@ matter clearing site data, dont directly override site caching in all cases. Wor have to change port. Or in dev tools of browser, you may be able to disable caching fully. -Currently the server to communicate with is maintained globally and not as part of a specific -chat session. So if one changes the server ip/url in setting, then all chat sessions will auto -switch to this new server, when you try using those sessions. +Currently the settings are maintained globally and not as part of a specific chat session, including +the server to communicate with. So if one changes the server ip/url in setting, then all chat sessions +will auto switch to this new server, when you try using those sessions. By switching between chat.add_system_begin/anytime, one can control whether one can change @@ -298,7 +298,9 @@ wrt the set of fields sent to server along with the user query, to check how the wrt repeatations in general in the generated text response. A end-user can change these behaviour by editing gMe from browser's devel-tool/console or by -using the provided settings ui (for settings exposed through the ui). +using the provided settings ui (for settings exposed through the ui). The logic uses a generic +helper which autocreates property edit ui elements for the specified set of properties. If the +new property is a number or text or boolean, the autocreate logic will handle it. ### OpenAi / Equivalent API WebService @@ -404,6 +406,8 @@ Is the promise land trap deep enough, need to think through and explore around t Trap error responses. +Handle reasoning/thinking responses from ai models. + ### Debuging the handshake From 80dbbb89a559e50d9886840d249a06279541c5b1 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 18:59:18 +0530 Subject: [PATCH 101/446] SimpleChatTC:WebFetch: Enable only if something at proxyUrl NOTE: not a robust check, just tries to establish a http connection for now and doesnt really check if it is the specific proxy srvr of interest or not. --- tools/server/public_simplechat/readme.md | 7 +++- tools/server/public_simplechat/tooljs.mjs | 49 ++++++++++++++++++----- tools/server/public_simplechat/tools.mjs | 9 +++-- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index a856b21b82..3a1b5e104c 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -350,8 +350,11 @@ server logic, this helps bypass the CORS restrictions applied if trying to direc browser js runtime environment. Depending on the path specified wrt the proxy server, if urltext (and not urlraw), it additionally tries to convert html content into equivalent text to some extent in a simple minded manner by dropping head block as well as all scripts/styles/footers/headers/nav. -May add support for white list of allowed sites to access or so. The simple proxy can be found at -* tools/server/public_simplechat/local.tools/simpleproxy.py +May add support for white list of allowed sites to access or so. +* the logic does a simple dumb check to see if there is something running at specified proxyUrl + before enabling fetch web related tool calls. +* The bundled simple proxy can be found at + * tools/server/public_simplechat/local.tools/simpleproxy.py #### Extending with new tools diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index ffaab5de2e..f013cac70a 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -131,6 +131,23 @@ function fetchweburlraw_run(toolcallid, toolname, obj) { } +/** + * Setup fetch_web_url_raw for tool calling + * NOTE: Currently it just checks there is something at given proxyUrl + * @param {Object>} tcs + */ +async function fetchweburlraw_setup(tcs) { + // @ts-ignore + let got = await fetch(document["gMe"].proxyUrl).then(resp=>{ + tcs["fetch_web_url_raw"] = { + "handler": fetchweburlraw_run, + "meta": fetchweburlraw_meta, + "result": "" + }; + }).catch(err=>console.log(`WARN:ToolJS:FetchWebUrlRaw:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) +} + + let fetchweburltext_meta = { "type": "function", "function": { @@ -182,6 +199,23 @@ function fetchweburltext_run(toolcallid, toolname, obj) { } +/** + * Setup fetch_web_url_text for tool calling + * NOTE: Currently it just checks there is something at given proxyUrl + * @param {Object>} tcs + */ +async function fetchweburltext_setup(tcs) { + // @ts-ignore + let got = await fetch(document["gMe"].proxyUrl).then(resp=>{ + tcs["fetch_web_url_text"] = { + "handler": fetchweburltext_run, + "meta": fetchweburltext_meta, + "result": "" + }; + }).catch(err=>console.log(`WARN:ToolJS:FetchWebUrlText:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) +} + + /** * @type {Object>} */ @@ -196,23 +230,16 @@ export let tc_switch = { "meta": calc_meta, "result": "" }, - "fetch_web_url_raw": { - "handler": fetchweburlraw_run, - "meta": fetchweburlraw_meta, - "result": "" - }, - "fetch_web_url_text": { - "handler": fetchweburltext_run, - "meta": fetchweburltext_meta, - "result": "" - } } /** * Used to get hold of the web worker to use for running tool/function call related code + * Also to setup tool calls, which need to cross check things at runtime * @param {Worker} toolsWorker */ -export function init(toolsWorker) { +export async function init(toolsWorker) { gToolsWorker = toolsWorker + await fetchweburlraw_setup(tc_switch) + await fetchweburltext_setup(tc_switch) } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 8c89e96525..14249b517a 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -15,10 +15,11 @@ let gToolsWorker = new Worker('./toolsworker.mjs', { type: 'module' }); export let tc_switch = {} export function init() { - tjs.init(gToolsWorker) - for (const key in tjs.tc_switch) { - tc_switch[key] = tjs.tc_switch[key] - } + tjs.init(gToolsWorker).then(()=>{ + for (const key in tjs.tc_switch) { + tc_switch[key] = tjs.tc_switch[key] + } + }) } export function meta() { From a6aa563a1802384bff2e2062079f57f11da914a7 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 19:45:12 +0530 Subject: [PATCH 102/446] SimpleChatTC:WebFetch: Check for the specific proxy paths --- tools/server/public_simplechat/tooljs.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index f013cac70a..b195288aa6 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -138,7 +138,7 @@ function fetchweburlraw_run(toolcallid, toolname, obj) { */ async function fetchweburlraw_setup(tcs) { // @ts-ignore - let got = await fetch(document["gMe"].proxyUrl).then(resp=>{ + let got = await fetch(`${document["gMe"].proxyUrl}/urlraw?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ tcs["fetch_web_url_raw"] = { "handler": fetchweburlraw_run, "meta": fetchweburlraw_meta, @@ -206,7 +206,7 @@ function fetchweburltext_run(toolcallid, toolname, obj) { */ async function fetchweburltext_setup(tcs) { // @ts-ignore - let got = await fetch(document["gMe"].proxyUrl).then(resp=>{ + let got = await fetch(`${document["gMe"].proxyUrl}/urltext?url=jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ tcs["fetch_web_url_text"] = { "handler": fetchweburltext_run, "meta": fetchweburltext_meta, From 98d43fac7f15159d30890ed04f35560bef15b3fa Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 19:55:12 +0530 Subject: [PATCH 103/446] SimpleChatTC:WebFetch: Try confirm simpleproxy before enabling --- .../local.tools/simpleproxy.py | 8 ++++++++ tools/server/public_simplechat/readme.md | 2 +- tools/server/public_simplechat/tooljs.mjs | 20 +++++++++++++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index ce638dfa15..8185382297 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -47,6 +47,8 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): handle_urlraw(self, pr) case '/urltext': handle_urltext(self, pr) + case '/aum': + handle_aum(self, pr) case _: print(f"WARN:ProxyHandler:GET:UnknownPath{pr.path}") self.send_error(400, f"WARN:UnknownPath:{pr.path}") @@ -58,6 +60,12 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): self.send_headers_common() +def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): + ph.send_response_only(200, "bharatavarshe") + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + + @dataclass(frozen=True) class UrlReqResp: callOk: bool diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 3a1b5e104c..63b9a43ead 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -351,7 +351,7 @@ browser js runtime environment. Depending on the path specified wrt the proxy se (and not urlraw), it additionally tries to convert html content into equivalent text to some extent in a simple minded manner by dropping head block as well as all scripts/styles/footers/headers/nav. May add support for white list of allowed sites to access or so. -* the logic does a simple dumb check to see if there is something running at specified proxyUrl +* the logic does a simple check to see if the bundled simpleproxy is running at specified proxyUrl before enabling fetch web related tool calls. * The bundled simple proxy can be found at * tools/server/public_simplechat/local.tools/simpleproxy.py diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index b195288aa6..755d9eae75 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -133,12 +133,18 @@ function fetchweburlraw_run(toolcallid, toolname, obj) { /** * Setup fetch_web_url_raw for tool calling - * NOTE: Currently it just checks there is something at given proxyUrl + * NOTE: Currently the logic is setup for the bundled simpleproxy.py * @param {Object>} tcs */ async function fetchweburlraw_setup(tcs) { // @ts-ignore - let got = await fetch(`${document["gMe"].proxyUrl}/urlraw?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ + let got = await fetch(`${document["gMe"].proxyUrl}/aum?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ + if (resp.statusText != 'bharatavarshe') { + console.log("WARN:ToolJS:FetchWebUrlRaw:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") + return + } else { + console.log("INFO:ToolJS:FetchWebUrlRaw:Enabling...") + } tcs["fetch_web_url_raw"] = { "handler": fetchweburlraw_run, "meta": fetchweburlraw_meta, @@ -201,12 +207,18 @@ function fetchweburltext_run(toolcallid, toolname, obj) { /** * Setup fetch_web_url_text for tool calling - * NOTE: Currently it just checks there is something at given proxyUrl + * NOTE: Currently the logic is setup for the bundled simpleproxy.py * @param {Object>} tcs */ async function fetchweburltext_setup(tcs) { // @ts-ignore - let got = await fetch(`${document["gMe"].proxyUrl}/urltext?url=jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ + let got = await fetch(`${document["gMe"].proxyUrl}/aum?url=jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ + if (resp.statusText != 'bharatavarshe') { + console.log("WARN:ToolJS:FetchWebUrlText:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") + return + } else { + console.log("INFO:ToolJS:FetchWebUrlText:Enabling...") + } tcs["fetch_web_url_text"] = { "handler": fetchweburltext_run, "meta": fetchweburltext_meta, From 2a94cb3786117821d0f3177fc0c67339b01eabd7 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 20 Oct 2025 21:01:23 +0530 Subject: [PATCH 104/446] SimpleChatTC:Fetch:Proxy URL rename and in settings --- tools/server/public_simplechat/simplechat.js | 4 ++-- tools/server/public_simplechat/tooljs.mjs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index eaea16223f..89f8ce435b 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1044,7 +1044,7 @@ class Me { //"frequency_penalty": 1.2, //"presence_penalty": 1.2, }; - this.proxyUrl = "http://127.0.0.1:3128" + this.toolFetchProxyUrl = "http://127.0.0.1:3128" } /** @@ -1121,7 +1121,7 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "bTools", "apiRequestOptions", "TRAPME-apiEP", "TRAPME-iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ + ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "bTools", "apiRequestOptions", "TRAPME-apiEP", "TRAPME-iRecentUserMsgCnt", "toolFetchProxyUrl", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ if (prop == "headers:Authorization") { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 755d9eae75..7a217e2ef6 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -116,7 +116,7 @@ let fetchweburlraw_meta = { function fetchweburlraw_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { // @ts-ignore - let newUrl = `${document['gMe'].proxyUrl}/urlraw?url=${encodeURIComponent(obj.url)}` + let newUrl = `${document['gMe'].toolFetchProxyUrl}/urlraw?url=${encodeURIComponent(obj.url)}` fetch(newUrl).then(resp => { if (!resp.ok) { throw new Error(`${resp.status}:${resp.statusText}`); @@ -138,7 +138,7 @@ function fetchweburlraw_run(toolcallid, toolname, obj) { */ async function fetchweburlraw_setup(tcs) { // @ts-ignore - let got = await fetch(`${document["gMe"].proxyUrl}/aum?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ + let got = await fetch(`${document["gMe"].toolFetchProxyUrl}/aum?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ if (resp.statusText != 'bharatavarshe') { console.log("WARN:ToolJS:FetchWebUrlRaw:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") return @@ -190,7 +190,7 @@ let fetchweburltext_meta = { function fetchweburltext_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { // @ts-ignore - let newUrl = `${document['gMe'].proxyUrl}/urltext?url=${encodeURIComponent(obj.url)}` + let newUrl = `${document['gMe'].toolFetchProxyUrl}/urltext?url=${encodeURIComponent(obj.url)}` fetch(newUrl).then(resp => { if (!resp.ok) { throw new Error(`${resp.status}:${resp.statusText}`); @@ -212,7 +212,7 @@ function fetchweburltext_run(toolcallid, toolname, obj) { */ async function fetchweburltext_setup(tcs) { // @ts-ignore - let got = await fetch(`${document["gMe"].proxyUrl}/aum?url=jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ + let got = await fetch(`${document["gMe"].toolFetchProxyUrl}/aum?url=jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ if (resp.statusText != 'bharatavarshe') { console.log("WARN:ToolJS:FetchWebUrlText:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") return From 34b2beea1a26fe7927172b7b0aa4228a2d526714 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 01:57:57 +0530 Subject: [PATCH 105/446] SimpleChatTC:ShowInfo: Create and use common automated info show Also fetch info from ai-server, and place path and ctx size into current Me instance and include in show info. --- tools/server/public_simplechat/simplechat.js | 43 +++++--------------- tools/server/public_simplechat/ui.mjs | 23 +++++++++++ 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 89f8ce435b..1ea8dd62e2 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -402,8 +402,9 @@ class SimpleChat { * Show the contents in the specified div * @param {HTMLDivElement} div * @param {boolean} bClear + * @param {boolean} bShowInfoAll */ - show(div, bClear=true) { + show(div, bClear=true, bShowInfoAll=false) { if (bClear) { div.replaceChildren(); } @@ -419,7 +420,7 @@ class SimpleChat { if (bClear) { div.innerHTML = gUsageMsg; gMe.setup_load(div, this); - gMe.show_info(div); + gMe.show_info(div, bShowInfoAll); } } return last; @@ -1072,7 +1073,7 @@ class Me { console.log("DBUG:SimpleChat:SC:Load", chat); chat.load(); queueMicrotask(()=>{ - chat.show(div); + chat.show(div, true, true); this.multiChat.elInSystem.value = chat.get_system_latest().ns.content; }); }); @@ -1085,35 +1086,13 @@ class Me { * @param {boolean} bAll */ show_info(elDiv, bAll=false) { - - let p = ui.el_create_append_p("Settings (devel-tools-console document[gMe])", elDiv); - p.className = "role-system"; - - if (bAll) { - - ui.el_create_append_p(`baseURL:${this.baseURL}`, elDiv); - - ui.el_create_append_p(`Authorization:${this.headers["Authorization"]}`, elDiv); - - ui.el_create_append_p(`bStream:${this.bStream}`, elDiv); - - ui.el_create_append_p(`bTools:${this.bTools}`, elDiv); - - ui.el_create_append_p(`bTrimGarbage:${this.bTrimGarbage}`, elDiv); - - ui.el_create_append_p(`ApiEndPoint:${this.apiEP}`, elDiv); - - ui.el_create_append_p(`iRecentUserMsgCnt:${this.iRecentUserMsgCnt}`, elDiv); - - ui.el_create_append_p(`bCompletionFreshChatAlways:${this.bCompletionFreshChatAlways}`, elDiv); - - ui.el_create_append_p(`bCompletionInsertStandardRolePrefix:${this.bCompletionInsertStandardRolePrefix}`, elDiv); - - } - - ui.el_create_append_p(`apiRequestOptions:${JSON.stringify(this.apiRequestOptions, null, " - ")}`, elDiv); - ui.el_create_append_p(`headers:${JSON.stringify(this.headers, null, " - ")}`, elDiv); - + fetch(`${this.baseURL}/props`).then(resp=>resp.json()).then(json=>{ + this.modelInfo = { + modelPath: json["model_path"], + ctxSize: json["default_generation_settings"]["n_ctx"] + } + ui.ui_show_obj_props_info(elDiv, this, ["baseURL", "modelInfo","headers", "bStream", "bTools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "toolFetchProxyUrl", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings/Info (devel-tools-console document[gMe])") + }).catch(err=>console.log(`WARN:ShowInfo:${err}`)) } /** diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index b16a19ba77..23ec676315 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -303,3 +303,26 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, fRefiner=un } } } + + +/** + * Show the specified properties and their values wrt the given object. + * @param {HTMLElement | undefined} elDiv + * @param {any} oObj + * @param {Array} lProps + * @param {string} sLegend + */ +export function ui_show_obj_props_info(elDiv, oObj, lProps, sLegend) { + let p = el_create_append_p(`${sLegend}`, elDiv); + p.className = "role-system"; + + for (const k of lProps) { + let val = oObj[k]; + let vtype = typeof(val) + if (vtype != 'object') { + el_create_append_p(`${k}:${oObj[k]}`, elDiv) + } else { + el_create_append_p(`${k}:${JSON.stringify(oObj[k], null, " - ")}`, elDiv); + } + } +} From 24ba85026e8b93511d4cfcbc1b60fee879e51bd2 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 02:14:17 +0530 Subject: [PATCH 106/446] SimpleChatTC:ShowInfo: Make logic recursive, avoid JSON.stringify --- tools/server/public_simplechat/ui.mjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 23ec676315..b051c6a713 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -312,17 +312,21 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, fRefiner=un * @param {Array} lProps * @param {string} sLegend */ -export function ui_show_obj_props_info(elDiv, oObj, lProps, sLegend) { +export function ui_show_obj_props_info(elDiv, oObj, lProps, sLegend, sOffset="") { let p = el_create_append_p(`${sLegend}`, elDiv); - p.className = "role-system"; + if (sOffset.length == 0) { + p.className = "role-system"; + } for (const k of lProps) { + let kPrint = `${sOffset}${k}` let val = oObj[k]; let vtype = typeof(val) if (vtype != 'object') { - el_create_append_p(`${k}:${oObj[k]}`, elDiv) + el_create_append_p(`${kPrint}:${oObj[k]}`, elDiv) } else { - el_create_append_p(`${k}:${JSON.stringify(oObj[k], null, " - ")}`, elDiv); + ui_show_obj_props_info(elDiv, val, Object.keys(val), kPrint, `>${sOffset}`) + //el_create_append_p(`${k}:${JSON.stringify(oObj[k], null, " - ")}`, elDiv); } } } From fc26e47222eb2dadb28a6e6c6c818dd11bcade3c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 18:53:49 +0530 Subject: [PATCH 107/446] SimpleChatTC:ShowObjPropsInfo: Use sections to indicate relations Also create a top level div wrt whole. And allow class to be specified for the same as well as the top level legend, optionally --- tools/server/public_simplechat/simplechat.js | 2 +- tools/server/public_simplechat/ui.mjs | 32 +++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 1ea8dd62e2..c352be0300 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1091,7 +1091,7 @@ class Me { modelPath: json["model_path"], ctxSize: json["default_generation_settings"]["n_ctx"] } - ui.ui_show_obj_props_info(elDiv, this, ["baseURL", "modelInfo","headers", "bStream", "bTools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "toolFetchProxyUrl", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings/Info (devel-tools-console document[gMe])") + ui.ui_show_obj_props_info(elDiv, this, ["baseURL", "modelInfo","headers", "bStream", "bTools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "toolFetchProxyUrl", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings/Info (devel-tools-console document[gMe])", "", { legend: 'role-system' }) }).catch(err=>console.log(`WARN:ShowInfo:${err}`)) } diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index b051c6a713..24756c4414 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -306,26 +306,44 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, fRefiner=un /** - * Show the specified properties and their values wrt the given object. - * @param {HTMLElement | undefined} elDiv + * Show the specified properties and their values wrt the given object, + * with in the elParent provided. + * @param {HTMLDivElement | HTMLElement} elParent * @param {any} oObj * @param {Array} lProps * @param {string} sLegend + * @param {string} sOffset - can be used to prefix each of the prop entries + * @param {any | undefined} dClassNames - can specify class for top level div and legend */ -export function ui_show_obj_props_info(elDiv, oObj, lProps, sLegend, sOffset="") { - let p = el_create_append_p(`${sLegend}`, elDiv); +export function ui_show_obj_props_info(elParent, oObj, lProps, sLegend, sOffset="", dClassNames=undefined) { if (sOffset.length == 0) { - p.className = "role-system"; + let div = document.createElement("div"); + div.classList.add(`DivObjPropsInfoL${sOffset.length}`) + elParent.appendChild(div) + elParent = div } + let elPLegend = el_create_append_p(sLegend, elParent) + if (dClassNames) { + if (dClassNames['div']) { + elParent.className = dClassNames['div'] + } + if (dClassNames['legend']) { + elPLegend.className = dClassNames['legend'] + } + } + let elS = document.createElement("section"); + elS.classList.add(`SectionObjPropsInfoL${sOffset.length}`) + elParent.appendChild(elPLegend); + elParent.appendChild(elS); for (const k of lProps) { let kPrint = `${sOffset}${k}` let val = oObj[k]; let vtype = typeof(val) if (vtype != 'object') { - el_create_append_p(`${kPrint}:${oObj[k]}`, elDiv) + el_create_append_p(`${kPrint}: ${oObj[k]}`, elS) } else { - ui_show_obj_props_info(elDiv, val, Object.keys(val), kPrint, `>${sOffset}`) + ui_show_obj_props_info(elS, val, Object.keys(val), kPrint, `>${sOffset}`) //el_create_append_p(`${k}:${JSON.stringify(oObj[k], null, " - ")}`, elDiv); } } From 0e21d67e8ae055dd848894116a5cddabe7d90528 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 19:37:18 +0530 Subject: [PATCH 108/446] SimpleChatTC:ShowInfo: Allow showing minimal info set, if needed --- tools/server/public_simplechat/simplechat.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index c352be0300..03791f4455 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -399,7 +399,11 @@ class SimpleChat { } /** - * Show the contents in the specified div + * Show the chat contents in the specified div. + * If requested to clear prev stuff and inturn no chat content then show + * * usage info + * * option to load prev saved chat if any + * * as well as settings/info. * @param {HTMLDivElement} div * @param {boolean} bClear * @param {boolean} bShowInfoAll @@ -1086,12 +1090,16 @@ class Me { * @param {boolean} bAll */ show_info(elDiv, bAll=false) { + let props = ["baseURL", "modelInfo","headers", "bStream", "bTools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "toolFetchProxyUrl", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"]; + if (!bAll) { + props = [ "baseURL", "modelInfo", "headers", "bStream", "bTools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt" ]; + } fetch(`${this.baseURL}/props`).then(resp=>resp.json()).then(json=>{ this.modelInfo = { modelPath: json["model_path"], ctxSize: json["default_generation_settings"]["n_ctx"] } - ui.ui_show_obj_props_info(elDiv, this, ["baseURL", "modelInfo","headers", "bStream", "bTools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "toolFetchProxyUrl", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings/Info (devel-tools-console document[gMe])", "", { legend: 'role-system' }) + ui.ui_show_obj_props_info(elDiv, this, props, "Settings/Info (devel-tools-console document[gMe])", "", { legend: 'role-system' }) }).catch(err=>console.log(`WARN:ShowInfo:${err}`)) } From 303af1800e9bf26b216cbe9ffe5eaed7be28dc9a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 19:57:53 +0530 Subject: [PATCH 109/446] SimpleChatTC:ShowInfo:Clean up layout of showing of props data Also ensure when switching between sessions, the full set of props info is shown. --- tools/server/public_simplechat/simplechat.css | 10 ++++++++++ tools/server/public_simplechat/simplechat.js | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.css b/tools/server/public_simplechat/simplechat.css index d4755074b7..98e88d99fb 100644 --- a/tools/server/public_simplechat/simplechat.css +++ b/tools/server/public_simplechat/simplechat.css @@ -69,10 +69,20 @@ button { padding-inline-start: 2vw; } + +.DivObjPropsInfoL0 { + margin: 0%; +} +[class^=SectionObjPropsInfoL] { + margin-left: 2vmin; +} + + * { margin: 0.6vmin; } + @media print { #fullbody { diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 03791f4455..20a13c65bd 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1000,7 +1000,7 @@ class MultiChatUI { } this.elInSystem.value = chat.get_system_latest().ns.content; this.elInUser.value = ""; - chat.show(this.elDivChat); + chat.show(this.elDivChat, true, true); this.elInUser.focus(); this.curChatId = chatId; console.log(`INFO:SimpleChat:MCUI:HandleSessionSwitch:${chatId} entered...`); From 3e490cefc50e40b9175e55958e28ef5d433d135a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 21:44:30 +0530 Subject: [PATCH 110/446] SimpleChatTC:Cleanup: Move bTools and toolFetchProxyUrl into tools Also update the readme wrt same and related --- tools/server/public_simplechat/readme.md | 17 +++++++++++++---- tools/server/public_simplechat/simplechat.js | 14 ++++++++------ tools/server/public_simplechat/tooljs.mjs | 8 ++++---- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 63b9a43ead..4ed51a0f6d 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -74,10 +74,15 @@ remember to * pass --jinja to llama-server to enable tool calling support from the server ai engine end. -* enable bTools in the settings page of the client side gui. +* set tools.enabled to true in the settings page of the client side gui. * use a GenAi/LLM model which supports tool calling. +* if fetch web url / page tool call is needed, remember to run the bundled local.tools/simpleproxy.py helper + + * remember that this is a relatively dumb proxy logic along with optional stripping of scripts/styles/headers/footers/..., + Be careful if trying to fetch web pages, and use it only with known safe sites. + ### using the front end Open this simple web front end from your local browser @@ -184,9 +189,13 @@ It is attached to the document object. Some of these can also be updated using t inturn the machine goes into power saving mode or so, the platform may stop network connection, leading to exception. - bTools - control whether tool calling is enabled or not + tools - contains controls related to tool calling - remember to enable this only for GenAi/LLM models which support tool/function calling. + enabled - control whether tool calling is enabled or not + + remember to enable this only for GenAi/LLM models which support tool/function calling. + + fetchProxyUrl - specify the address for the running instance of bundled local.tools/simpleproxy.py the builtin tools' meta data is sent to the ai model in the requests sent to it. @@ -351,7 +360,7 @@ browser js runtime environment. Depending on the path specified wrt the proxy se (and not urlraw), it additionally tries to convert html content into equivalent text to some extent in a simple minded manner by dropping head block as well as all scripts/styles/footers/headers/nav. May add support for white list of allowed sites to access or so. -* the logic does a simple check to see if the bundled simpleproxy is running at specified proxyUrl +* the logic does a simple check to see if the bundled simpleproxy is running at specified fetchProxyUrl before enabling fetch web related tool calls. * The bundled simple proxy can be found at * tools/server/public_simplechat/local.tools/simpleproxy.py diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 20a13c65bd..9a2321690a 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -462,7 +462,7 @@ class SimpleChat { if (gMe.bStream) { obj["stream"] = true; } - if (gMe.bTools) { + if (gMe.tools.enabled) { obj["tools"] = tools.meta(); } return JSON.stringify(obj); @@ -1016,7 +1016,10 @@ class Me { this.defaultChatIds = [ "Default", "Other" ]; this.multiChat = new MultiChatUI(); this.bStream = true; - this.bTools = false; + this.tools = { + enabled: false, + fetchProxyUrl: "http://127.0.0.1:3128" + }; this.bCompletionFreshChatAlways = true; this.bCompletionInsertStandardRolePrefix = false; this.bTrimGarbage = true; @@ -1049,7 +1052,6 @@ class Me { //"frequency_penalty": 1.2, //"presence_penalty": 1.2, }; - this.toolFetchProxyUrl = "http://127.0.0.1:3128" } /** @@ -1090,9 +1092,9 @@ class Me { * @param {boolean} bAll */ show_info(elDiv, bAll=false) { - let props = ["baseURL", "modelInfo","headers", "bStream", "bTools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "toolFetchProxyUrl", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"]; + let props = ["baseURL", "modelInfo","headers", "bStream", "tools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"]; if (!bAll) { - props = [ "baseURL", "modelInfo", "headers", "bStream", "bTools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt" ]; + props = [ "baseURL", "modelInfo", "headers", "bStream", "tools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt" ]; } fetch(`${this.baseURL}/props`).then(resp=>resp.json()).then(json=>{ this.modelInfo = { @@ -1108,7 +1110,7 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "bTools", "apiRequestOptions", "TRAPME-apiEP", "TRAPME-iRecentUserMsgCnt", "toolFetchProxyUrl", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ + ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "tools", "apiRequestOptions", "TRAPME-apiEP", "TRAPME-iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ if (prop == "headers:Authorization") { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 7a217e2ef6..3943ca4534 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -116,7 +116,7 @@ let fetchweburlraw_meta = { function fetchweburlraw_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { // @ts-ignore - let newUrl = `${document['gMe'].toolFetchProxyUrl}/urlraw?url=${encodeURIComponent(obj.url)}` + let newUrl = `${document['gMe'].tools.fetchProxyUrl}/urlraw?url=${encodeURIComponent(obj.url)}` fetch(newUrl).then(resp => { if (!resp.ok) { throw new Error(`${resp.status}:${resp.statusText}`); @@ -138,7 +138,7 @@ function fetchweburlraw_run(toolcallid, toolname, obj) { */ async function fetchweburlraw_setup(tcs) { // @ts-ignore - let got = await fetch(`${document["gMe"].toolFetchProxyUrl}/aum?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ + let got = await fetch(`${document["gMe"].tools.fetchProxyUrl}/aum?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ if (resp.statusText != 'bharatavarshe') { console.log("WARN:ToolJS:FetchWebUrlRaw:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") return @@ -190,7 +190,7 @@ let fetchweburltext_meta = { function fetchweburltext_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { // @ts-ignore - let newUrl = `${document['gMe'].toolFetchProxyUrl}/urltext?url=${encodeURIComponent(obj.url)}` + let newUrl = `${document['gMe'].tools.fetchProxyUrl}/urltext?url=${encodeURIComponent(obj.url)}` fetch(newUrl).then(resp => { if (!resp.ok) { throw new Error(`${resp.status}:${resp.statusText}`); @@ -212,7 +212,7 @@ function fetchweburltext_run(toolcallid, toolname, obj) { */ async function fetchweburltext_setup(tcs) { // @ts-ignore - let got = await fetch(`${document["gMe"].toolFetchProxyUrl}/aum?url=jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ + let got = await fetch(`${document["gMe"].tools.fetchProxyUrl}/aum?url=jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ if (resp.statusText != 'bharatavarshe') { console.log("WARN:ToolJS:FetchWebUrlText:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") return From 03426f027666a623cc32850806701ccf880d181d Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 21:55:23 +0530 Subject: [PATCH 111/446] SimpleChatTC:Cleanup:EditObjProps: rename vars followingConvention Part 1 - add el prefix wrt the element handle related vars --- tools/server/public_simplechat/ui.mjs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 24756c4414..3317c6c82a 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -258,16 +258,16 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, fRefiner=un "string": "text", "number": "number", }; - let fs = document.createElement("fieldset"); - let legend = document.createElement("legend"); - legend.innerText = sLegend; - fs.appendChild(legend); - elDiv.appendChild(fs); + let elFS = document.createElement("fieldset"); + let elLegend = document.createElement("legend"); + elLegend.innerText = sLegend; + elFS.appendChild(elLegend); + elDiv.appendChild(elFS); for(const k of lProps) { if (sTrapTag) { if (k.startsWith(sTrapTag)) { if (fTrapper) { - fTrapper(k, fs) + fTrapper(k, elFS) } continue } @@ -284,7 +284,7 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, fRefiner=un if (fRefiner) { fRefiner(k, inp.el) } - fs.appendChild(inp.div); + elFS.appendChild(inp.div); } else if (type == "boolean") { let bbtn = el_creatediv_boolbutton(`Set{k}`, k, {true: "true", false: "false"}, val, (userVal)=>{ oObj[k] = userVal; @@ -292,9 +292,9 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, fRefiner=un if (fRefiner) { fRefiner(k, bbtn.el) } - fs.appendChild(bbtn.div); + elFS.appendChild(bbtn.div); } else if (type == "object") { - ui_show_obj_props_edit(fs, val, Object.keys(val), k, (prop, elProp)=>{ + ui_show_obj_props_edit(elFS, val, Object.keys(val), k, (prop, elProp)=>{ if (fRefiner) { let theProp = `${k}:${prop}` fRefiner(theProp, elProp) From b19e754322751f655184befcc1f9788ac3fc6079 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 22:00:12 +0530 Subject: [PATCH 112/446] SimpleChatTC:Cleanup:Rename func arg to match semantic better --- tools/server/public_simplechat/ui.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 3317c6c82a..761e0571c5 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -245,7 +245,7 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= * * prefix the prop name in lProps with sTrapTag * * fTrapper will be called with the parent ui element * into which the new ui elements created for editting the prop, if any, should be attached - * @param {HTMLDivElement|HTMLFieldSetElement} elDiv + * @param {HTMLDivElement|HTMLFieldSetElement} elParent * @param {any} oObj * @param {Array} lProps * @param {string} sLegend @@ -253,7 +253,7 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= * @param {string | undefined} sTrapTag * @param {((tagPlusProp: string, elParent: HTMLFieldSetElement)=>void) | undefined} fTrapper */ -export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, fRefiner=undefined, sTrapTag=undefined, fTrapper=undefined) { +export function ui_show_obj_props_edit(elParent, oObj, lProps, sLegend, fRefiner=undefined, sTrapTag=undefined, fTrapper=undefined) { let typeDict = { "string": "text", "number": "number", @@ -262,7 +262,7 @@ export function ui_show_obj_props_edit(elDiv, oObj, lProps, sLegend, fRefiner=un let elLegend = document.createElement("legend"); elLegend.innerText = sLegend; elFS.appendChild(elLegend); - elDiv.appendChild(elFS); + elParent.appendChild(elFS); for(const k of lProps) { if (sTrapTag) { if (k.startsWith(sTrapTag)) { @@ -344,7 +344,7 @@ export function ui_show_obj_props_info(elParent, oObj, lProps, sLegend, sOffset= el_create_append_p(`${kPrint}: ${oObj[k]}`, elS) } else { ui_show_obj_props_info(elS, val, Object.keys(val), kPrint, `>${sOffset}`) - //el_create_append_p(`${k}:${JSON.stringify(oObj[k], null, " - ")}`, elDiv); + //el_create_append_p(`${k}:${JSON.stringify(oObj[k], null, " - ")}`, elS); } } } From 8d7eb687127bfaa2711f3480261383bf5aa042b9 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 22:15:58 +0530 Subject: [PATCH 113/446] SimpleChatTC:ShowObjPropsEdit:Any depth trapping of ui setup Maintain the current property hierarchy to its root over recursive calls. Allow callers to specify the props to be trapped using the prop hierarchy. Pass the prop hierarchy to the fTrapper. This should allow one to trap any prop wrt its editing ui setup, irrespective of whether it is a prop of the main object passed, or a member of a child prop of the main object passed or so ... Update the setting up of ChatHistoryInCtxt and ApiEndPoint to follow the new semantic/flow. --- tools/server/public_simplechat/simplechat.js | 8 ++++---- tools/server/public_simplechat/ui.mjs | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 9a2321690a..91e6b4647c 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1110,20 +1110,20 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, this, ["baseURL", "headers", "bStream", "tools", "apiRequestOptions", "TRAPME-apiEP", "TRAPME-iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ + ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "bStream", "tools", "apiRequestOptions", "TRAPME-apiEP", "TRAPME-iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ if (prop == "headers:Authorization") { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; } - }, "TRAPME-", (tag, elParent)=>{ - if (tag == "TRAPME-apiEP") { + }, ["apiEP", "iRecentUserMsgCnt"], (propWithPath, prop, elParent)=>{ + if (propWithPath == "apiEP") { let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ // @ts-ignore this.apiEP = ApiEP.Type[val]; }); elParent.appendChild(sel.div); } - if (tag == "TRAPME-iRecentUserMsgCnt") { + if (propWithPath == "iRecentUserMsgCnt") { let sel = ui.el_creatediv_select("SetChatHistoryInCtxt", "ChatHistoryInCtxt", this.sRecentUserMsgCnt, this.iRecentUserMsgCnt, (val)=>{ this.iRecentUserMsgCnt = this.sRecentUserMsgCnt[val]; }); diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 761e0571c5..0497924581 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -246,14 +246,15 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= * * fTrapper will be called with the parent ui element * into which the new ui elements created for editting the prop, if any, should be attached * @param {HTMLDivElement|HTMLFieldSetElement} elParent + * @param {string} propsTreeRoot * @param {any} oObj * @param {Array} lProps * @param {string} sLegend * @param {((prop:string, elProp: HTMLElement)=>void)| undefined} fRefiner - * @param {string | undefined} sTrapTag - * @param {((tagPlusProp: string, elParent: HTMLFieldSetElement)=>void) | undefined} fTrapper + * @param {Array | undefined} lTrapThese + * @param {((propWithPath: string, prop: string, elParent: HTMLFieldSetElement)=>void) | undefined} fTrapper */ -export function ui_show_obj_props_edit(elParent, oObj, lProps, sLegend, fRefiner=undefined, sTrapTag=undefined, fTrapper=undefined) { +export function ui_show_obj_props_edit(elParent, propsTreeRoot, oObj, lProps, sLegend, fRefiner=undefined, lTrapThese=undefined, fTrapper=undefined) { let typeDict = { "string": "text", "number": "number", @@ -264,10 +265,11 @@ export function ui_show_obj_props_edit(elParent, oObj, lProps, sLegend, fRefiner elFS.appendChild(elLegend); elParent.appendChild(elFS); for(const k of lProps) { - if (sTrapTag) { - if (k.startsWith(sTrapTag)) { + let propsTreeRootNew = `${propsTreeRoot}:${k}` + if (lTrapThese) { + if (propsTreeRootNew in lTrapThese) { if (fTrapper) { - fTrapper(k, elFS) + fTrapper(propsTreeRootNew, k, elFS) } continue } @@ -294,12 +296,12 @@ export function ui_show_obj_props_edit(elParent, oObj, lProps, sLegend, fRefiner } elFS.appendChild(bbtn.div); } else if (type == "object") { - ui_show_obj_props_edit(elFS, val, Object.keys(val), k, (prop, elProp)=>{ + ui_show_obj_props_edit(elFS, propsTreeRootNew, val, Object.keys(val), k, (prop, elProp)=>{ if (fRefiner) { let theProp = `${k}:${prop}` fRefiner(theProp, elProp) } - }) + }, lTrapThese, fTrapper) } } } From a54fa472dd48215aa84b9c6d57ff0b36630dbec8 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 23:06:44 +0530 Subject: [PATCH 114/446] SimpleChatTC:ShowObjPropsEdit:Any depth trapping of ui setup - t2 Fix up the oversights wrt any depth trapping flow Remember to start the propWithTree being checked/trapped with : to indicate the root of the prop hierarchy and also use : as sep between the elements of the props hierarchy tree Also had forgotten about the goof up possible with using in in a condition statement to check for array to contain a entry of interest in JS, fixed it now. --- tools/server/public_simplechat/simplechat.js | 8 ++++---- tools/server/public_simplechat/ui.mjs | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 91e6b4647c..daa5b5052f 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1110,20 +1110,20 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "bStream", "tools", "apiRequestOptions", "TRAPME-apiEP", "TRAPME-iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ + ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "bStream", "tools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ if (prop == "headers:Authorization") { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; } - }, ["apiEP", "iRecentUserMsgCnt"], (propWithPath, prop, elParent)=>{ - if (propWithPath == "apiEP") { + }, [":apiEP", ":iRecentUserMsgCnt"], (propWithPath, prop, elParent)=>{ + if (propWithPath == ":apiEP") { let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ // @ts-ignore this.apiEP = ApiEP.Type[val]; }); elParent.appendChild(sel.div); } - if (propWithPath == "iRecentUserMsgCnt") { + if (propWithPath == ":iRecentUserMsgCnt") { let sel = ui.el_creatediv_select("SetChatHistoryInCtxt", "ChatHistoryInCtxt", this.sRecentUserMsgCnt, this.iRecentUserMsgCnt, (val)=>{ this.iRecentUserMsgCnt = this.sRecentUserMsgCnt[val]; }); diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 0497924581..fb447d3e6e 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -242,9 +242,13 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= * The fRefiner callback even helps work with Obj with-in Obj scenarios. * * For some reason if caller wants to handle certain properties on their own - * * prefix the prop name in lProps with sTrapTag + * * specify the prop name of interest along with its prop-tree-hierarchy in lTrapThese + * * always start with : when ever refering to propWithPath, + * as it indirectly signifies root of properties tree + * * remember to seperate the properties tree hierarchy members using : * * fTrapper will be called with the parent ui element - * into which the new ui elements created for editting the prop, if any, should be attached + * into which the new ui elements created for editting the prop, if any, should be attached, + * along with the current prop of interest and its full propWithPath representation. * @param {HTMLDivElement|HTMLFieldSetElement} elParent * @param {string} propsTreeRoot * @param {any} oObj @@ -267,7 +271,7 @@ export function ui_show_obj_props_edit(elParent, propsTreeRoot, oObj, lProps, sL for(const k of lProps) { let propsTreeRootNew = `${propsTreeRoot}:${k}` if (lTrapThese) { - if (propsTreeRootNew in lTrapThese) { + if (lTrapThese.indexOf(propsTreeRootNew) != -1) { if (fTrapper) { fTrapper(propsTreeRootNew, k, elFS) } From 7409b298622ed8ab58e66a5f32ea8d13d07cc165 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 23:37:33 +0530 Subject: [PATCH 115/446] SimpleChatTC:Cleanup:ChatProps: Move bStream into it --- tools/server/public_simplechat/readme.md | 18 ++++++++++-------- tools/server/public_simplechat/simplechat.js | 14 ++++++++------ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 4ed51a0f6d..418a765972 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -177,17 +177,19 @@ It is attached to the document object. Some of these can also be updated using t baseURL - the domain-name/ip-address and inturn the port to send the request. - bStream - control between oneshot-at-end and live-stream-as-its-generated collating and showing - of the generated response. + chatProps - maintain a set of properties which manipulate chatting with ai engine - the logic assumes that the text sent from the server follows utf-8 encoding. + stream - control between oneshot-at-end and live-stream-as-its-generated collating and showing + of the generated response. - in streaming mode - if there is any exception, the logic traps the same and tries to ensure - that text generated till then is not lost. + the logic assumes that the text sent from the server follows utf-8 encoding. - if a very long text is being generated, which leads to no user interaction for sometime and - inturn the machine goes into power saving mode or so, the platform may stop network connection, - leading to exception. + in streaming mode - if there is any exception, the logic traps the same and tries to ensure + that text generated till then is not lost. + + if a very long text is being generated, which leads to no user interaction for sometime and + inturn the machine goes into power saving mode or so, the platform may stop network connection, + leading to exception. tools - contains controls related to tool calling diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index daa5b5052f..8696227193 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -459,7 +459,7 @@ class SimpleChat { for(let k in gMe.apiRequestOptions) { obj[k] = gMe.apiRequestOptions[k]; } - if (gMe.bStream) { + if (gMe.chatProps.stream) { obj["stream"] = true; } if (gMe.tools.enabled) { @@ -622,7 +622,7 @@ class SimpleChat { */ async handle_response(resp, apiEP, elDiv) { let theResp = null; - if (gMe.bStream) { + if (gMe.chatProps.stream) { try { theResp = await this.handle_response_multipart(resp, apiEP, elDiv); this.latestResponse.clear(); @@ -1015,11 +1015,13 @@ class Me { this.baseURL = "http://127.0.0.1:8080"; this.defaultChatIds = [ "Default", "Other" ]; this.multiChat = new MultiChatUI(); - this.bStream = true; this.tools = { enabled: false, fetchProxyUrl: "http://127.0.0.1:3128" }; + this.chatProps = { + stream: true, + } this.bCompletionFreshChatAlways = true; this.bCompletionInsertStandardRolePrefix = false; this.bTrimGarbage = true; @@ -1092,9 +1094,9 @@ class Me { * @param {boolean} bAll */ show_info(elDiv, bAll=false) { - let props = ["baseURL", "modelInfo","headers", "bStream", "tools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"]; + let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"]; if (!bAll) { - props = [ "baseURL", "modelInfo", "headers", "bStream", "tools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt" ]; + props = [ "baseURL", "modelInfo", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "iRecentUserMsgCnt" ]; } fetch(`${this.baseURL}/props`).then(resp=>resp.json()).then(json=>{ this.modelInfo = { @@ -1110,7 +1112,7 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "bStream", "tools", "apiRequestOptions", "apiEP", "iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ + ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ if (prop == "headers:Authorization") { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; From 78ccca056fc06c961a543973979dfe663e2a3ae1 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 21 Oct 2025 23:44:39 +0530 Subject: [PATCH 116/446] SimpleChatTC:Cleanup:ChatProps: iRecentUserMsgCnt Update Me class Update show settings Update show props info Update readme --- tools/server/public_simplechat/readme.md | 35 ++++++++++---------- tools/server/public_simplechat/simplechat.js | 24 +++++++------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 418a765972..8e6c180738 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -191,6 +191,17 @@ It is attached to the document object. Some of these can also be updated using t inturn the machine goes into power saving mode or so, the platform may stop network connection, leading to exception. + iRecentUserMsgCnt - a simple minded SlidingWindow to limit context window load at Ai Model end. + This is set to 10 by default. So in addition to latest system message, last/latest iRecentUserMsgCnt + user messages after the latest system prompt and its responses from the ai model will be sent + to the ai-model, when querying for a new response. Note that if enabled, only user messages after + the latest system message/prompt will be considered. + + This specified sliding window user message count also includes the latest user query. + <0 : Send entire chat history to server + 0 : Send only the system message if any to the server + >0 : Send the latest chat history from the latest system prompt, limited to specified cnt. + tools - contains controls related to tool calling enabled - control whether tool calling is enabled or not @@ -250,22 +261,12 @@ It is attached to the document object. Some of these can also be updated using t Content-Type is set to application/json. Additionally Authorization entry is provided, which can be set if needed using the settings ui. - iRecentUserMsgCnt - a simple minded SlidingWindow to limit context window load at Ai Model end. - This is set to 10 by default. So in addition to latest system message, last/latest iRecentUserMsgCnt - user messages after the latest system prompt and its responses from the ai model will be sent - to the ai-model, when querying for a new response. Note that if enabled, only user messages after - the latest system message/prompt will be considered. - This specified sliding window user message count also includes the latest user query. - <0 : Send entire chat history to server - 0 : Send only the system message if any to the server - >0 : Send the latest chat history from the latest system prompt, limited to specified cnt. - - -By using gMe's iRecentUserMsgCnt and apiRequestOptions.max_tokens/n_predict one can try to control -the implications of loading of the ai-model's context window by chat history, wrt chat response to -some extent in a simple crude way. You may also want to control the context size enabled when the -server loads ai-model, on the server end. +By using gMe's chatProps.iRecentUserMsgCnt and apiRequestOptions.max_tokens/n_predict one can try to +control the implications of loading of the ai-model's context window by chat history, wrt chat response +to some extent in a simple crude way. You may also want to control the context size enabled when the +server loads ai-model, on the server end. One can look at the current context size set on the server +end by looking at the settings/info block shown when ever one switches-to/is-shown a new session. Sometimes the browser may be stuborn with caching of the file, so your updates to html/css/js @@ -288,8 +289,8 @@ the system prompt, anytime during the conversation or only at the beginning. By default things are setup to try and make the user experience a bit better, if possible. However a developer when testing the server of ai-model may want to change these value. -Using iRecentUserMsgCnt reduce chat history context sent to the server/ai-model to be -just the system-prompt, prev-user-request-and-ai-response and cur-user-request, instead of +Using chatProps.iRecentUserMsgCnt reduce chat history context sent to the server/ai-model to be +just the system-prompt, few prev-user-requests-and-ai-responses and cur-user-request, instead of full chat history. This way if there is any response with garbage/repeatation, it doesnt mess with things beyond the next question/request/query, in some ways. The trim garbage option also tries to help avoid issues with garbage in the context to an extent. diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 8696227193..05e6190321 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -413,7 +413,7 @@ class SimpleChat { div.replaceChildren(); } let last = undefined; - for(const x of this.recent_chat(gMe.iRecentUserMsgCnt)) { + for(const x of this.recent_chat(gMe.chatProps.iRecentUserMsgCnt)) { let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, div); entry.className = `role-${x.ns.role}`; last = entry; @@ -473,7 +473,7 @@ class SimpleChat { */ request_messages_jsonstr() { let req = { - messages: this.recent_chat_ns(gMe.iRecentUserMsgCnt), + messages: this.recent_chat_ns(gMe.chatProps.iRecentUserMsgCnt), } return this.request_jsonstr_extend(req); } @@ -485,7 +485,7 @@ class SimpleChat { request_prompt_jsonstr(bInsertStandardRolePrefix) { let prompt = ""; let iCnt = 0; - for(const msg of this.recent_chat(gMe.iRecentUserMsgCnt)) { + for(const msg of this.recent_chat(gMe.chatProps.iRecentUserMsgCnt)) { iCnt += 1; if (iCnt > 1) { prompt += "\n"; @@ -1021,11 +1021,11 @@ class Me { }; this.chatProps = { stream: true, - } + iRecentUserMsgCnt: 10, + }; this.bCompletionFreshChatAlways = true; this.bCompletionInsertStandardRolePrefix = false; this.bTrimGarbage = true; - this.iRecentUserMsgCnt = 10; /** @type {Object} */ this.sRecentUserMsgCnt = { "Full": -1, @@ -1094,9 +1094,9 @@ class Me { * @param {boolean} bAll */ show_info(elDiv, bAll=false) { - let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"]; + let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"]; if (!bAll) { - props = [ "baseURL", "modelInfo", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "iRecentUserMsgCnt" ]; + props = [ "baseURL", "modelInfo", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps" ]; } fetch(`${this.baseURL}/props`).then(resp=>resp.json()).then(json=>{ this.modelInfo = { @@ -1112,12 +1112,12 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "iRecentUserMsgCnt", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ + ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ if (prop == "headers:Authorization") { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; } - }, [":apiEP", ":iRecentUserMsgCnt"], (propWithPath, prop, elParent)=>{ + }, [":apiEP", ":chatProps:iRecentUserMsgCnt"], (propWithPath, prop, elParent)=>{ if (propWithPath == ":apiEP") { let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ // @ts-ignore @@ -1125,9 +1125,9 @@ class Me { }); elParent.appendChild(sel.div); } - if (propWithPath == ":iRecentUserMsgCnt") { - let sel = ui.el_creatediv_select("SetChatHistoryInCtxt", "ChatHistoryInCtxt", this.sRecentUserMsgCnt, this.iRecentUserMsgCnt, (val)=>{ - this.iRecentUserMsgCnt = this.sRecentUserMsgCnt[val]; + if (propWithPath == ":chatProps:iRecentUserMsgCnt") { + let sel = ui.el_creatediv_select("SetChatHistoryInCtxt", "ChatHistoryInCtxt", this.sRecentUserMsgCnt, this.chatProps.iRecentUserMsgCnt, (val)=>{ + this.chatProps.iRecentUserMsgCnt = this.sRecentUserMsgCnt[val]; }); elParent.appendChild(sel.div); } From 734f74c908c3b52c0db6f8580c2d66c295fa3490 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 22 Oct 2025 00:07:22 +0530 Subject: [PATCH 117/446] SimpleChatTC:Cleanup:ChatProps: bCompletionFreshChatAlways Moved into Me.chatProps --- tools/server/public_simplechat/readme.md | 6 +++--- tools/server/public_simplechat/simplechat.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 8e6c180738..287685be0b 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -202,6 +202,9 @@ It is attached to the document object. Some of these can also be updated using t 0 : Send only the system message if any to the server >0 : Send the latest chat history from the latest system prompt, limited to specified cnt. + bCompletionFreshChatAlways - whether Completion mode collates complete/sliding-window history when + communicating with the server or only sends the latest user query/message. + tools - contains controls related to tool calling enabled - control whether tool calling is enabled or not @@ -221,9 +224,6 @@ It is attached to the document object. Some of these can also be updated using t apiEP - select between /completions and /chat/completions endpoint provided by the server/ai-model. - bCompletionFreshChatAlways - whether Completion mode collates complete/sliding-window history when - communicating with the server or only sends the latest user query/message. - bCompletionInsertStandardRolePrefix - whether Completion mode inserts role related prefix wrt the messages that get inserted into prompt field wrt /Completion endpoint. diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 05e6190321..48aa673742 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -855,7 +855,7 @@ class MultiChatUI { // So if user wants to simulate a multi-chat based completion query, // they will have to enter the full thing, as a suitable multiline // user input/query. - if ((apiEP == ApiEP.Type.Completion) && (gMe.bCompletionFreshChatAlways)) { + if ((apiEP == ApiEP.Type.Completion) && (gMe.chatProps.bCompletionFreshChatAlways)) { chat.clear(); } @@ -1022,8 +1022,8 @@ class Me { this.chatProps = { stream: true, iRecentUserMsgCnt: 10, + bCompletionFreshChatAlways: true, }; - this.bCompletionFreshChatAlways = true; this.bCompletionInsertStandardRolePrefix = false; this.bTrimGarbage = true; /** @type {Object} */ @@ -1094,7 +1094,7 @@ class Me { * @param {boolean} bAll */ show_info(elDiv, bAll=false) { - let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"]; + let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage", "bCompletionInsertStandardRolePrefix"]; if (!bAll) { props = [ "baseURL", "modelInfo", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps" ]; } @@ -1112,7 +1112,7 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage", "bCompletionFreshChatAlways", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ + ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ if (prop == "headers:Authorization") { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; From 82be13aa33dd3e735824657ef43ee2a0637f61d7 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 22 Oct 2025 00:11:14 +0530 Subject: [PATCH 118/446] SimpleChatTC:Cleanup:ChatProps: bCompletionInsertStandardRolePrefix --- tools/server/public_simplechat/readme.md | 6 +++--- tools/server/public_simplechat/simplechat.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 287685be0b..e9fc991cf3 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -205,6 +205,9 @@ It is attached to the document object. Some of these can also be updated using t bCompletionFreshChatAlways - whether Completion mode collates complete/sliding-window history when communicating with the server or only sends the latest user query/message. + bCompletionInsertStandardRolePrefix - whether Completion mode inserts role related prefix wrt the + messages that get inserted into prompt field wrt /Completion endpoint. + tools - contains controls related to tool calling enabled - control whether tool calling is enabled or not @@ -224,9 +227,6 @@ It is attached to the document object. Some of these can also be updated using t apiEP - select between /completions and /chat/completions endpoint provided by the server/ai-model. - bCompletionInsertStandardRolePrefix - whether Completion mode inserts role related prefix wrt the - messages that get inserted into prompt field wrt /Completion endpoint. - bTrimGarbage - whether garbage repeatation at the end of the generated ai response, should be trimmed or left as is. If enabled, it will be trimmed so that it wont be sent back as part of subsequent chat history. At the same time the actual trimmed text is shown to the user, once diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 48aa673742..e26750fdd9 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -509,7 +509,7 @@ class SimpleChat { if (apiEP == ApiEP.Type.Chat) { return this.request_messages_jsonstr(); } else { - return this.request_prompt_jsonstr(gMe.bCompletionInsertStandardRolePrefix); + return this.request_prompt_jsonstr(gMe.chatProps.bCompletionInsertStandardRolePrefix); } } @@ -1023,8 +1023,8 @@ class Me { stream: true, iRecentUserMsgCnt: 10, bCompletionFreshChatAlways: true, + bCompletionInsertStandardRolePrefix: false, }; - this.bCompletionInsertStandardRolePrefix = false; this.bTrimGarbage = true; /** @type {Object} */ this.sRecentUserMsgCnt = { @@ -1094,7 +1094,7 @@ class Me { * @param {boolean} bAll */ show_info(elDiv, bAll=false) { - let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage", "bCompletionInsertStandardRolePrefix"]; + let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage"]; if (!bAll) { props = [ "baseURL", "modelInfo", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps" ]; } @@ -1112,7 +1112,7 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage", "bCompletionInsertStandardRolePrefix"], "Settings", (prop, elProp)=>{ + ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage"], "Settings", (prop, elProp)=>{ if (prop == "headers:Authorization") { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; From ad65659a632d277e0cca8022ba621b786a38e3a4 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 22 Oct 2025 00:20:41 +0530 Subject: [PATCH 119/446] SimpleChatTC:Cleanup:ChatProps: bTrimGarbage Also remove more inner/detailed stuff from show info in not bAll mode, given that many of the previous differentiated stuff have been moved into chatProps and inturn shown for now --- tools/server/public_simplechat/readme.md | 26 ++++++++++---------- tools/server/public_simplechat/simplechat.js | 10 ++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index e9fc991cf3..bf2dc98b6c 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -208,6 +208,19 @@ It is attached to the document object. Some of these can also be updated using t bCompletionInsertStandardRolePrefix - whether Completion mode inserts role related prefix wrt the messages that get inserted into prompt field wrt /Completion endpoint. + bTrimGarbage - whether garbage repeatation at the end of the generated ai response, should be + trimmed or left as is. If enabled, it will be trimmed so that it wont be sent back as part of + subsequent chat history. At the same time the actual trimmed text is shown to the user, once + when it was generated, so user can check if any useful info/data was there in the response. + + One may be able to request the ai-model to continue (wrt the last response) (if chat-history + is enabled as part of the chat-history-in-context setting), and chances are the ai-model will + continue starting from the trimmed part, thus allows long response to be recovered/continued + indirectly, in many cases. + + The histogram/freq based trimming logic is currently tuned for english language wrt its + is-it-a-alpabetic|numeral-char regex match logic. + tools - contains controls related to tool calling enabled - control whether tool calling is enabled or not @@ -227,19 +240,6 @@ It is attached to the document object. Some of these can also be updated using t apiEP - select between /completions and /chat/completions endpoint provided by the server/ai-model. - bTrimGarbage - whether garbage repeatation at the end of the generated ai response, should be - trimmed or left as is. If enabled, it will be trimmed so that it wont be sent back as part of - subsequent chat history. At the same time the actual trimmed text is shown to the user, once - when it was generated, so user can check if any useful info/data was there in the response. - - One may be able to request the ai-model to continue (wrt the last response) (if chat-history - is enabled as part of the chat-history-in-context setting), and chances are the ai-model will - continue starting from the trimmed part, thus allows long response to be recovered/continued - indirectly, in many cases. - - The histogram/freq based trimming logic is currently tuned for english language wrt its - is-it-a-alpabetic|numeral-char regex match logic. - apiRequestOptions - maintains the list of options/fields to send along with api request, irrespective of whether /chat/completions or /completions endpoint. diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index e26750fdd9..557c5f8d00 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -636,7 +636,7 @@ class SimpleChat { } else { theResp = await this.handle_response_oneshot(resp, apiEP); } - if (gMe.bTrimGarbage) { + if (gMe.chatProps.bTrimGarbage) { let origMsg = theResp.ns.content; theResp.ns.content = du.trim_garbage_at_end(origMsg); theResp.trimmedContent = origMsg.substring(theResp.ns.content.length); @@ -1024,8 +1024,8 @@ class Me { iRecentUserMsgCnt: 10, bCompletionFreshChatAlways: true, bCompletionInsertStandardRolePrefix: false, + bTrimGarbage: true, }; - this.bTrimGarbage = true; /** @type {Object} */ this.sRecentUserMsgCnt = { "Full": -1, @@ -1094,9 +1094,9 @@ class Me { * @param {boolean} bAll */ show_info(elDiv, bAll=false) { - let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage"]; + let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "apiEP", "chatProps"]; if (!bAll) { - props = [ "baseURL", "modelInfo", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps" ]; + props = [ "baseURL", "modelInfo", "tools", "apiEP", "chatProps" ]; } fetch(`${this.baseURL}/props`).then(resp=>resp.json()).then(json=>{ this.modelInfo = { @@ -1112,7 +1112,7 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps", "bTrimGarbage"], "Settings", (prop, elProp)=>{ + ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps"], "Settings", (prop, elProp)=>{ if (prop == "headers:Authorization") { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; From aa8c8040cf3f2f60d765591c7b61873f4e070c4f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 22 Oct 2025 08:54:15 +0530 Subject: [PATCH 120/446] SimpleChatTC:Cleanup:ChatProps: apiEP --- tools/server/public_simplechat/readme.md | 4 ++-- tools/server/public_simplechat/simplechat.js | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index bf2dc98b6c..779a48adaf 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -179,6 +179,8 @@ It is attached to the document object. Some of these can also be updated using t chatProps - maintain a set of properties which manipulate chatting with ai engine + apiEP - select between /completions and /chat/completions endpoint provided by the server/ai-model. + stream - control between oneshot-at-end and live-stream-as-its-generated collating and showing of the generated response. @@ -238,8 +240,6 @@ It is attached to the document object. Some of these can also be updated using t recommended to set iRecentUserMsgCnt to 10 or more, so that enough context is retained during chatting with ai models with tool support. - apiEP - select between /completions and /chat/completions endpoint provided by the server/ai-model. - apiRequestOptions - maintains the list of options/fields to send along with api request, irrespective of whether /chat/completions or /completions endpoint. diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 557c5f8d00..b58b1f196d 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -774,7 +774,7 @@ class MultiChatUI { if (this.elInUser.disabled) { return; } - this.handle_user_submit(this.curChatId, gMe.apiEP).catch((/** @type{Error} */reason)=>{ + this.handle_user_submit(this.curChatId, gMe.chatProps.apiEP).catch((/** @type{Error} */reason)=>{ let msg = `ERRR:SimpleChat\nMCUI:HandleUserSubmit:${this.curChatId}\n${reason.name}:${reason.message}`; console.error(msg.replace("\n", ":")); alert(msg); @@ -1020,6 +1020,7 @@ class Me { fetchProxyUrl: "http://127.0.0.1:3128" }; this.chatProps = { + apiEP: ApiEP.Type.Chat, stream: true, iRecentUserMsgCnt: 10, bCompletionFreshChatAlways: true, @@ -1035,7 +1036,6 @@ class Me { "Last4": 5, "Last9": 10, }; - this.apiEP = ApiEP.Type.Chat; /** @type {Object} */ this.headers = { "Content-Type": "application/json", @@ -1094,9 +1094,9 @@ class Me { * @param {boolean} bAll */ show_info(elDiv, bAll=false) { - let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "apiEP", "chatProps"]; + let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "chatProps"]; if (!bAll) { - props = [ "baseURL", "modelInfo", "tools", "apiEP", "chatProps" ]; + props = [ "baseURL", "modelInfo", "tools", "chatProps" ]; } fetch(`${this.baseURL}/props`).then(resp=>resp.json()).then(json=>{ this.modelInfo = { @@ -1112,16 +1112,16 @@ class Me { * @param {HTMLDivElement} elDiv */ show_settings(elDiv) { - ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "apiEP", "chatProps"], "Settings", (prop, elProp)=>{ + ui.ui_show_obj_props_edit(elDiv, "", this, ["baseURL", "headers", "tools", "apiRequestOptions", "chatProps"], "Settings", (prop, elProp)=>{ if (prop == "headers:Authorization") { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; } - }, [":apiEP", ":chatProps:iRecentUserMsgCnt"], (propWithPath, prop, elParent)=>{ - if (propWithPath == ":apiEP") { - let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{ + }, [":chatProps:apiEP", ":chatProps:iRecentUserMsgCnt"], (propWithPath, prop, elParent)=>{ + if (propWithPath == ":chatProps:apiEP") { + let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.chatProps.apiEP, (val)=>{ // @ts-ignore - this.apiEP = ApiEP.Type[val]; + this.chatProps.apiEP = ApiEP.Type[val]; }); elParent.appendChild(sel.div); } From aac5213104f933eaae6dd0923297a886c1c4d597 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 22 Oct 2025 09:43:34 +0530 Subject: [PATCH 121/446] SimpleChatTC:Tools: Show available tool names Dont allow tool names to be changed in settings page --- tools/server/public_simplechat/simplechat.js | 8 ++++++-- tools/server/public_simplechat/tools.mjs | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index b58b1f196d..e58f6daddc 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1017,7 +1017,8 @@ class Me { this.multiChat = new MultiChatUI(); this.tools = { enabled: false, - fetchProxyUrl: "http://127.0.0.1:3128" + fetchProxyUrl: "http://127.0.0.1:3128", + toolNames: /** @type {Array} */([]) }; this.chatProps = { apiEP: ApiEP.Type.Chat, @@ -1117,6 +1118,9 @@ class Me { // @ts-ignore elProp.placeholder = "Bearer OPENAI_API_KEY"; } + if (prop.startsWith("tools:toolName")) { + /** @type {HTMLInputElement} */(elProp).disabled = true + } }, [":chatProps:apiEP", ":chatProps:iRecentUserMsgCnt"], (propWithPath, prop, elParent)=>{ if (propWithPath == ":chatProps:apiEP") { let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.chatProps.apiEP, (val)=>{ @@ -1150,7 +1154,7 @@ function startme() { document["du"] = du; // @ts-ignore document["tools"] = tools; - tools.init() + tools.init().then((toolNames)=>gMe.tools.toolNames=toolNames) for (let cid of gMe.defaultChatIds) { gMe.multiChat.new_chat_session(cid); } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 14249b517a..2b4237258e 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -14,11 +14,14 @@ let gToolsWorker = new Worker('./toolsworker.mjs', { type: 'module' }); */ export let tc_switch = {} -export function init() { - tjs.init(gToolsWorker).then(()=>{ +export async function init() { + return tjs.init(gToolsWorker).then(()=>{ + let toolNames = [] for (const key in tjs.tc_switch) { tc_switch[key] = tjs.tc_switch[key] + toolNames.push(key) } + return toolNames }) } From 62dcd506e3374d3710e99cd16fae44c448973925 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 23 Oct 2025 17:33:54 +0530 Subject: [PATCH 122/446] SimpleChatTC:SimpleProxy:Allow for loading json based config file The config entries should be named same as their equivalent cmdline argument entries but without the -- prefix --- .../local.tools/simpleproxy.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 8185382297..d15be10faf 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -17,6 +17,7 @@ import html.parser gMe = { '--port': 3128, + '--config': '/dev/null', 'server': None } @@ -196,6 +197,25 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") +def load_config(): + """ + Allow loading of a json based config file + + The config entries should be named same as their equivalent cmdline argument + entries but without the -- prefix. They will be loaded into gMe after adding + -- prefix. + + As far as the program is concerned the entries could either come from cmdline + or from a json based config file. + """ + global gMe + import json + with open(gMe['--config']) as f: + cfg = json.load(f) + for k in cfg: + gMe[f"--{k}"] = cfg[k] + + def process_args(args: list[str]): global gMe gMe['INTERNAL.ProcessArgs.Malformed'] = [] @@ -213,10 +233,16 @@ def process_args(args: list[str]): iArg += 1 gMe[cArg] = int(args[iArg]) iArg += 1 + case '--config': + iArg += 1 + gMe[cArg] = args[iArg] + iArg += 1 + load_config() case _: gMe['INTERNAL.ProcessArgs.Unknown'].append(cArg) print(f"WARN:ProcessArgs:{iArg}:IgnoringUnknownCommand:{cArg}") iArg += 1 + print(gMe) def run(): From 58954c8814d051be5f6895e978c9d7f6694bc05a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 23 Oct 2025 18:02:53 +0530 Subject: [PATCH 123/446] SimpleChatTC:SimpleProxy: Update doc following python convention --- .../local.tools/simpleproxy.py | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index d15be10faf..2787a4420b 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -2,8 +2,16 @@ # by Humans for All # # Listens on the specified port (defaults to squids 3128) -# * if a url query is got (http://localhost:3128/?url=http://site.of.interest/path/of/interest) +# * if a url query is got wrt urlraw path +# http://localhost:3128/urlraw?url=http://site.of.interest/path/of/interest # fetches the contents of the specified url and returns the same to the requester +# * if a url query is got wrt urltext path +# http://localhost:3128/urltext?url=http://site.of.interest/path/of/interest +# fetches the contents of the specified url and returns the same to the requester +# after removing html tags in general as well as contents of tags like style +# script, header, footer, nav ... +# * any request to aum path is used to respond with a predefined text response +# which can help identify this server, in a simple way. # @@ -23,23 +31,32 @@ gMe = { class ProxyHandler(http.server.BaseHTTPRequestHandler): + """ + Implements the logic for handling requests sent to this server. + """ - # Common headers to include in responses from this server def send_headers_common(self): + """ + Common headers to include in responses from this server + """ self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') self.send_header('Access-Control-Allow-Headers', '*') self.end_headers() - # overrides the SendError helper - # so that the common headers mentioned above can get added to them - # else CORS failure will be triggered by the browser on fetch from browser. def send_error(self, code: int, message: str | None = None, explain: str | None = None) -> None: + """ + Overrides the SendError helper + so that the common headers mentioned above can get added to them + else CORS failure will be triggered by the browser on fetch from browser. + """ self.send_response(code, message) self.send_headers_common() - # Handle GET requests def do_GET(self): + """ + Handle GET requests + """ print(f"DBUG:ProxyHandler:GET:{self.path}") pr = urllib.parse.urlparse(self.path) print(f"DBUG:ProxyHandler:GET:{pr}") @@ -54,14 +71,20 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): print(f"WARN:ProxyHandler:GET:UnknownPath{pr.path}") self.send_error(400, f"WARN:UnknownPath:{pr.path}") - # Handle OPTIONS for CORS preflights (just in case from browser) def do_OPTIONS(self): + """ + Handle OPTIONS for CORS preflights (just in case from browser) + """ print(f"DBUG:ProxyHandler:OPTIONS:{self.path}") self.send_response(200) self.send_headers_common() def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): + """ + Handle requests to aum path, which is used in a simple way to + verify that one is communicating with this proxy server + """ ph.send_response_only(200, "bharatavarshe") ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() @@ -69,6 +92,9 @@ def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): @dataclass(frozen=True) class UrlReqResp: + """ + Used to return result wrt urlreq helper below. + """ callOk: bool httpStatus: int httpStatusMsg: str = "" @@ -77,6 +103,9 @@ class UrlReqResp: def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): + """ + Common part of the url request handling used by both urlraw and urltext. + """ print(f"DBUG:{tag}:{pr}") queryParams = urllib.parse.parse_qs(pr.query) url = queryParams['url'] @@ -114,6 +143,17 @@ def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): class TextHtmlParser(html.parser.HTMLParser): + """ + A simple minded logic used to strip html content of + * all the html tags as well as + * all the contents belonging to below predefined tags like script, style, header, ... + + NOTE: if the html content/page uses any javascript for client side manipulation/generation of + html content, that logic wont be triggered, so also such client side dynamic content wont be + got. + + This helps return a relatively clean textual representation of the html file/content being parsed. + """ def __init__(self): super().__init__() @@ -131,6 +171,9 @@ class TextHtmlParser(html.parser.HTMLParser): self.textStripped = "" def do_capture(self): + """ + Helps decide whether to capture contents or discard them. + """ if self.inside['body'] and not (self.inside['script'] or self.inside['style'] or self.inside['header'] or self.inside['footer'] or self.inside['nav']): return True return False @@ -217,6 +260,9 @@ def load_config(): def process_args(args: list[str]): + """ + Helper to process command line arguments + """ global gMe gMe['INTERNAL.ProcessArgs.Malformed'] = [] gMe['INTERNAL.ProcessArgs.Unknown'] = [] From 71ad609db65f43b1d01ea6d052b7bbfa734bf694 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 23 Oct 2025 18:30:42 +0530 Subject: [PATCH 124/446] SimpleChatTC:SimpleProxy: AllowedDomains based filtering Allow fetching from only specified allowed.domains --- .../local.tools/simpleproxy.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 2787a4420b..85ab012827 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -21,6 +21,7 @@ import urllib.parse import urllib.request from dataclasses import dataclass import html.parser +import re gMe = { @@ -102,6 +103,23 @@ class UrlReqResp: contentData: str = "" +def validate_url(url: str, tag: str): + """ + Implement a re based filter logic on the specified url. + """ + urlParts = urllib.parse.urlparse(url) + urlHName = urlParts.hostname + if not urlHName: + return UrlReqResp(False, 400, f"WARN:{tag}:Missing hostname in Url") + bMatched = False + for filter in gMe['--allowed.domains']: + if re.match(filter, urlHName): + bMatched = True + if not bMatched: + return UrlReqResp(False, 400, f"WARN:{tag}:requested hostname not allowed") + return UrlReqResp(True, 200) + + def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): """ Common part of the url request handling used by both urlraw and urltext. @@ -113,6 +131,11 @@ def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): url = url[0] if (not url) or (len(url) == 0): return UrlReqResp(False, 400, f"WARN:{tag}:MissingUrl") + if (not gMe['--allowed.domains']): + return UrlReqResp(False, 400, f"DBUG:{tag}:MissingAllowedDomains") + gotVU = validate_url(url, tag) + if not gotVU.callOk: + return gotVU try: # Get requested url with urllib.request.urlopen(url, timeout=10) as response: @@ -260,6 +283,7 @@ def load_config(): def process_args(args: list[str]): + import ast """ Helper to process command line arguments """ @@ -284,6 +308,10 @@ def process_args(args: list[str]): gMe[cArg] = args[iArg] iArg += 1 load_config() + case '--allowed.domains': + iArg += 1 + gMe[cArg] = ast.literal_eval(args[iArg]) + iArg += 1 case _: gMe['INTERNAL.ProcessArgs.Unknown'].append(cArg) print(f"WARN:ProcessArgs:{iArg}:IgnoringUnknownCommand:{cArg}") From 370326b1ecc93ed8175cd055eba9a4ac32e0e27f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 23 Oct 2025 19:01:50 +0530 Subject: [PATCH 125/446] SimpleChatTC:SimpleProxy: Cleanup domain filtering and general Had confused between js and python wrt accessing dictionary contents and its consequence on non existent key. Fixed it. Use different error ids to distinguish between failure in common urlreq and the specific urltext and urlraw helpers. --- tools/server/public_simplechat/local.tools/simpleproxy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 85ab012827..9d1a328702 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -131,7 +131,7 @@ def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): url = url[0] if (not url) or (len(url) == 0): return UrlReqResp(False, 400, f"WARN:{tag}:MissingUrl") - if (not gMe['--allowed.domains']): + if (not gMe.get('--allowed.domains')): return UrlReqResp(False, 400, f"DBUG:{tag}:MissingAllowedDomains") gotVU = validate_url(url, tag) if not gotVU.callOk: @@ -144,7 +144,7 @@ def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): contentType = response.getheader('Content-Type') or 'text/html' return UrlReqResp(True, statusCode, "", contentType, contentData) except Exception as exc: - return UrlReqResp(False, 502, f"WARN:UrlFetchFailed:{exc}") + return UrlReqResp(False, 502, f"WARN:UrlReqFailed:{exc}") def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): @@ -162,7 +162,7 @@ def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.end_headers() ph.wfile.write(got.contentData.encode('utf-8')) except Exception as exc: - ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") + ph.send_error(502, f"WARN:UrlRawFailed:{exc}") class TextHtmlParser(html.parser.HTMLParser): @@ -260,7 +260,7 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.end_headers() ph.wfile.write(textHtml.get_stripped_text().encode('utf-8')) except Exception as exc: - ph.send_error(502, f"WARN:UrlFetchFailed:{exc}") + ph.send_error(502, f"WARN:UrlTextFailed:{exc}") def load_config(): From 840cab0b1c6ea8f234a7d8019f3ca1c004228a40 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 23 Oct 2025 19:30:48 +0530 Subject: [PATCH 126/446] SimpleChatTC:SimpleProxy: Include a sample config file with allowed domains set to few sites in general to show its use this includes some sites which allow search to be carried out through them as well as provide news aggregation --- .../public_simplechat/local.tools/simpleproxy.json | 12 ++++++++++++ .../public_simplechat/local.tools/simpleproxy.py | 1 + 2 files changed, 13 insertions(+) create mode 100644 tools/server/public_simplechat/local.tools/simpleproxy.json diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json new file mode 100644 index 0000000000..396567652b --- /dev/null +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -0,0 +1,12 @@ +{ + "allowed.domains": [ + "^www\\.bing\\.com$", + ".*\\.yahoo\\.com$", + "^search\\.yahoo\\.com$", + ".*\\.brave\\.com$", + "^search\\.brave\\.com$", + ".*\\.duckduckgo\\.com$", + ".*\\.google\\.com$", + "^google\\.com$" + ] +} diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 9d1a328702..d0036dbff0 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -108,6 +108,7 @@ def validate_url(url: str, tag: str): Implement a re based filter logic on the specified url. """ urlParts = urllib.parse.urlparse(url) + print(f"DBUG:ValidateUrl:{urlParts}, {urlParts.hostname}") urlHName = urlParts.hostname if not urlHName: return UrlReqResp(False, 400, f"WARN:{tag}:Missing hostname in Url") From 17365ed4b941f264f1d4c8fee8e959226dd99894 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 23 Oct 2025 20:57:22 +0530 Subject: [PATCH 127/446] SimpleChatTC: Update readme a bit --- tools/server/public_simplechat/readme.md | 39 +++++++++++++++--------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 779a48adaf..702b833799 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -42,8 +42,9 @@ any adaptive culling of old messages nor of replacing them with summary of their is a optional sliding window based chat logic, which provides a simple minded culling of old messages from the chat history before sending to the ai model. -NOTE: Wrt options sent with the request, it mainly sets temperature, max_tokens and optionaly stream for now. -However if someone wants they can update the js file or equivalent member in gMe as needed. +NOTE: Wrt options sent with the request, it mainly sets temperature, max_tokens and optionaly stream as well +as tool_calls mainly for now. However if someone wants they can update the js file or equivalent member in +gMe as needed. NOTE: One may be able to use this to chat with openai api web-service /chat/completions endpoint, in a very limited / minimal way. One will need to set model, openai url and authorization bearer key in settings ui. @@ -55,7 +56,7 @@ One could run this web frontend directly using server itself or if anyone is thi frontend to configure the server over http(s) or so, then run this web frontend using something like python's http module. -### running using tools/server +### running directly using tools/server ./llama-server -m path/model.gguf --path tools/server/public_simplechat [--port PORT] @@ -78,10 +79,15 @@ remember to * use a GenAi/LLM model which supports tool calling. -* if fetch web url / page tool call is needed, remember to run the bundled local.tools/simpleproxy.py helper +* if fetch web url / page tool call is needed remember to run the bundled local.tools/simpleproxy.py + helper along with its config file - * remember that this is a relatively dumb proxy logic along with optional stripping of scripts/styles/headers/footers/..., - Be careful if trying to fetch web pages, and use it only with known safe sites. + * cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config simpleproxy.json + + * remember that this is a relatively dumb proxy logic along with optional stripping of scripts / styles + / headers / footers /..., Be careful if trying to fetch web pages, and use it only with known safe sites. + + * it allows one to specify a white list of allowed.domains, look into local.tools/simpleproxy.json ### using the front end @@ -312,7 +318,9 @@ wrt repeatations in general in the generated text response. A end-user can change these behaviour by editing gMe from browser's devel-tool/console or by using the provided settings ui (for settings exposed through the ui). The logic uses a generic helper which autocreates property edit ui elements for the specified set of properties. If the -new property is a number or text or boolean, the autocreate logic will handle it. +new property is a number or text or boolean or a object with properties within it, autocreate +logic will try handle it automatically. A developer can trap this autocreation flow and change +things if needed. ### OpenAi / Equivalent API WebService @@ -362,11 +370,11 @@ server logic, this helps bypass the CORS restrictions applied if trying to direc browser js runtime environment. Depending on the path specified wrt the proxy server, if urltext (and not urlraw), it additionally tries to convert html content into equivalent text to some extent in a simple minded manner by dropping head block as well as all scripts/styles/footers/headers/nav. -May add support for white list of allowed sites to access or so. * the logic does a simple check to see if the bundled simpleproxy is running at specified fetchProxyUrl before enabling fetch web related tool calls. * The bundled simple proxy can be found at * tools/server/public_simplechat/local.tools/simpleproxy.py +* it provides for a basic white list of allowed domains to access or so #### Extending with new tools @@ -374,10 +382,10 @@ May add support for white list of allowed sites to access or so. Provide a descriptive meta data explaining the tool / function being provided for tool calling, as well as its arguments. -Provide a handler which should implement the specified tool / function call or rather constructs -the code to be run to get the tool / function call job done, and inturn pass the same to the -provided web worker to get it executed. Remember to use console.log while generating any response -that should be sent back to the ai model, in your constructed code. +Provide a handler which should implement the specified tool / function call or rather for many +cases constructs the code to be run to get the tool / function call job done, and inturn pass +the same to the provided web worker to get it executed. Remember to use console.log while +generating any response that should be sent back to the ai model, in your constructed code. Update the tc_switch to include a object entry for the tool, which inturn includes * the meta data as well as @@ -415,14 +423,17 @@ so that any scheduled asynchronous code or related async error handling using pr gets executed, before tool calling returns and thus data / error generated by those async code also get incorporated in result sent to ai engine on the server side. -#### ToDo -Is the promise land trap deep enough, need to think through and explore around this once later. +### ToDo + +Is the tool call promise land trap deep enough, need to think through and explore around this once later. Trap error responses. Handle reasoning/thinking responses from ai models. +Handle multimodal handshaking with ai models. + ### Debuging the handshake From e6e0adbe90a4210c8b6ae5e1e99174577a839a29 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 23 Oct 2025 22:39:05 +0530 Subject: [PATCH 128/446] SimpleChatTC:SimpleProxy: Some debug prints which give info --- tools/server/public_simplechat/local.tools/simpleproxy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index d0036dbff0..3108678037 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -51,6 +51,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): so that the common headers mentioned above can get added to them else CORS failure will be triggered by the browser on fetch from browser. """ + print(f"WARN:PH:SendError:{code}:{message}") self.send_response(code, message) self.send_headers_common() @@ -58,7 +59,8 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): """ Handle GET requests """ - print(f"DBUG:ProxyHandler:GET:{self.path}") + print(f"\n\n\nDBUG:ProxyHandler:GET:{self.address_string()}:{self.path}") + print(f"DBUG:PH:Get:Headers:{self.headers}") pr = urllib.parse.urlparse(self.path) print(f"DBUG:ProxyHandler:GET:{pr}") match pr.path: From d0b9103176997ee26f96edf3546a12907577c62b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 23 Oct 2025 22:49:07 +0530 Subject: [PATCH 129/446] SimpleChatTC:SimpleProxy:Try mimic real client using got req info ie include User-Agent, Accept-Language and Accept in the generated request using equivalent values got in the request being proxied. --- .../local.tools/simpleproxy.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 3108678037..598d77b2f4 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -123,9 +123,18 @@ def validate_url(url: str, tag: str): return UrlReqResp(True, 200) -def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): +def handle_urlreq(ph: ProxyHandler, pr: urllib.parse.ParseResult, tag: str): """ Common part of the url request handling used by both urlraw and urltext. + + Verify the url being requested is allowed. + + Include User-Agent, Accept-Language and Accept in the generated request using + equivalent values got in the request being proxied, so as to try mimic the + real client, whose request we are proxying. In case a header is missing in the + got request, fallback to using some possibly ok enough defaults. + + Fetch the requested url. """ print(f"DBUG:{tag}:{pr}") queryParams = urllib.parse.parse_qs(pr.query) @@ -140,8 +149,17 @@ def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): if not gotVU.callOk: return gotVU try: + hUA = ph.headers.get('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0') + hAL = ph.headers.get('Accept-Language', "en-US,en") + hA = ph.headers.get('Accept', "text/html,*/*") + headers = { + 'User-Agent': hUA, + 'Accept': hA, + 'Accept-Language': hAL + } + req = urllib.request.Request(url, headers=headers) # Get requested url - with urllib.request.urlopen(url, timeout=10) as response: + with urllib.request.urlopen(req, timeout=10) as response: contentData = response.read().decode('utf-8') statusCode = response.status or 200 contentType = response.getheader('Content-Type') or 'text/html' @@ -153,7 +171,7 @@ def handle_urlreq(pr: urllib.parse.ParseResult, tag: str): def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): try: # Get requested url - got = handle_urlreq(pr, "HandleUrlRaw") + got = handle_urlreq(ph, pr, "HandleUrlRaw") if not got.callOk: ph.send_error(got.httpStatus, got.httpStatusMsg) return @@ -248,7 +266,7 @@ class TextHtmlParser(html.parser.HTMLParser): def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): try: # Get requested url - got = handle_urlreq(pr, "HandleUrlText") + got = handle_urlreq(ph, pr, "HandleUrlText") if not got.callOk: ph.send_error(got.httpStatus, got.httpStatusMsg) return From bebf846157a4325cc31f38a1ddbfbaa4b36e785a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 23 Oct 2025 23:18:06 +0530 Subject: [PATCH 130/446] SimpleChatTC:SimpleProxy:Cleanup a bit The tagging of messages wrt ValidateUrl and UrlReq Also dump req Move check for --allowed.domains to ValidateUrl NOTE: Also with mimicing of user agent etal from got request to the generated request, yahoo search/news is returning results now, instead of the bland error before. --- .../server/public_simplechat/local.tools/simpleproxy.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 598d77b2f4..6846caf5ee 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -109,6 +109,9 @@ def validate_url(url: str, tag: str): """ Implement a re based filter logic on the specified url. """ + tag=f"VU:{tag}" + if (not gMe.get('--allowed.domains')): + return UrlReqResp(False, 400, f"DBUG:{tag}:MissingAllowedDomains") urlParts = urllib.parse.urlparse(url) print(f"DBUG:ValidateUrl:{urlParts}, {urlParts.hostname}") urlHName = urlParts.hostname @@ -136,6 +139,7 @@ def handle_urlreq(ph: ProxyHandler, pr: urllib.parse.ParseResult, tag: str): Fetch the requested url. """ + tag=f"UrlReq:{tag}" print(f"DBUG:{tag}:{pr}") queryParams = urllib.parse.parse_qs(pr.query) url = queryParams['url'] @@ -143,8 +147,6 @@ def handle_urlreq(ph: ProxyHandler, pr: urllib.parse.ParseResult, tag: str): url = url[0] if (not url) or (len(url) == 0): return UrlReqResp(False, 400, f"WARN:{tag}:MissingUrl") - if (not gMe.get('--allowed.domains')): - return UrlReqResp(False, 400, f"DBUG:{tag}:MissingAllowedDomains") gotVU = validate_url(url, tag) if not gotVU.callOk: return gotVU @@ -159,13 +161,14 @@ def handle_urlreq(ph: ProxyHandler, pr: urllib.parse.ParseResult, tag: str): } req = urllib.request.Request(url, headers=headers) # Get requested url + print(f"DBUG:{tag}:Req:{req.full_url}:{req.headers}") with urllib.request.urlopen(req, timeout=10) as response: contentData = response.read().decode('utf-8') statusCode = response.status or 200 contentType = response.getheader('Content-Type') or 'text/html' return UrlReqResp(True, statusCode, "", contentType, contentData) except Exception as exc: - return UrlReqResp(False, 502, f"WARN:UrlReqFailed:{exc}") + return UrlReqResp(False, 502, f"WARN:{tag}:Failed:{exc}") def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): From c109da870f6bb6826a3eefbe77ad6ba18eae17c5 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 24 Oct 2025 00:36:54 +0530 Subject: [PATCH 131/446] SimpleChatTC:SimpleProxy: mimicing got req helps wrt duckduckgo mimicing got req in generated req helps with duckduckgo also and not just yahoo. also update allowed.domains to allow a url generated by ai when trying to access the bing's news aggregation url --- tools/server/public_simplechat/local.tools/simpleproxy.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index 396567652b..a0af1169d0 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -1,11 +1,13 @@ { "allowed.domains": [ + ".*\\.bing\\.com$", "^www\\.bing\\.com$", ".*\\.yahoo\\.com$", "^search\\.yahoo\\.com$", ".*\\.brave\\.com$", "^search\\.brave\\.com$", ".*\\.duckduckgo\\.com$", + "^duckduckgo\\.com$", ".*\\.google\\.com$", "^google\\.com$" ] From 74226a0992d7685e69a0647ccb323acb06744b68 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 24 Oct 2025 01:11:22 +0530 Subject: [PATCH 132/446] SimpleChatTC:ToolCall response relaxed handling Use DOMParser parseFromString in text/html mode rather than text/xml as it makes it more relaxed without worrying about special chars of xml like & etal --- tools/server/public_simplechat/simplechat.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index e58f6daddc..cdaf47bcd1 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -107,12 +107,18 @@ class ChatMessageEx { /** * Extract the elements of the all in one tool call result string - * This should potentially account for content tag having xml content within to an extent. + * This should potentially account for content tag having xml/html content within to an extent. + * + * NOTE: Rather text/html is a more relaxed/tolarent mode for parseFromString than text/xml. + * NOTE: Maybe better to switch to a json string format or use a more intelligent xml encoder + * in createToolCallResultAllInOne so that extractor like this dont have to worry about special + * xml chars like & as is, in the AllInOne content. For now text/html tolarence seems ok enough. + * * @param {string} allInOne */ static extractToolCallResultAllInOne(allInOne) { const dParser = new DOMParser(); - const got = dParser.parseFromString(allInOne, 'text/xml'); + const got = dParser.parseFromString(allInOne, 'text/html'); const parseErrors = got.querySelector('parseerror') if (parseErrors) { console.debug("WARN:ChatMessageEx:ExtractToolCallResultAllInOne:", parseErrors.textContent.trim()) From 90d232dc4a76fd47106f5a38862c11cfcf25f825 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 24 Oct 2025 01:31:30 +0530 Subject: [PATCH 133/446] SimpleChatTC:SimpleProxy: Update readme wrt mimicing client req ie during proxying --- tools/server/public_simplechat/readme.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 702b833799..83781a31ea 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -374,7 +374,11 @@ in a simple minded manner by dropping head block as well as all scripts/styles/f before enabling fetch web related tool calls. * The bundled simple proxy can be found at * tools/server/public_simplechat/local.tools/simpleproxy.py -* it provides for a basic white list of allowed domains to access or so + * it provides for a basic white list of allowed domains to access, to an extent + * it tries to mimic the client/browser making the request to it by propogating header entries like + user-agent, accept and accept-language from the got request to the generated request during proxying + so that websites will hopefully respect the request rather than blindly rejecting it as coming from + a non-browser entity. #### Extending with new tools From dbb24fec77d10e99ff270e67ae384083f6b19c78 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 24 Oct 2025 03:00:09 +0530 Subject: [PATCH 134/446] SimpleChatTC:ToolResponse: Use browser dom for xml/html safe Instead of simple concatenating of tool call id, name and result now use browser's dom logic to create the xml structure used for now to store these within content field. This should take care of transforming / escaping any xml special chars in the result, so that extracting them later for putting into different fields in the server handshake doesnt have any problem. --- tools/server/public_simplechat/simplechat.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index cdaf47bcd1..6f4d55940c 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -79,12 +79,21 @@ class ChatMessageEx { /** * Create a all in one tool call result string + * Use browser's dom logic to handle strings in a xml/html safe way by escaping things where needed, + * so that extracting the same later doesnt create any problems. * @param {string} toolCallId * @param {string} toolName * @param {string} toolResult */ static createToolCallResultAllInOne(toolCallId, toolName, toolResult) { - return ` ${toolCallId} ${toolName} ${toolResult} `; + let dp = new DOMParser() + let doc = dp.parseFromString("", "text/xml") + for (const k of [["id", toolCallId], ["name", toolName], ["content", toolResult]]) { + let el = doc.createElement(k[0]) + el.appendChild(doc.createTextNode(k[1])) + doc.documentElement.appendChild(el) + } + return new XMLSerializer().serializeToString(doc); } /** From 4c1c36350433faf2d7cf202f7d3e5a528bd02390 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 24 Oct 2025 03:40:55 +0530 Subject: [PATCH 135/446] SimpleChatTC:SimpleProxy: debug dumps to identify funny bing bing raised a challenge for chrome triggered search requests after few requests, which were spread few minutes apart, while still seemingly allowing wget based search to continue (again spread few minutes apart). Added a simple helper to trace this, use --debug True to enable same. --- .../local.tools/simpleproxy.json | 1 + .../local.tools/simpleproxy.py | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index a0af1169d0..30f14b1d2c 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -6,6 +6,7 @@ "^search\\.yahoo\\.com$", ".*\\.brave\\.com$", "^search\\.brave\\.com$", + "^brave\\.com$", ".*\\.duckduckgo\\.com$", "^duckduckgo\\.com$", ".*\\.google\\.com$", diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 6846caf5ee..76ea363ca3 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -22,11 +22,13 @@ import urllib.request from dataclasses import dataclass import html.parser import re +import time gMe = { '--port': 3128, '--config': '/dev/null', + '--debug': False, 'server': None } @@ -105,6 +107,18 @@ class UrlReqResp: contentData: str = "" +def debug_dump(meta: dict, data: dict): + if not gMe['--debug']: + return + timeTag = f"{time.time():0.12f}" + with open(f"/tmp/simpleproxy.{timeTag}.meta", '+w') as f: + for k in meta: + f.write(f"\n\n\n\n{k}:{meta[k]}\n\n\n\n") + with open(f"/tmp/simpleproxy.{timeTag}.data", '+w') as f: + for k in data: + f.write(f"\n\n\n\n{k}:{data[k]}\n\n\n\n") + + def validate_url(url: str, tag: str): """ Implement a re based filter logic on the specified url. @@ -152,7 +166,7 @@ def handle_urlreq(ph: ProxyHandler, pr: urllib.parse.ParseResult, tag: str): return gotVU try: hUA = ph.headers.get('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0') - hAL = ph.headers.get('Accept-Language', "en-US,en") + hAL = ph.headers.get('Accept-Language', "en-US,en;q=0.9") hA = ph.headers.get('Accept', "text/html,*/*") headers = { 'User-Agent': hUA, @@ -166,6 +180,7 @@ def handle_urlreq(ph: ProxyHandler, pr: urllib.parse.ParseResult, tag: str): contentData = response.read().decode('utf-8') statusCode = response.status or 200 contentType = response.getheader('Content-Type') or 'text/html' + debug_dump({ 'url': req.full_url, 'headers': req.headers, 'ctype': contentType }, { 'cdata': contentData }) return UrlReqResp(True, statusCode, "", contentType, contentData) except Exception as exc: return UrlReqResp(False, 502, f"WARN:{tag}:Failed:{exc}") @@ -283,6 +298,7 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() ph.wfile.write(textHtml.get_stripped_text().encode('utf-8')) + debug_dump({ 'RawText': 'yes', 'StrippedText': 'yes' }, { 'RawText': textHtml.text, 'StrippedText': textHtml.get_stripped_text() }) except Exception as exc: ph.send_error(502, f"WARN:UrlTextFailed:{exc}") @@ -336,6 +352,10 @@ def process_args(args: list[str]): iArg += 1 gMe[cArg] = ast.literal_eval(args[iArg]) iArg += 1 + case '--debug': + iArg += 1 + gMe[cArg] = ast.literal_eval(args[iArg]) + iArg += 1 case _: gMe['INTERNAL.ProcessArgs.Unknown'].append(cArg) print(f"WARN:ProcessArgs:{iArg}:IgnoringUnknownCommand:{cArg}") From 9e97880dde462c94f7e61b537bd6fdebdee9e7bc Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 24 Oct 2025 04:18:36 +0530 Subject: [PATCH 136/446] SimpleChatTC:SimpleProxy:Cleanup avoid logically duplicate debug log --- tools/server/public_simplechat/local.tools/simpleproxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 76ea363ca3..86f5aa0e7b 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -154,7 +154,6 @@ def handle_urlreq(ph: ProxyHandler, pr: urllib.parse.ParseResult, tag: str): Fetch the requested url. """ tag=f"UrlReq:{tag}" - print(f"DBUG:{tag}:{pr}") queryParams = urllib.parse.parse_qs(pr.query) url = queryParams['url'] print(f"DBUG:{tag}:Url:{url}") From 45f9db9963e3993ed0b24ab8896177bddae3ad97 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 24 Oct 2025 05:05:24 +0530 Subject: [PATCH 137/446] SimpleChatTC:Auto tool calling control to end user Instead of enforcing always explicit user triggered tool calling, now user is given the option whether to use explicit user triggered tool calling or to use auto triggering after showing tool details for a user specified amount of seconds. NOTE: The current logic doesnt account for user clicking the buttons before the autoclick triggers; need to cancel the auto clicks, if user triggers before autoclick, ie in future. --- tools/server/public_simplechat/simplechat.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 6f4d55940c..13d8034c51 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -741,6 +741,11 @@ class MultiChatUI { this.elInToolName.dataset.tool_call_id = ar.ns.tool_calls[0].id this.elInToolArgs.value = ar.ns.tool_calls[0].function.arguments this.elBtnTool.disabled = false + if (gMe.tools.auto > 0) { + setTimeout(()=>{ + this.elBtnTool.click() + }, gMe.tools.auto*1000) + } } else { this.elDivTool.hidden = true this.elInToolName.value = "" @@ -808,6 +813,11 @@ class MultiChatUI { clearTimeout(this.idTimeOut) this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(id, name, data); this.ui_reset_userinput(false) + if (gMe.tools.auto > 0) { + setTimeout(()=>{ + this.elBtnUser.click() + }, gMe.tools.auto*1000) + } }) this.elInUser.addEventListener("keyup", (ev)=> { @@ -1033,7 +1043,8 @@ class Me { this.tools = { enabled: false, fetchProxyUrl: "http://127.0.0.1:3128", - toolNames: /** @type {Array} */([]) + toolNames: /** @type {Array} */([]), + auto: 0 }; this.chatProps = { apiEP: ApiEP.Type.Chat, From fb968347b0cce1873450986801315869bbcd1e3b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 24 Oct 2025 19:04:33 +0530 Subject: [PATCH 138/446] SimpleChatTC:AutoToolCalls: Track and clear related timers also cleanup the existing toolResponseTimeout timer to be in the same structure and have similar flow convention. --- tools/server/public_simplechat/simplechat.js | 47 +++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 13d8034c51..c0f0ac4544 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -691,6 +691,29 @@ class MultiChatUI { /** @type {string} */ this.curChatId = ""; + this.TimePeriods = { + ToolCallResponseTimeout: 10000, + ToolCallAutoTimeUnit: 1000 + } + + this.timers = { + /** + * Used to identify Delay with getting response from a tool call. + * @type {number | undefined} + */ + toolcallResponseTimeout: undefined, + /** + * Used to auto trigger tool call, after a set time, if enabled. + * @type {number | undefined} + */ + toolcallTriggerClick: undefined, + /** + * Used to auto submit tool call response, after a set time, if enabled. + * @type {number | undefined} + */ + toolcallResponseSubmitClick: undefined + } + // the ui elements this.elInSystem = /** @type{HTMLInputElement} */(document.getElementById("system-in")); this.elDivChat = /** @type{HTMLDivElement} */(document.getElementById("chat-div")); @@ -742,9 +765,9 @@ class MultiChatUI { this.elInToolArgs.value = ar.ns.tool_calls[0].function.arguments this.elBtnTool.disabled = false if (gMe.tools.auto > 0) { - setTimeout(()=>{ + this.timers.toolcallTriggerClick = setTimeout(()=>{ this.elBtnTool.click() - }, gMe.tools.auto*1000) + }, gMe.tools.auto*this.TimePeriods.ToolCallAutoTimeUnit) } } else { this.elDivTool.hidden = true @@ -791,6 +814,8 @@ class MultiChatUI { }); this.elBtnUser.addEventListener("click", (ev)=>{ + clearTimeout(this.timers.toolcallResponseSubmitClick) + this.timers.toolcallResponseSubmitClick = undefined if (this.elInUser.disabled) { return; } @@ -803,20 +828,24 @@ class MultiChatUI { }); this.elBtnTool.addEventListener("click", (ev)=>{ + clearTimeout(this.timers.toolcallTriggerClick) + this.timers.toolcallTriggerClick = undefined if (this.elDivTool.hidden) { return; } this.handle_tool_run(this.curChatId); }) + // Handle messages from Tools web worker tools.setup((id, name, data)=>{ - clearTimeout(this.idTimeOut) + clearTimeout(this.timers.toolcallResponseTimeout) + this.timers.toolcallResponseTimeout = undefined this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(id, name, data); this.ui_reset_userinput(false) if (gMe.tools.auto > 0) { - setTimeout(()=>{ + this.timers.toolcallResponseSubmitClick = setTimeout(()=>{ this.elBtnUser.click() - }, gMe.tools.auto*1000) + }, gMe.tools.auto*this.TimePeriods.ToolCallAutoTimeUnit) } }) @@ -946,10 +975,10 @@ class MultiChatUI { this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, toolResult); this.ui_reset_userinput(false) } else { - this.idTimeOut = setTimeout(() => { + this.timers.toolcallResponseTimeout = setTimeout(() => { this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, `Tool/Function call ${toolname} taking too much time, aborting...`); this.ui_reset_userinput(false) - }, 10000) + }, this.TimePeriods.ToolCallResponseTimeout) } } @@ -1044,6 +1073,10 @@ class Me { enabled: false, fetchProxyUrl: "http://127.0.0.1:3128", toolNames: /** @type {Array} */([]), + /** + * Control how many seconds to wait before auto triggering tool call or its response submission. + * A value of 0 is treated as auto triggering disable. + */ auto: 0 }; this.chatProps = { From f74ce327e59ab6880ee3571ec957feeceb9a6204 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 24 Oct 2025 20:51:10 +0530 Subject: [PATCH 139/446] SimpleChatTC: Cleanup whitespaces identified by llama.cpp editorconfig check * convert tab to spaces in json config file * remove extra space at end of line --- .../local.tools/simpleproxy.json | 28 +++++++++---------- tools/server/public_simplechat/tooljs.mjs | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index 30f14b1d2c..4a6a520612 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -1,15 +1,15 @@ { - "allowed.domains": [ - ".*\\.bing\\.com$", - "^www\\.bing\\.com$", - ".*\\.yahoo\\.com$", - "^search\\.yahoo\\.com$", - ".*\\.brave\\.com$", - "^search\\.brave\\.com$", - "^brave\\.com$", - ".*\\.duckduckgo\\.com$", - "^duckduckgo\\.com$", - ".*\\.google\\.com$", - "^google\\.com$" - ] -} + "allowed.domains": [ + ".*\\.bing\\.com$", + "^www\\.bing\\.com$", + ".*\\.yahoo\\.com$", + "^search\\.yahoo\\.com$", + ".*\\.brave\\.com$", + "^search\\.brave\\.com$", + "^brave\\.com$", + ".*\\.duckduckgo\\.com$", + "^duckduckgo\\.com$", + ".*\\.google\\.com$", + "^google\\.com$" + ] +} \ No newline at end of file diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 3943ca4534..cfd216e366 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -73,7 +73,7 @@ function calc_run(toolcallid, toolname, obj) { /** - * Send a message to Tools WebWorker's monitor in main thread directly + * Send a message to Tools WebWorker's monitor in main thread directly * @param {MessageEvent} mev */ function message_toolsworker(mev) { From 2192ae6dd320539cf05bb0ba8c0ae8414adae00b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 24 Oct 2025 20:55:44 +0530 Subject: [PATCH 140/446] SimpleChatTC:Cleanup whitespace - github editorconfig checker Add missing newline to ending bracket line of json config file --- tools/server/public_simplechat/local.tools/simpleproxy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index 4a6a520612..949b7e014d 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -12,4 +12,4 @@ ".*\\.google\\.com$", "^google\\.com$" ] -} \ No newline at end of file +} From 8c8ddb1e5991d5be02fd57be9792855ce5c2677d Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 26 Oct 2025 12:01:56 +0530 Subject: [PATCH 141/446] SimpleChatTC:Update and cleanup the readme a bit include info about the auto option within tools. use nonwrapped text wrt certain sections, so that the markdown readme can be viewed properly wrt the structure of the content in it. --- tools/server/public_simplechat/readme.md | 232 ++++++++++++++--------- 1 file changed, 138 insertions(+), 94 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 83781a31ea..6576e26914 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -21,7 +21,7 @@ own system prompts. This allows seeing the generated text / ai-model response in oneshot at the end, after it is fully generated, or potentially as it is being generated, in a streamed manner from the server/ai-model. -![Chat and Settings screens](./simplechat_screens.webp "Chat and Settings screens") +![Chat and Settings (old) screens](./simplechat_screens.webp "Chat and Settings (old) screens") Auto saves the chat session locally as and when the chat is progressing and inturn at a later time when you open SimpleChat, option is provided to restore the old chat session, if a matching one exists. @@ -80,7 +80,7 @@ remember to * use a GenAi/LLM model which supports tool calling. * if fetch web url / page tool call is needed remember to run the bundled local.tools/simpleproxy.py - helper along with its config file + helper along with its config file, before using/loading this client ui through a browser * cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config simpleproxy.json @@ -154,6 +154,9 @@ Once inside User can even modify the response generated by the tool, if required, before submitting. * just refresh the page, to reset wrt the chat history and or system prompt and start afresh. + This also helps if you had forgotten to start the bundled simpleproxy.py server before hand. + Start the simpleproxy.py server and refresh the client ui page, to get access to web access + related tool calls. * Using NewChat one can start independent chat sessions. * two independent chat sessions are setup by default. @@ -181,91 +184,71 @@ Me/gMe consolidates the settings which control the behaviour into one object. One can see the current settings, as well as change/update them using browsers devel-tool/console. It is attached to the document object. Some of these can also be updated using the Settings UI. - baseURL - the domain-name/ip-address and inturn the port to send the request. + * baseURL - the domain-name/ip-address and inturn the port to send the request. - chatProps - maintain a set of properties which manipulate chatting with ai engine + * chatProps - maintain a set of properties which manipulate chatting with ai engine - apiEP - select between /completions and /chat/completions endpoint provided by the server/ai-model. + * apiEP - select between /completions and /chat/completions endpoint provided by the server/ai-model. - stream - control between oneshot-at-end and live-stream-as-its-generated collating and showing - of the generated response. + * stream - control between oneshot-at-end and live-stream-as-its-generated collating and showing of the generated response. the logic assumes that the text sent from the server follows utf-8 encoding. - in streaming mode - if there is any exception, the logic traps the same and tries to ensure - that text generated till then is not lost. + in streaming mode - if there is any exception, the logic traps the same and tries to ensure that text generated till then is not lost. - if a very long text is being generated, which leads to no user interaction for sometime and - inturn the machine goes into power saving mode or so, the platform may stop network connection, - leading to exception. + * if a very long text is being generated, which leads to no user interaction for sometime and inturn the machine goes into power saving mode or so, the platform may stop network connection, leading to exception. - iRecentUserMsgCnt - a simple minded SlidingWindow to limit context window load at Ai Model end. - This is set to 10 by default. So in addition to latest system message, last/latest iRecentUserMsgCnt - user messages after the latest system prompt and its responses from the ai model will be sent - to the ai-model, when querying for a new response. Note that if enabled, only user messages after - the latest system message/prompt will be considered. + * iRecentUserMsgCnt - a simple minded SlidingWindow to limit context window load at Ai Model end. This is set to 10 by default. So in addition to latest system message, last/latest iRecentUserMsgCnt user messages after the latest system prompt and its responses from the ai model will be sent to the ai-model, when querying for a new response. Note that if enabled, only user messages after the latest system message/prompt will be considered. This specified sliding window user message count also includes the latest user query. - <0 : Send entire chat history to server - 0 : Send only the system message if any to the server - >0 : Send the latest chat history from the latest system prompt, limited to specified cnt. - bCompletionFreshChatAlways - whether Completion mode collates complete/sliding-window history when - communicating with the server or only sends the latest user query/message. + * less than 0 : Send entire chat history to server - bCompletionInsertStandardRolePrefix - whether Completion mode inserts role related prefix wrt the - messages that get inserted into prompt field wrt /Completion endpoint. + * 0 : Send only the system message if any to the server - bTrimGarbage - whether garbage repeatation at the end of the generated ai response, should be - trimmed or left as is. If enabled, it will be trimmed so that it wont be sent back as part of - subsequent chat history. At the same time the actual trimmed text is shown to the user, once - when it was generated, so user can check if any useful info/data was there in the response. + * greater than 0 : Send the latest chat history from the latest system prompt, limited to specified cnt. - One may be able to request the ai-model to continue (wrt the last response) (if chat-history - is enabled as part of the chat-history-in-context setting), and chances are the ai-model will - continue starting from the trimmed part, thus allows long response to be recovered/continued - indirectly, in many cases. + * bCompletionFreshChatAlways - whether Completion mode collates complete/sliding-window history when communicating with the server or only sends the latest user query/message. - The histogram/freq based trimming logic is currently tuned for english language wrt its - is-it-a-alpabetic|numeral-char regex match logic. + * bCompletionInsertStandardRolePrefix - whether Completion mode inserts role related prefix wrt the messages that get inserted into prompt field wrt /Completion endpoint. - tools - contains controls related to tool calling + * bTrimGarbage - whether garbage repeatation at the end of the generated ai response, should be trimmed or left as is. If enabled, it will be trimmed so that it wont be sent back as part of subsequent chat history. At the same time the actual trimmed text is shown to the user, once when it was generated, so user can check if any useful info/data was there in the response. - enabled - control whether tool calling is enabled or not + One may be able to request the ai-model to continue (wrt the last response) (if chat-history is enabled as part of the chat-history-in-context setting), and chances are the ai-model will continue starting from the trimmed part, thus allows long response to be recovered/continued indirectly, in many cases. + + The histogram/freq based trimming logic is currently tuned for english language wrt its is-it-a-alpabetic|numeral-char regex match logic. + + * tools - contains controls related to tool calling + + * enabled - control whether tool calling is enabled or not remember to enable this only for GenAi/LLM models which support tool/function calling. - fetchProxyUrl - specify the address for the running instance of bundled local.tools/simpleproxy.py + * fetchProxyUrl - specify the address for the running instance of bundled local.tools/simpleproxy.py + + * auto - the amount of time in seconds to wait before the tool call request is auto triggered and generated response is auto submitted back. + + setting this value to 0 (default), disables auto logic, so that end user can review the tool calls requested by ai and if needed even modify them, before triggering/executing them as well as review and modify results generated by the tool call, before submitting them back to the ai. the builtin tools' meta data is sent to the ai model in the requests sent to it. - inturn if the ai model requests a tool call to be made, the same will be done and the response - sent back to the ai model, under user control. + inturn if the ai model requests a tool call to be made, the same will be done and the response sent back to the ai model, under user control, by default. - as tool calling will involve a bit of back and forth between ai assistant and end user, it is - recommended to set iRecentUserMsgCnt to 10 or more, so that enough context is retained during - chatting with ai models with tool support. + as tool calling will involve a bit of back and forth between ai assistant and end user, it is recommended to set iRecentUserMsgCnt to 10 or more, so that enough context is retained during chatting with ai models with tool support. - apiRequestOptions - maintains the list of options/fields to send along with api request, - irrespective of whether /chat/completions or /completions endpoint. + * apiRequestOptions - maintains the list of options/fields to send along with api request, irrespective of whether /chat/completions or /completions endpoint. - If you want to add additional options/fields to send to the server/ai-model, and or - modify the existing options value or remove them, for now you can update this global var - using browser's development-tools/console. + If you want to add additional options/fields to send to the server/ai-model, and or modify the existing options value or remove them, for now you can update this global var using browser's development-tools/console. - For string, numeric and boolean fields in apiRequestOptions, including even those added by a - user at runtime by directly modifying gMe.apiRequestOptions, setting ui entries will be auto - created. + For string, numeric and boolean fields in apiRequestOptions, including even those added by a user at runtime by directly modifying gMe.apiRequestOptions, setting ui entries will be auto created. - cache_prompt option supported by example/server is allowed to be controlled by user, so that - any caching supported wrt system-prompt and chat history, if usable can get used. When chat - history sliding window is enabled, cache_prompt logic may or may not kick in at the backend - wrt same, based on aspects related to model, positional encoding, attention mechanism etal. - However system prompt should ideally get the benefit of caching. + cache_prompt option supported by example/server is allowed to be controlled by user, so that any caching supported wrt system-prompt and chat history, if usable can get used. When chat history sliding window is enabled, cache_prompt logic may or may not kick in at the backend wrt same, based on aspects related to model, positional encoding, attention mechanism etal. However system prompt should ideally get the benefit of caching. - headers - maintains the list of http headers sent when request is made to the server. By default - Content-Type is set to application/json. Additionally Authorization entry is provided, which can - be set if needed using the settings ui. + * headers - maintains the list of http headers sent when request is made to the server. By default + + * Content-Type is set to application/json. + + * Additionally Authorization entry is provided, which can be set if needed using the settings ui. By using gMe's chatProps.iRecentUserMsgCnt and apiRequestOptions.max_tokens/n_predict one can try to @@ -298,14 +281,14 @@ However a developer when testing the server of ai-model may want to change these Using chatProps.iRecentUserMsgCnt reduce chat history context sent to the server/ai-model to be just the system-prompt, few prev-user-requests-and-ai-responses and cur-user-request, instead of full chat history. This way if there is any response with garbage/repeatation, it doesnt -mess with things beyond the next question/request/query, in some ways. The trim garbage +mess with things beyond the next few question/request/query, in some ways. The trim garbage option also tries to help avoid issues with garbage in the context to an extent. -Set max_tokens to 2048, so that a relatively large previous reponse doesnt eat up the space -available wrt next query-response. While parallely allowing a good enough context size for -some amount of the chat history in the current session to influence future answers. However +Set max_tokens to 2048 or as needed, so that a relatively large previous reponse doesnt eat up +the space available wrt next query-response. While parallely allowing a good enough context size +for some amount of the chat history in the current session to influence future answers. However dont forget that the server when started should also be started with a model context size of -2k or more, to be on safe side. +2k or more, as needed. The /completions endpoint of tools/server doesnt take max_tokens, instead it takes the internal n_predict, for now add the same here on the client side, maybe later add max_tokens @@ -346,56 +329,115 @@ work. ### Tool Calling -ALERT: The simple minded way in which this is implemented, it can be dangerous in the worst case, -Always remember to verify all the tool calls requested and the responses generated manually to -ensure everything is fine, during interaction with ai models with tools support. +Given that browsers provide a implicit env for not only showing ui, but also running logic, +simplechat client ui allows use of tool calling support provided by the newer ai models by +end users of llama.cpp's server in a simple way without needing to worry about seperate mcp +host / router, tools etal, for basic useful tools/functions like calculator, code execution +(javascript in this case). + +Additionally if users want to work with web content as part of their ai chat session, Few +functions related to web access which work with a included python based simple proxy server +have been implemented. + +This can allow end users to use some basic yet useful tool calls to enhance their ai chat +sessions to some extent. It also provides for a simple minded exploration of tool calling +support in newer ai models and some fun along the way as well as occasional practical use +like + +* verifying mathematical or logical statements/reasoning made by the ai model during chat +sessions by getting it to also create and execute mathematical expressions or code to verify +such stuff and so. + +* access content from internet and augment the ai model's context with additional data as +needed to help generate better responses. this can also be used for + * generating the latest news summary by fetching from news aggregator sites and collating + organising and summarising the same + * searching for specific topics and summarising the results + * or so + +The tool calling feature has been tested with Gemma3N, Granite4 and GptOss (given that +reasoning is currently unsupported by this client ui, it can mess with things) + +ALERT: The simple minded way in which this is implemented, it provides some minimal safety +mechanism like running ai generated code in web workers and restricting web access to user +specified whitelist and so, but it can still be dangerous in the worst case, So remember +to verify all the tool calls requested and the responses generated manually to ensure +everything is fine, during interaction with ai models with tools support. #### Builtin Tools The following tools/functions are currently provided by default + +##### directly in browser + * simple_calculator - which can solve simple arithmatic expressions + * run_javascript_function_code - which can be used to run some javascript code in the browser context. -* fetch_web_url_raw - fetch requested url through a proxy server -* fetch_web_url_text - fetch requested url through a proxy server - and also try strip the html respose of html tags and also head, script, style, header,footer,... blocks. -Currently the generated code / expression is run through a simple minded eval inside a web worker +Currently the ai generated code / expression is run through a simple minded eval inside a web worker mechanism. Use of WebWorker helps avoid exposing browser global scope to the generated code directly. However any shared web worker scope isnt isolated. Either way always remember to cross check the tool requests and generated responses when using tool calling. -fetch_web_url_raw/text and family works along with a corresponding simple local web proxy/caching -server logic, this helps bypass the CORS restrictions applied if trying to directly fetch from the -browser js runtime environment. Depending on the path specified wrt the proxy server, if urltext -(and not urlraw), it additionally tries to convert html content into equivalent text to some extent -in a simple minded manner by dropping head block as well as all scripts/styles/footers/headers/nav. -* the logic does a simple check to see if the bundled simpleproxy is running at specified fetchProxyUrl - before enabling fetch web related tool calls. -* The bundled simple proxy can be found at - * tools/server/public_simplechat/local.tools/simpleproxy.py - * it provides for a basic white list of allowed domains to access, to an extent - * it tries to mimic the client/browser making the request to it by propogating header entries like - user-agent, accept and accept-language from the got request to the generated request during proxying - so that websites will hopefully respect the request rather than blindly rejecting it as coming from - a non-browser entity. +##### using bundled simpleproxy.py (helps bypass browser cors restriction, ...) +* fetch_web_url_raw - fetch contents of the requested url through a proxy server + +* fetch_web_url_text - fetch text parts of the content from the requested url through a proxy server. + Related logic tries to strip html response of html tags and also head, script, style, header,footer, + nav, ... blocks. + +fetch_web_url_raw/text and family works along with a corresponding simple local web proxy (/caching +in future) server logic, this helps bypass the CORS restrictions applied if trying to directly fetch +from the browser js runtime environment. + +Depending on the path specified wrt the proxy server, it executes the corresponding logic. Like if +urltext path is used (and not urlraw), the logic in addition to fetching content from given url, it +tries to convert html content into equivalent text content to some extent in a simple minded manner +by dropping head block as well as all scripts/styles/footers/headers/nav blocks and inturn dropping +the html tags. + +The client ui logic does a simple check to see if the bundled simpleproxy is running at specified +fetchProxyUrl before enabling these web and related tool calls. + +The bundled simple proxy + +* can be found at + * tools/server/public_simplechat/local.tools/simpleproxy.py + +* it provides for a basic white list of allowed domains to access, to be specified by the end user. + This should help limit web access to a safe set of sites determined by the end user. + +* it tries to mimic the client/browser making the request to it by propogating header entries like + user-agent, accept and accept-language from the got request to the generated request during proxying + so that websites will hopefully respect the request rather than blindly rejecting it as coming from + a non-browser entity. + +In future it can be extended to help with other relatively simple yet useful tool calls like search_web, +data/documents_store and so. + + * for now search_web can be indirectly achieved using fetch_web_url_text/raw. #### Extending with new tools +This client ui implements the json schema based function calling convention supported by gen ai +engines over http. + Provide a descriptive meta data explaining the tool / function being provided for tool calling, as well as its arguments. -Provide a handler which should implement the specified tool / function call or rather for many -cases constructs the code to be run to get the tool / function call job done, and inturn pass -the same to the provided web worker to get it executed. Remember to use console.log while -generating any response that should be sent back to the ai model, in your constructed code. +Provide a handler which +* implements the specified tool / function call or +* rather in some cases constructs the code to be run to get the tool / function call job done, + and inturn pass the same to the provided web worker to get it executed. Use console.log while + generating any response that should be sent back to the ai model, in your constructed code. +* once the job is done, return the generated result as needed. Update the tc_switch to include a object entry for the tool, which inturn includes -* the meta data as well as -* a reference to the handler and also - the handler should take toolCallId, toolName and toolArgs and pass these along to - web worker as needed. +* the meta data wrt the tool call +* a reference to the handler - the handler should take toolCallId, toolName and toolArgs. + It should pass these along to the tools web worker, if used. * the result key (was used previously, may use in future, but for now left as is) #### OLD: Mapping tool calls and responses to normal assistant - user chat flow @@ -406,7 +448,7 @@ the SimpleChatTC pushes it into the normal assistant - user chat flow itself, by tool call and response as a pair of tagged request with details in the assistant block and inturn tagged response in the subsequent user block. -This allows the GenAi/LLM to be aware of the tool calls it made as well as the responses it got, +This allows GenAi/LLM to be still aware of the tool calls it made as well as the responses it got, so that it can incorporate the results of the same in the subsequent chat / interactions. NOTE: This flow tested to be ok enough with Gemma-3N-E4B-it-Q8_0 LLM ai model for now. Logically @@ -418,7 +460,7 @@ the tool call responses or even go further and have the logically seperate tool_ structures also. DONE: rather both tool_calls structure wrt assistant messages and tool role based tool call -result messages are generated as needed. +result messages are generated as needed now. #### Related stuff @@ -438,6 +480,8 @@ Handle reasoning/thinking responses from ai models. Handle multimodal handshaking with ai models. +Add search_web and documents|data_store tool calling, through the simpleproxy.py if and where needed. + ### Debuging the handshake From d00e5b341a3e6fcaca517dac5d9e28d8973dacc0 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 26 Oct 2025 15:51:48 +0530 Subject: [PATCH 142/446] SimpleChatTC:Duplicate tooljs.mjs to toolweb.mjs So as to split browser js webworker based tool calls from web related tool calls. --- tools/server/public_simplechat/toolweb.mjs | 257 +++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 tools/server/public_simplechat/toolweb.mjs diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs new file mode 100644 index 0000000000..cfd216e366 --- /dev/null +++ b/tools/server/public_simplechat/toolweb.mjs @@ -0,0 +1,257 @@ +//@ts-check +// DANGER DANGER DANGER - Simple and Stupid - Use from a discardable VM only +// Helpers to handle tools/functions calling wrt +// * javascript interpreter +// * simple arithmatic calculator +// by Humans for All +// + + +let gToolsWorker = /** @type{Worker} */(/** @type {unknown} */(null)); + + +let js_meta = { + "type": "function", + "function": { + "name": "run_javascript_function_code", + "description": "Runs given code using eval within a web worker context in a browser's javascript environment and returns the console.log outputs of the execution after few seconds", + "parameters": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "The code that will be run using eval within a web worker in the browser's javascript interpreter environment." + } + }, + "required": ["code"] + } + } + } + + +/** + * Implementation of the javascript interpretor logic. Minimal skeleton for now. + * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function js_run(toolcallid, toolname, obj) { + gToolsWorker.postMessage({ id: toolcallid, name: toolname, code: obj["code"]}) +} + + +let calc_meta = { + "type": "function", + "function": { + "name": "simple_calculator", + "description": "Calculates the provided arithmatic expression using console.log within a web worker of a browser's javascript interpreter environment and returns the output of the execution once it is done in few seconds", + "parameters": { + "type": "object", + "properties": { + "arithexpr":{ + "type":"string", + "description":"The arithmatic expression that will be calculated by passing it to console.log of a browser's javascript interpreter." + } + }, + "required": ["arithexpr"] + } + } + } + + +/** + * Implementation of the simple calculator logic. Minimal skeleton for now. + * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function calc_run(toolcallid, toolname, obj) { + gToolsWorker.postMessage({ id: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) +} + + +/** + * Send a message to Tools WebWorker's monitor in main thread directly + * @param {MessageEvent} mev + */ +function message_toolsworker(mev) { + // @ts-ignore + gToolsWorker.onmessage(mev) +} + + +let fetchweburlraw_meta = { + "type": "function", + "function": { + "name": "fetch_web_url_raw", + "description": "Fetch the requested web url through a proxy server and return the got content as is, in few seconds", + "parameters": { + "type": "object", + "properties": { + "url":{ + "type":"string", + "description":"url of the web page to fetch from the internet" + } + }, + "required": ["url"] + } + } + } + + +/** + * Implementation of the fetch web url raw logic. Dumb initial go. + * Expects a simple minded proxy server to be running locally + * * listening on port 3128 + * * expecting http requests + * * with a query token named url wrt the path urlraw + * which gives the actual url to fetch + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function fetchweburlraw_run(toolcallid, toolname, obj) { + if (gToolsWorker.onmessage != null) { + // @ts-ignore + let newUrl = `${document['gMe'].tools.fetchProxyUrl}/urlraw?url=${encodeURIComponent(obj.url)}` + fetch(newUrl).then(resp => { + if (!resp.ok) { + throw new Error(`${resp.status}:${resp.statusText}`); + } + return resp.text() + }).then(data => { + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) + }).catch((err)=>{ + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) + }) + } +} + + +/** + * Setup fetch_web_url_raw for tool calling + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {Object>} tcs + */ +async function fetchweburlraw_setup(tcs) { + // @ts-ignore + let got = await fetch(`${document["gMe"].tools.fetchProxyUrl}/aum?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ + if (resp.statusText != 'bharatavarshe') { + console.log("WARN:ToolJS:FetchWebUrlRaw:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") + return + } else { + console.log("INFO:ToolJS:FetchWebUrlRaw:Enabling...") + } + tcs["fetch_web_url_raw"] = { + "handler": fetchweburlraw_run, + "meta": fetchweburlraw_meta, + "result": "" + }; + }).catch(err=>console.log(`WARN:ToolJS:FetchWebUrlRaw:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) +} + + +let fetchweburltext_meta = { + "type": "function", + "function": { + "name": "fetch_web_url_text", + "description": "Fetch the requested web url through a proxy server and return its text content after stripping away the html tags as well as head, script, style, header, footer, nav blocks, in few seconds", + "parameters": { + "type": "object", + "properties": { + "url":{ + "type":"string", + "description":"url of the page that will be fetched from the internet and inturn unwanted stuff stripped from its contents to some extent" + } + }, + "required": ["url"] + } + } + } + + +/** + * Implementation of the fetch web url text logic. Dumb initial go. + * Expects a simple minded proxy server to be running locally + * * listening on port 3128 + * * expecting http requests + * * with a query token named url wrt urltext path, + * which gives the actual url to fetch + * * strips out head as well as any script, style, header, footer, nav and so blocks in body + * before returning remaining body contents. + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function fetchweburltext_run(toolcallid, toolname, obj) { + if (gToolsWorker.onmessage != null) { + // @ts-ignore + let newUrl = `${document['gMe'].tools.fetchProxyUrl}/urltext?url=${encodeURIComponent(obj.url)}` + fetch(newUrl).then(resp => { + if (!resp.ok) { + throw new Error(`${resp.status}:${resp.statusText}`); + } + return resp.text() + }).then(data => { + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) + }).catch((err)=>{ + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) + }) + } +} + + +/** + * Setup fetch_web_url_text for tool calling + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {Object>} tcs + */ +async function fetchweburltext_setup(tcs) { + // @ts-ignore + let got = await fetch(`${document["gMe"].tools.fetchProxyUrl}/aum?url=jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ + if (resp.statusText != 'bharatavarshe') { + console.log("WARN:ToolJS:FetchWebUrlText:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") + return + } else { + console.log("INFO:ToolJS:FetchWebUrlText:Enabling...") + } + tcs["fetch_web_url_text"] = { + "handler": fetchweburltext_run, + "meta": fetchweburltext_meta, + "result": "" + }; + }).catch(err=>console.log(`WARN:ToolJS:FetchWebUrlText:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) +} + + +/** + * @type {Object>} + */ +export let tc_switch = { + "run_javascript_function_code": { + "handler": js_run, + "meta": js_meta, + "result": "" + }, + "simple_calculator": { + "handler": calc_run, + "meta": calc_meta, + "result": "" + }, +} + + +/** + * Used to get hold of the web worker to use for running tool/function call related code + * Also to setup tool calls, which need to cross check things at runtime + * @param {Worker} toolsWorker + */ +export async function init(toolsWorker) { + gToolsWorker = toolsWorker + await fetchweburlraw_setup(tc_switch) + await fetchweburltext_setup(tc_switch) +} From 978ee3db1e2d4f1914c6d5efaf7a0f27daa7d63a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 26 Oct 2025 15:54:47 +0530 Subject: [PATCH 143/446] SimpleChatTC:ToolCalling:Seprat out JSWebWorker and ProxyBasedWeb Remove the unneed (belonging to the other file) stuff from tooljs and toolweb files. Update tools manager to make use of the new toolweb module --- tools/server/public_simplechat/tooljs.mjs | 160 +-------------------- tools/server/public_simplechat/tools.mjs | 21 ++- tools/server/public_simplechat/toolweb.mjs | 90 +----------- 3 files changed, 25 insertions(+), 246 deletions(-) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index cfd216e366..0e9ce61c3e 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -1,5 +1,5 @@ //@ts-check -// DANGER DANGER DANGER - Simple and Stupid - Use from a discardable VM only +// ALERT - Simple Stupid flow - Using from a discardable VM is better // Helpers to handle tools/functions calling wrt // * javascript interpreter // * simple arithmatic calculator @@ -72,162 +72,6 @@ function calc_run(toolcallid, toolname, obj) { } -/** - * Send a message to Tools WebWorker's monitor in main thread directly - * @param {MessageEvent} mev - */ -function message_toolsworker(mev) { - // @ts-ignore - gToolsWorker.onmessage(mev) -} - - -let fetchweburlraw_meta = { - "type": "function", - "function": { - "name": "fetch_web_url_raw", - "description": "Fetch the requested web url through a proxy server and return the got content as is, in few seconds", - "parameters": { - "type": "object", - "properties": { - "url":{ - "type":"string", - "description":"url of the web page to fetch from the internet" - } - }, - "required": ["url"] - } - } - } - - -/** - * Implementation of the fetch web url raw logic. Dumb initial go. - * Expects a simple minded proxy server to be running locally - * * listening on port 3128 - * * expecting http requests - * * with a query token named url wrt the path urlraw - * which gives the actual url to fetch - * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function fetchweburlraw_run(toolcallid, toolname, obj) { - if (gToolsWorker.onmessage != null) { - // @ts-ignore - let newUrl = `${document['gMe'].tools.fetchProxyUrl}/urlraw?url=${encodeURIComponent(obj.url)}` - fetch(newUrl).then(resp => { - if (!resp.ok) { - throw new Error(`${resp.status}:${resp.statusText}`); - } - return resp.text() - }).then(data => { - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) - }).catch((err)=>{ - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) - }) - } -} - - -/** - * Setup fetch_web_url_raw for tool calling - * NOTE: Currently the logic is setup for the bundled simpleproxy.py - * @param {Object>} tcs - */ -async function fetchweburlraw_setup(tcs) { - // @ts-ignore - let got = await fetch(`${document["gMe"].tools.fetchProxyUrl}/aum?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ - if (resp.statusText != 'bharatavarshe') { - console.log("WARN:ToolJS:FetchWebUrlRaw:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") - return - } else { - console.log("INFO:ToolJS:FetchWebUrlRaw:Enabling...") - } - tcs["fetch_web_url_raw"] = { - "handler": fetchweburlraw_run, - "meta": fetchweburlraw_meta, - "result": "" - }; - }).catch(err=>console.log(`WARN:ToolJS:FetchWebUrlRaw:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) -} - - -let fetchweburltext_meta = { - "type": "function", - "function": { - "name": "fetch_web_url_text", - "description": "Fetch the requested web url through a proxy server and return its text content after stripping away the html tags as well as head, script, style, header, footer, nav blocks, in few seconds", - "parameters": { - "type": "object", - "properties": { - "url":{ - "type":"string", - "description":"url of the page that will be fetched from the internet and inturn unwanted stuff stripped from its contents to some extent" - } - }, - "required": ["url"] - } - } - } - - -/** - * Implementation of the fetch web url text logic. Dumb initial go. - * Expects a simple minded proxy server to be running locally - * * listening on port 3128 - * * expecting http requests - * * with a query token named url wrt urltext path, - * which gives the actual url to fetch - * * strips out head as well as any script, style, header, footer, nav and so blocks in body - * before returning remaining body contents. - * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function fetchweburltext_run(toolcallid, toolname, obj) { - if (gToolsWorker.onmessage != null) { - // @ts-ignore - let newUrl = `${document['gMe'].tools.fetchProxyUrl}/urltext?url=${encodeURIComponent(obj.url)}` - fetch(newUrl).then(resp => { - if (!resp.ok) { - throw new Error(`${resp.status}:${resp.statusText}`); - } - return resp.text() - }).then(data => { - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) - }).catch((err)=>{ - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) - }) - } -} - - -/** - * Setup fetch_web_url_text for tool calling - * NOTE: Currently the logic is setup for the bundled simpleproxy.py - * @param {Object>} tcs - */ -async function fetchweburltext_setup(tcs) { - // @ts-ignore - let got = await fetch(`${document["gMe"].tools.fetchProxyUrl}/aum?url=jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ - if (resp.statusText != 'bharatavarshe') { - console.log("WARN:ToolJS:FetchWebUrlText:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") - return - } else { - console.log("INFO:ToolJS:FetchWebUrlText:Enabling...") - } - tcs["fetch_web_url_text"] = { - "handler": fetchweburltext_run, - "meta": fetchweburltext_meta, - "result": "" - }; - }).catch(err=>console.log(`WARN:ToolJS:FetchWebUrlText:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) -} - - /** * @type {Object>} */ @@ -252,6 +96,4 @@ export let tc_switch = { */ export async function init(toolsWorker) { gToolsWorker = toolsWorker - await fetchweburlraw_setup(tc_switch) - await fetchweburltext_setup(tc_switch) } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 2b4237258e..23eb7e35e8 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -1,30 +1,42 @@ //@ts-check -// DANGER DANGER DANGER - Simple and Stupid - Use from a discardable VM only +// ALERT - Simple Stupid flow - Using from a discardable VM is better // Helpers to handle tools/functions calling in a direct and dangerous way // by Humans for All // import * as tjs from './tooljs.mjs' +import * as tweb from './toolweb.mjs' let gToolsWorker = new Worker('./toolsworker.mjs', { type: 'module' }); /** + * Maintain currently available tool/function calls * @type {Object>} */ export let tc_switch = {} + export async function init() { - return tjs.init(gToolsWorker).then(()=>{ - let toolNames = [] + /** + * @type {string[]} + */ + let toolNames = [] + await tjs.init(gToolsWorker).then(()=>{ for (const key in tjs.tc_switch) { tc_switch[key] = tjs.tc_switch[key] toolNames.push(key) } - return toolNames }) + let tNs = await tweb.init(gToolsWorker) + for (const key in tNs) { + tc_switch[key] = tNs[key] + toolNames.push(key) + } + return toolNames } + export function meta() { let tools = [] for (const key in tc_switch) { @@ -33,6 +45,7 @@ export function meta() { return tools } + /** * Setup the callback that will be called when ever message * is recieved from the Tools Web Worker. diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index cfd216e366..351b41ff48 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -1,8 +1,6 @@ //@ts-check -// DANGER DANGER DANGER - Simple and Stupid - Use from a discardable VM only -// Helpers to handle tools/functions calling wrt -// * javascript interpreter -// * simple arithmatic calculator +// ALERT - Simple Stupid flow - Using from a discardable VM is better +// Helpers to handle tools/functions calling related to web access // by Humans for All // @@ -10,68 +8,6 @@ let gToolsWorker = /** @type{Worker} */(/** @type {unknown} */(null)); -let js_meta = { - "type": "function", - "function": { - "name": "run_javascript_function_code", - "description": "Runs given code using eval within a web worker context in a browser's javascript environment and returns the console.log outputs of the execution after few seconds", - "parameters": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The code that will be run using eval within a web worker in the browser's javascript interpreter environment." - } - }, - "required": ["code"] - } - } - } - - -/** - * Implementation of the javascript interpretor logic. Minimal skeleton for now. - * ALERT: Has access to the javascript web worker environment and can mess with it and beyond - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function js_run(toolcallid, toolname, obj) { - gToolsWorker.postMessage({ id: toolcallid, name: toolname, code: obj["code"]}) -} - - -let calc_meta = { - "type": "function", - "function": { - "name": "simple_calculator", - "description": "Calculates the provided arithmatic expression using console.log within a web worker of a browser's javascript interpreter environment and returns the output of the execution once it is done in few seconds", - "parameters": { - "type": "object", - "properties": { - "arithexpr":{ - "type":"string", - "description":"The arithmatic expression that will be calculated by passing it to console.log of a browser's javascript interpreter." - } - }, - "required": ["arithexpr"] - } - } - } - - -/** - * Implementation of the simple calculator logic. Minimal skeleton for now. - * ALERT: Has access to the javascript web worker environment and can mess with it and beyond - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function calc_run(toolcallid, toolname, obj) { - gToolsWorker.postMessage({ id: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) -} - - /** * Send a message to Tools WebWorker's monitor in main thread directly * @param {MessageEvent} mev @@ -228,30 +164,18 @@ async function fetchweburltext_setup(tcs) { } -/** - * @type {Object>} - */ -export let tc_switch = { - "run_javascript_function_code": { - "handler": js_run, - "meta": js_meta, - "result": "" - }, - "simple_calculator": { - "handler": calc_run, - "meta": calc_meta, - "result": "" - }, -} - - /** * Used to get hold of the web worker to use for running tool/function call related code * Also to setup tool calls, which need to cross check things at runtime * @param {Worker} toolsWorker */ export async function init(toolsWorker) { + /** + * @type {Object>} tcs + */ + let tc_switch = {} gToolsWorker = toolsWorker await fetchweburlraw_setup(tc_switch) await fetchweburltext_setup(tc_switch) + return tc_switch } From de6f370d3b285aa208655896ed171ffe40a17280 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 26 Oct 2025 19:13:30 +0530 Subject: [PATCH 144/446] SimpleChatTC:ToolCall:SearchWebText using UrlText Initial go at implementing a web search tool call, which uses the existing UrlText support of the bundled simpleproxy.py. It allows user to control the search engine to use, by allowing them to set the search engine url template. The logic comes with search engine url template strings for duckduckgo, brave, bing and google. With duckduckgo set by default. --- tools/server/public_simplechat/simplechat.js | 13 +++ tools/server/public_simplechat/toolweb.mjs | 97 +++++++++++++++++++- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index c0f0ac4544..9992ec2474 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1063,6 +1063,18 @@ class MultiChatUI { } +/** + * Few web search engine url template strings. + * The SEARCHWORDS keyword will get replaced by the actual user specified search words at runtime. + */ +const SearchURLS = { + duckduckgo: "https://duckduckgo.com/html/?q=SEARCHWORDS", + bing: "https://www.bing.com/search?q=SEARCHWORDS", // doesnt seem to like google chrome clients in particular + brave: "https://search.brave.com/search?q=SEARCHWORDS", + google: "https://www.google.com/search?q=SEARCHWORDS", // doesnt seem to like any client in general +} + + class Me { constructor() { @@ -1072,6 +1084,7 @@ class Me { this.tools = { enabled: false, fetchProxyUrl: "http://127.0.0.1:3128", + searchUrl: SearchURLS.duckduckgo, toolNames: /** @type {Array} */([]), /** * Control how many seconds to wait before auto triggering tool call or its response submission. diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 351b41ff48..6d64099c5d 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -1,6 +1,7 @@ //@ts-check // ALERT - Simple Stupid flow - Using from a discardable VM is better // Helpers to handle tools/functions calling related to web access +// which work in sync with the bundled simpleproxy.py server logic. // by Humans for All // @@ -18,6 +19,15 @@ function message_toolsworker(mev) { } +/** + * Retrieve a member of the global Me instance + */ +function get_gme() { + return (/** @type {Object>} */(/** @type {unknown} */(document)))['gMe'] + //return (/** @type {Object>} */(/** @type {unknown} */(document)))['gMe'][key] +} + + let fetchweburlraw_meta = { "type": "function", "function": { @@ -40,7 +50,7 @@ let fetchweburlraw_meta = { /** * Implementation of the fetch web url raw logic. Dumb initial go. * Expects a simple minded proxy server to be running locally - * * listening on port 3128 + * * listening on a configured port * * expecting http requests * * with a query token named url wrt the path urlraw * which gives the actual url to fetch @@ -112,7 +122,7 @@ let fetchweburltext_meta = { /** * Implementation of the fetch web url text logic. Dumb initial go. * Expects a simple minded proxy server to be running locally - * * listening on port 3128 + * * listening on a configured port * * expecting http requests * * with a query token named url wrt urltext path, * which gives the actual url to fetch @@ -164,6 +174,88 @@ async function fetchweburltext_setup(tcs) { } +// +// Search Web Text +// + + +let searchwebtext_meta = { + "type": "function", + "function": { + "name": "search_web_text", + "description": "search web for given words and return the plain text content after stripping the html tags as well as head, script, style, header, footer, nav blocks from got html result page, in few seconds", + "parameters": { + "type": "object", + "properties": { + "words":{ + "type":"string", + "description":"the words to search for on the web" + } + }, + "required": ["words"] + } + } + } + + +/** + * Implementation of the search web text logic. Initial go. + * Expects simpleproxy.py server to be running locally + * * listening on a configured port + * * expecting http requests + * * with a query token named url wrt urltext path, + * which gives the actual url to fetch + * * strips out head as well as any script, style, header, footer, nav and so blocks in body + * before returning remaining body contents. + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function searchwebtext_run(toolcallid, toolname, obj) { + if (gToolsWorker.onmessage != null) { + /** @type {string} */ + let searchUrl = get_gme().tools.searchUrl; + searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)) + let newUrl = `${get_gme().tools.fetchProxyUrl}/urltext?url=${encodeURIComponent(searchUrl)}` + fetch(newUrl).then(resp => { + if (!resp.ok) { + throw new Error(`${resp.status}:${resp.statusText}`); + } + return resp.text() + }).then(data => { + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) + }).catch((err)=>{ + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) + }) + } +} + + +/** + * Setup search_web_text for tool calling + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {Object>} tcs + */ +async function searchwebtext_setup(tcs) { + // @ts-ignore + let got = await fetch(`${document["gMe"].tools.fetchProxyUrl}/aum?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ + if (resp.statusText != 'bharatavarshe') { + console.log("WARN:ToolJS:SearchWebText:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") + return + } else { + console.log("INFO:ToolJS:SearchWebText:Enabling...") + } + tcs["search_web_text"] = { + "handler": searchwebtext_run, + "meta": searchwebtext_meta, + "result": "" + }; + }).catch(err=>console.log(`WARN:ToolJS:SearchWebText:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) +} + + + /** * Used to get hold of the web worker to use for running tool/function call related code * Also to setup tool calls, which need to cross check things at runtime @@ -177,5 +269,6 @@ export async function init(toolsWorker) { gToolsWorker = toolsWorker await fetchweburlraw_setup(tc_switch) await fetchweburltext_setup(tc_switch) + await searchwebtext_setup(tc_switch) return tc_switch } From 221b5a9228c80683513786ac510b478874a0b73b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 26 Oct 2025 21:12:48 +0530 Subject: [PATCH 145/446] SimpleChatTC:ToolCallWeby: Cleanup the toolweb module flow Avoid code duplication, by creating helpers for setup and toolcall. Also send indication of the path that will be used, when checking for simpleproxy.py server to be running at runtime setup. --- tools/server/public_simplechat/toolweb.mjs | 174 +++++++++++---------- 1 file changed, 88 insertions(+), 86 deletions(-) diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 6d64099c5d..11f4cd6b3f 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -20,14 +20,73 @@ function message_toolsworker(mev) { /** - * Retrieve a member of the global Me instance + * Retrieve the global Me instance */ function get_gme() { return (/** @type {Object>} */(/** @type {unknown} */(document)))['gMe'] - //return (/** @type {Object>} */(/** @type {unknown} */(document)))['gMe'][key] } +/** + * Helper http get logic wrt the bundled SimpleProxy server, + * which helps execute a given proxy dependent tool call. + * Expects the simple minded proxy server to be running locally + * * listening on a configured port + * * expecting http requests + * * with a predefined query token and value wrt a predefined path + * NOTE: Initial go, handles textual data type. + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + * @param {string} path + * @param {string} qkey + * @param {string} qvalue + */ +function proxyserver_get_1arg(toolcallid, toolname, obj, path, qkey, qvalue) { + if (gToolsWorker.onmessage != null) { + let newUrl = `${get_gme().tools.fetchProxyUrl}/${path}?${qkey}=${qvalue}` + fetch(newUrl).then(resp => { + if (!resp.ok) { + throw new Error(`${resp.status}:${resp.statusText}`); + } + return resp.text() + }).then(data => { + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) + }).catch((err)=>{ + message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) + }) + } +} + + +/** + * Setup a proxy server dependent tool call + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {string} tag + * @param {string} tcPath + * @param {string} tcName + * @param {{ [x: string]: any; }} tcsData + * @param {Object>} tcs + */ +async function proxyserver_tc_setup(tag, tcPath, tcName, tcsData, tcs) { + await fetch(`${get_gme().tools.fetchProxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ + if (resp.statusText != 'bharatavarshe') { + console.log(`WARN:ToolWeb:${tag}:Dont forget to run the bundled local.tools/simpleproxy.py to enable me`) + return + } else { + console.log(`INFO:ToolWeb:${tag}:Enabling...`) + } + tcs[tcName] = tcsData; + }).catch(err=>console.log(`WARN:ToolWeb:${tag}:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) +} + + +// +// Fetch Web Url Raw +// + + let fetchweburlraw_meta = { "type": "function", "function": { @@ -48,7 +107,7 @@ let fetchweburlraw_meta = { /** - * Implementation of the fetch web url raw logic. Dumb initial go. + * Implementation of the fetch web url raw logic. * Expects a simple minded proxy server to be running locally * * listening on a configured port * * expecting http requests @@ -60,20 +119,7 @@ let fetchweburlraw_meta = { * @param {any} obj */ function fetchweburlraw_run(toolcallid, toolname, obj) { - if (gToolsWorker.onmessage != null) { - // @ts-ignore - let newUrl = `${document['gMe'].tools.fetchProxyUrl}/urlraw?url=${encodeURIComponent(obj.url)}` - fetch(newUrl).then(resp => { - if (!resp.ok) { - throw new Error(`${resp.status}:${resp.statusText}`); - } - return resp.text() - }).then(data => { - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) - }).catch((err)=>{ - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) - }) - } + return proxyserver_get_1arg(toolcallid, toolname, obj, 'urlraw', 'url', encodeURIComponent(obj.url)); } @@ -83,23 +129,19 @@ function fetchweburlraw_run(toolcallid, toolname, obj) { * @param {Object>} tcs */ async function fetchweburlraw_setup(tcs) { - // @ts-ignore - let got = await fetch(`${document["gMe"].tools.fetchProxyUrl}/aum?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ - if (resp.statusText != 'bharatavarshe') { - console.log("WARN:ToolJS:FetchWebUrlRaw:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") - return - } else { - console.log("INFO:ToolJS:FetchWebUrlRaw:Enabling...") - } - tcs["fetch_web_url_raw"] = { - "handler": fetchweburlraw_run, - "meta": fetchweburlraw_meta, - "result": "" - }; - }).catch(err=>console.log(`WARN:ToolJS:FetchWebUrlRaw:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) + return proxyserver_tc_setup('FetchWebUrlRaw', 'urlraw', 'fetch_web_url_raw', { + "handler": fetchweburlraw_run, + "meta": fetchweburlraw_meta, + "result": "" + }, tcs); } +// +// Fetch Web Url Text +// + + let fetchweburltext_meta = { "type": "function", "function": { @@ -120,7 +162,7 @@ let fetchweburltext_meta = { /** - * Implementation of the fetch web url text logic. Dumb initial go. + * Implementation of the fetch web url text logic. * Expects a simple minded proxy server to be running locally * * listening on a configured port * * expecting http requests @@ -134,20 +176,7 @@ let fetchweburltext_meta = { * @param {any} obj */ function fetchweburltext_run(toolcallid, toolname, obj) { - if (gToolsWorker.onmessage != null) { - // @ts-ignore - let newUrl = `${document['gMe'].tools.fetchProxyUrl}/urltext?url=${encodeURIComponent(obj.url)}` - fetch(newUrl).then(resp => { - if (!resp.ok) { - throw new Error(`${resp.status}:${resp.statusText}`); - } - return resp.text() - }).then(data => { - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) - }).catch((err)=>{ - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) - }) - } + return proxyserver_get_1arg(toolcallid, toolname, obj, 'urltext', 'url', encodeURIComponent(obj.url)); } @@ -157,20 +186,11 @@ function fetchweburltext_run(toolcallid, toolname, obj) { * @param {Object>} tcs */ async function fetchweburltext_setup(tcs) { - // @ts-ignore - let got = await fetch(`${document["gMe"].tools.fetchProxyUrl}/aum?url=jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ - if (resp.statusText != 'bharatavarshe') { - console.log("WARN:ToolJS:FetchWebUrlText:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") - return - } else { - console.log("INFO:ToolJS:FetchWebUrlText:Enabling...") - } - tcs["fetch_web_url_text"] = { - "handler": fetchweburltext_run, - "meta": fetchweburltext_meta, - "result": "" - }; - }).catch(err=>console.log(`WARN:ToolJS:FetchWebUrlText:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) + return proxyserver_tc_setup('FetchWebUrlText', 'urltext', 'fetch_web_url_text', { + "handler": fetchweburltext_run, + "meta": fetchweburltext_meta, + "result": "" + }, tcs); } @@ -200,6 +220,7 @@ let searchwebtext_meta = { /** * Implementation of the search web text logic. Initial go. + * Builds on urltext path of the bundled simpleproxy.py. * Expects simpleproxy.py server to be running locally * * listening on a configured port * * expecting http requests @@ -216,18 +237,8 @@ function searchwebtext_run(toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { /** @type {string} */ let searchUrl = get_gme().tools.searchUrl; - searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)) - let newUrl = `${get_gme().tools.fetchProxyUrl}/urltext?url=${encodeURIComponent(searchUrl)}` - fetch(newUrl).then(resp => { - if (!resp.ok) { - throw new Error(`${resp.status}:${resp.statusText}`); - } - return resp.text() - }).then(data => { - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) - }).catch((err)=>{ - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) - }) + searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)); + return proxyserver_get_1arg(toolcallid, toolname, obj, 'urltext', 'url', encodeURIComponent(searchUrl)); } } @@ -238,20 +249,11 @@ function searchwebtext_run(toolcallid, toolname, obj) { * @param {Object>} tcs */ async function searchwebtext_setup(tcs) { - // @ts-ignore - let got = await fetch(`${document["gMe"].tools.fetchProxyUrl}/aum?url=jambudweepe.multiverse.987654321123456789`).then(resp=>{ - if (resp.statusText != 'bharatavarshe') { - console.log("WARN:ToolJS:SearchWebText:Dont forget to run the bundled local.tools/simpleproxy.py to enable me") - return - } else { - console.log("INFO:ToolJS:SearchWebText:Enabling...") - } - tcs["search_web_text"] = { - "handler": searchwebtext_run, - "meta": searchwebtext_meta, - "result": "" - }; - }).catch(err=>console.log(`WARN:ToolJS:SearchWebText:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) + return proxyserver_tc_setup('SearchWebText', 'urltext', 'search_web_text', { + "handler": searchwebtext_run, + "meta": searchwebtext_meta, + "result": "" + }, tcs); } From 252fb91e95857335710c861ae7a4c8da97eadc0f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 26 Oct 2025 23:12:06 +0530 Subject: [PATCH 146/446] SimpleChatTC:WebSearchPlus: Update readme, Wikipedia in allowed If using wikipedia or so, remember to have sufficient context window in general wrt the ai engine as well as wrt the handshake / chat end point. --- .../local.tools/simpleproxy.json | 1 + tools/server/public_simplechat/readme.md | 43 ++++++++++++------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index 949b7e014d..d68878199a 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -1,5 +1,6 @@ { "allowed.domains": [ + ".*\\.wikipedia\\.org$", ".*\\.bing\\.com$", "^www\\.bing\\.com$", ".*\\.yahoo\\.com$", diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 6576e26914..30c4a2271e 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -34,8 +34,9 @@ console. Parallely some of the directly useful to end-user settings can also be settings ui. For GenAi/LLM models supporting tool / function calling, allows one to interact with them and explore use of -ai driven augmenting of the knowledge used for generating answers by using the predefined tools/functions. -The end user is provided control over tool calling and response submitting. +ai driven augmenting of the knowledge used for generating answers as well as for cross checking ai generated +answers logically / programatically and by checking with other sources and lot more by making using of the +predefined tools / functions. The end user is provided control over tool calling and response submitting. NOTE: Current web service api doesnt expose the model context length directly, so client logic doesnt provide any adaptive culling of old messages nor of replacing them with summary of their content etal. However there @@ -79,13 +80,14 @@ remember to * use a GenAi/LLM model which supports tool calling. -* if fetch web url / page tool call is needed remember to run the bundled local.tools/simpleproxy.py +* if fetch web page or web search tool call is needed remember to run bundled local.tools/simpleproxy.py helper along with its config file, before using/loading this client ui through a browser * cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config simpleproxy.json - * remember that this is a relatively dumb proxy logic along with optional stripping of scripts / styles - / headers / footers /..., Be careful if trying to fetch web pages, and use it only with known safe sites. + * remember that this is a relatively minimal dumb proxy logic along with optional stripping of non textual + content like head, scripts, styles, headers, footers, ... Be careful when accessing web through this and + use it only with known safe sites. * it allows one to specify a white list of allowed.domains, look into local.tools/simpleproxy.json @@ -226,6 +228,8 @@ It is attached to the document object. Some of these can also be updated using t * fetchProxyUrl - specify the address for the running instance of bundled local.tools/simpleproxy.py + * searchUrl - specify the search engine's search url template along with the tag SEARCHWORDS in place where the search words should be substituted at runtime. + * auto - the amount of time in seconds to wait before the tool call request is auto triggered and generated response is auto submitted back. setting this value to 0 (default), disables auto logic, so that end user can review the tool calls requested by ai and if needed even modify them, before triggering/executing them as well as review and modify results generated by the tool call, before submitting them back to the ai. @@ -362,7 +366,8 @@ ALERT: The simple minded way in which this is implemented, it provides some mini mechanism like running ai generated code in web workers and restricting web access to user specified whitelist and so, but it can still be dangerous in the worst case, So remember to verify all the tool calls requested and the responses generated manually to ensure -everything is fine, during interaction with ai models with tools support. +everything is fine, during interaction with ai models with tools support. One could also +always run this from a discardable vm, just in case if one wants to be extra cautious. #### Builtin Tools @@ -388,15 +393,18 @@ requests and generated responses when using tool calling. Related logic tries to strip html response of html tags and also head, script, style, header,footer, nav, ... blocks. -fetch_web_url_raw/text and family works along with a corresponding simple local web proxy (/caching -in future) server logic, this helps bypass the CORS restrictions applied if trying to directly fetch -from the browser js runtime environment. +* search_web_text - search for the specified words using the configured search engine and return the +plain textual content from the search result page. + +the above set of web related tool calls work by handshaking with a bundled simple local web proxy +(/caching in future) server logic, this helps bypass the CORS restrictions applied if trying to +directly fetch from the browser js runtime environment. Depending on the path specified wrt the proxy server, it executes the corresponding logic. Like if urltext path is used (and not urlraw), the logic in addition to fetching content from given url, it -tries to convert html content into equivalent text content to some extent in a simple minded manner -by dropping head block as well as all scripts/styles/footers/headers/nav blocks and inturn dropping -the html tags. +tries to convert html content into equivalent plain text content to some extent in a simple minded +manner by dropping head block as well as all scripts/styles/footers/headers/nav blocks and inturn +dropping the html tags. The client ui logic does a simple check to see if the bundled simpleproxy is running at specified fetchProxyUrl before enabling these web and related tool calls. @@ -414,10 +422,10 @@ The bundled simple proxy so that websites will hopefully respect the request rather than blindly rejecting it as coming from a non-browser entity. -In future it can be extended to help with other relatively simple yet useful tool calls like search_web, -data/documents_store and so. +In future it can be further extended to help with other relatively simple yet useful tool calls like +data / documents_store, fetch_rss and so. - * for now search_web can be indirectly achieved using fetch_web_url_text/raw. + * for now fetch_rss can be indirectly achieved using fetch_web_url_raw. #### Extending with new tools @@ -440,6 +448,9 @@ Update the tc_switch to include a object entry for the tool, which inturn includ It should pass these along to the tools web worker, if used. * the result key (was used previously, may use in future, but for now left as is) +Look into tooljs.mjs for javascript and inturn web worker based tool calls and toolweb.mjs +for the simpleproxy.py based tool calls. + #### OLD: Mapping tool calls and responses to normal assistant - user chat flow Instead of maintaining tool_call request and resultant response in logically seperate parallel @@ -480,7 +491,7 @@ Handle reasoning/thinking responses from ai models. Handle multimodal handshaking with ai models. -Add search_web and documents|data_store tool calling, through the simpleproxy.py if and where needed. +Add fetch_rss and documents|data_store tool calling, through the simpleproxy.py if and where needed. ### Debuging the handshake From a1b33ecd1c52abd4c1e2c8365ab4a9ec30a5b65a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 26 Oct 2025 23:25:05 +0530 Subject: [PATCH 147/446] SimpleChatTC:ToolCallResponseTimeout: Allow end user to control Moved it into Me->tools, so that end user can modify the same as required from the settings ui. TODO: Currently, if tc response is got after a tool call timed out and user submitted default timed out error response, the delayed actual response when it is got may overwrite any new content in user query box, this needs to be tackled. --- tools/server/public_simplechat/readme.md | 4 ++++ tools/server/public_simplechat/simplechat.js | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 30c4a2271e..0fd69eec92 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -230,6 +230,10 @@ It is attached to the document object. Some of these can also be updated using t * searchUrl - specify the search engine's search url template along with the tag SEARCHWORDS in place where the search words should be substituted at runtime. + * toolCallResponseTimeoutMS - specifies the time (in msecs) for which the logic should wait for a tool call to respond + before a default timed out error response is generated and control given back to end user, for them to decide whether + to submit the error response or wait for actual tool call response further. + * auto - the amount of time in seconds to wait before the tool call request is auto triggered and generated response is auto submitted back. setting this value to 0 (default), disables auto logic, so that end user can review the tool calls requested by ai and if needed even modify them, before triggering/executing them as well as review and modify results generated by the tool call, before submitting them back to the ai. diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 9992ec2474..8758fa6366 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -692,7 +692,6 @@ class MultiChatUI { this.curChatId = ""; this.TimePeriods = { - ToolCallResponseTimeout: 10000, ToolCallAutoTimeUnit: 1000 } @@ -978,7 +977,7 @@ class MultiChatUI { this.timers.toolcallResponseTimeout = setTimeout(() => { this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, `Tool/Function call ${toolname} taking too much time, aborting...`); this.ui_reset_userinput(false) - }, this.TimePeriods.ToolCallResponseTimeout) + }, gMe.tools.toolCallResponseTimeoutMS) } } @@ -1086,6 +1085,11 @@ class Me { fetchProxyUrl: "http://127.0.0.1:3128", searchUrl: SearchURLS.duckduckgo, toolNames: /** @type {Array} */([]), + /** + * Control how many milliseconds to wait for tool call to respond, before generating a timed out + * error response and giving control back to end user. + */ + toolCallResponseTimeoutMS: 20000, /** * Control how many seconds to wait before auto triggering tool call or its response submission. * A value of 0 is treated as auto triggering disable. From f221a2c35689e4443d2dcf751a3ec872568aa5ff Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 27 Oct 2025 09:09:05 +0530 Subject: [PATCH 148/446] SimpleChatTC:SimpleProxy:LoadConfig ProcessArgs cleanup - initial Now both follow a similar mechanism and do the following * exit on finding any issue, so that things are in a known state from usage perspective, without any confusion/overlook * check if the cmdlineArgCmd/configCmd being processed is a known one or not. * check value of the cmd is of the expected type * have a generic flow which can accomodate more cmds in future in a simple way --- .../local.tools/simpleproxy.py | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 86f5aa0e7b..6658d26dd4 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -32,6 +32,13 @@ gMe = { 'server': None } +gConfigType = { + '--port': 'int', + '--config': 'str', + '--debug': 'bool', + '--allowed.domains': 'list' +} + class ProxyHandler(http.server.BaseHTTPRequestHandler): """ @@ -318,7 +325,18 @@ def load_config(): with open(gMe['--config']) as f: cfg = json.load(f) for k in cfg: - gMe[f"--{k}"] = cfg[k] + try: + cArg = f"--{k}" + aTypeCheck = gConfigType[cArg] + aValue = cfg[k] + aType = type(aValue).__name__ + if aType != aTypeCheck: + print(f"ERRR:LoadConfig:{k}:expected type [{aTypeCheck}] got type [{aType}]") + exit(112) + gMe[cArg] = aValue + except KeyError: + print(f"ERRR:LoadConfig:{k}:UnknownCommand") + exit(113) def process_args(args: list[str]): @@ -327,38 +345,25 @@ def process_args(args: list[str]): Helper to process command line arguments """ global gMe - gMe['INTERNAL.ProcessArgs.Malformed'] = [] - gMe['INTERNAL.ProcessArgs.Unknown'] = [] iArg = 1 while iArg < len(args): cArg = args[iArg] if (not cArg.startswith("--")): - gMe['INTERNAL.ProcessArgs.Malformed'].append(cArg) - print(f"WARN:ProcessArgs:{iArg}:IgnoringMalformedCommandOr???:{cArg}") + print(f"ERRR:ProcessArgs:{iArg}:{cArg}:MalformedCommandOr???") + exit(101) + try: + aTypeCheck = gConfigType[cArg] iArg += 1 - continue - match cArg: - case '--port': - iArg += 1 - gMe[cArg] = int(args[iArg]) - iArg += 1 - case '--config': - iArg += 1 - gMe[cArg] = args[iArg] - iArg += 1 - load_config() - case '--allowed.domains': - iArg += 1 - gMe[cArg] = ast.literal_eval(args[iArg]) - iArg += 1 - case '--debug': - iArg += 1 - gMe[cArg] = ast.literal_eval(args[iArg]) - iArg += 1 - case _: - gMe['INTERNAL.ProcessArgs.Unknown'].append(cArg) - print(f"WARN:ProcessArgs:{iArg}:IgnoringUnknownCommand:{cArg}") - iArg += 1 + aValue = ast.literal_eval(args[iArg]) + aType = type(aValue).__name__ + if aType != aTypeCheck: + print(f"ERRR:ProcessArgs:{iArg}:{cArg}:expected type [{aTypeCheck}] got type [{aType}]") + exit(102) + gMe[cArg] = aValue + iArg += 1 + except KeyError: + print(f"ERRR:ProcessArgs:{iArg}:{cArg}:UnknownCommand") + exit(103) print(gMe) From 0caa2e81011a8c1208bfdc4e5fcafbd0e74f3ee6 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 27 Oct 2025 09:43:49 +0530 Subject: [PATCH 149/446] SimpleChatTC:SimpleProxy: Prg Parameters handling cleanup - next Ensure load_config gets called on encountering --config in cmdline, so that the user has control over whether cmdline or config file will decide the final value of any given parameter. Ensure that str type values in cmdline are picked up directly, without running them through ast.literal_eval, bcas otherwise one will have to ensure throught the cmdline arg mechanism that string quote is retained for literal_eval Have the """ function note/description below def line immidiately so that it is interpreted as a function description. --- .../local.tools/simpleproxy.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 6658d26dd4..4ebaf83182 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -325,6 +325,7 @@ def load_config(): with open(gMe['--config']) as f: cfg = json.load(f) for k in cfg: + print(f"DBUG:LoadConfig:{k}") try: cArg = f"--{k}" aTypeCheck = gConfigType[cArg] @@ -340,10 +341,17 @@ def load_config(): def process_args(args: list[str]): + """ + Helper to process command line arguments. + + Flow setup below such that + * location of --config in commandline will decide whether command line or config file will get + priority wrt setting program parameters. + * str type values in cmdline are picked up directly, without running them through ast.literal_eval, + bcas otherwise one will have to ensure throught the cmdline arg mechanism that string quote is + retained for literal_eval + """ import ast - """ - Helper to process command line arguments - """ global gMe iArg = 1 while iArg < len(args): @@ -351,16 +359,20 @@ def process_args(args: list[str]): if (not cArg.startswith("--")): print(f"ERRR:ProcessArgs:{iArg}:{cArg}:MalformedCommandOr???") exit(101) + print(f"DBUG:ProcessArgs:{iArg}:{cArg}") try: aTypeCheck = gConfigType[cArg] - iArg += 1 - aValue = ast.literal_eval(args[iArg]) - aType = type(aValue).__name__ - if aType != aTypeCheck: - print(f"ERRR:ProcessArgs:{iArg}:{cArg}:expected type [{aTypeCheck}] got type [{aType}]") - exit(102) + aValue = args[iArg+1] + if aTypeCheck != 'str': + aValue = ast.literal_eval(aValue) + aType = type(aValue).__name__ + if aType != aTypeCheck: + print(f"ERRR:ProcessArgs:{iArg}:{cArg}:expected type [{aTypeCheck}] got type [{aType}]") + exit(102) gMe[cArg] = aValue - iArg += 1 + iArg += 2 + if cArg == '--config': + load_config() except KeyError: print(f"ERRR:ProcessArgs:{iArg}:{cArg}:UnknownCommand") exit(103) From 3f1fd289eb60062f9e553931176cc9e99395e34e Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 27 Oct 2025 10:32:19 +0530 Subject: [PATCH 150/446] SimpleChatTC:SimpleProxy:BearerInsecure a needed config Add a config entry called bearer.insecure which will contain a token used for bearer auth of http requests Make bearer.insecure and allowed.domains as needed configs, and exit program if they arent got through cmdline or config file. --- .../public_simplechat/local.tools/simpleproxy.json | 3 ++- .../server/public_simplechat/local.tools/simpleproxy.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index d68878199a..a4ea4305f0 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -12,5 +12,6 @@ "^duckduckgo\\.com$", ".*\\.google\\.com$", "^google\\.com$" - ] + ], + "bearer.insecure": "NeverSecure" } diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 4ebaf83182..f2d5b52722 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -36,9 +36,12 @@ gConfigType = { '--port': 'int', '--config': 'str', '--debug': 'bool', - '--allowed.domains': 'list' + '--allowed.domains': 'list', + '--bearer.insecure': 'str' } +gConfigNeeded = [ '--allowed.domains', '--bearer.insecure' ] + class ProxyHandler(http.server.BaseHTTPRequestHandler): """ @@ -377,6 +380,10 @@ def process_args(args: list[str]): print(f"ERRR:ProcessArgs:{iArg}:{cArg}:UnknownCommand") exit(103) print(gMe) + for k in gConfigNeeded: + if gMe.get(k) == None: + print(f"ERRR:ProcessArgs:{k}:missing, did you forget to pass the config file...") + exit(104) def run(): From 6d08cda9c83bd15c22ed3aa12bb6d4bd947b9bf4 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 27 Oct 2025 11:00:16 +0530 Subject: [PATCH 151/446] SimpleChatTC:SimpleProxy: Check for bearer authorization As noted in the comments in code, this is a very insecure flow for now. --- .../local.tools/simpleproxy.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index f2d5b52722..67b2741542 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -13,6 +13,9 @@ # * any request to aum path is used to respond with a predefined text response # which can help identify this server, in a simple way. # +# Expects a Bearer authorization line in the http header of the requests got. +# HOWEVER DO KEEP IN MIND THAT ITS A VERY INSECURE IMPLEMENTATION, AT BEST +# import sys @@ -67,12 +70,33 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): self.send_response(code, message) self.send_headers_common() + def auth_check(self): + """ + Simple Bearer authorization + ALERT: For multiple reasons, this is a very insecure implementation. + """ + authline = self.headers['Authorization'] + if authline == None: + return { 'AllOk': False, 'Msg': "No auth line" } + authlineA = authline.strip().split(' ') + if len(authlineA) != 2: + return { 'AllOk': False, 'Msg': "Invalid auth line" } + if authlineA[0] != 'Bearer': + return { 'AllOk': False, 'Msg': "Invalid auth type" } + if authlineA[1] != gMe['--bearer.insecure']: + return { 'AllOk': False, 'Msg': "Invalid auth" } + return { 'AllOk': True, 'Msg': "Auth Ok" } + def do_GET(self): """ Handle GET requests """ print(f"\n\n\nDBUG:ProxyHandler:GET:{self.address_string()}:{self.path}") print(f"DBUG:PH:Get:Headers:{self.headers}") + acGot = self.auth_check() + if not acGot['AllOk']: + self.send_error(400, f"WARN:{acGot['Msg']}") + return pr = urllib.parse.urlparse(self.path) print(f"DBUG:ProxyHandler:GET:{pr}") match pr.path: From 044d1cf53511efb26f80cad2115873f4f4a0430f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 27 Oct 2025 11:10:08 +0530 Subject: [PATCH 152/446] SimpleChatTC:tools.proxyUrl: rename to just proxyUrl Next will be adding a proxyAuth field also to tools. --- tools/server/public_simplechat/readme.md | 4 ++-- tools/server/public_simplechat/simplechat.js | 2 +- tools/server/public_simplechat/toolweb.mjs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 0fd69eec92..973ef77a76 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -226,7 +226,7 @@ It is attached to the document object. Some of these can also be updated using t remember to enable this only for GenAi/LLM models which support tool/function calling. - * fetchProxyUrl - specify the address for the running instance of bundled local.tools/simpleproxy.py + * proxyUrl - specify the address for the running instance of bundled local.tools/simpleproxy.py * searchUrl - specify the search engine's search url template along with the tag SEARCHWORDS in place where the search words should be substituted at runtime. @@ -411,7 +411,7 @@ manner by dropping head block as well as all scripts/styles/footers/headers/nav dropping the html tags. The client ui logic does a simple check to see if the bundled simpleproxy is running at specified -fetchProxyUrl before enabling these web and related tool calls. +proxyUrl before enabling these web and related tool calls. The bundled simple proxy diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 8758fa6366..1b61b8451a 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1082,7 +1082,7 @@ class Me { this.multiChat = new MultiChatUI(); this.tools = { enabled: false, - fetchProxyUrl: "http://127.0.0.1:3128", + proxyUrl: "http://127.0.0.1:3128", searchUrl: SearchURLS.duckduckgo, toolNames: /** @type {Array} */([]), /** diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 11f4cd6b3f..7e71961cf1 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -45,7 +45,7 @@ function get_gme() { */ function proxyserver_get_1arg(toolcallid, toolname, obj, path, qkey, qvalue) { if (gToolsWorker.onmessage != null) { - let newUrl = `${get_gme().tools.fetchProxyUrl}/${path}?${qkey}=${qvalue}` + let newUrl = `${get_gme().tools.proxyUrl}/${path}?${qkey}=${qvalue}` fetch(newUrl).then(resp => { if (!resp.ok) { throw new Error(`${resp.status}:${resp.statusText}`); @@ -70,7 +70,7 @@ function proxyserver_get_1arg(toolcallid, toolname, obj, path, qkey, qvalue) { * @param {Object>} tcs */ async function proxyserver_tc_setup(tag, tcPath, tcName, tcsData, tcs) { - await fetch(`${get_gme().tools.fetchProxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ + await fetch(`${get_gme().tools.proxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ if (resp.statusText != 'bharatavarshe') { console.log(`WARN:ToolWeb:${tag}:Dont forget to run the bundled local.tools/simpleproxy.py to enable me`) return From 0552ff909875d663d6e1bb5d18b022b7be0c0d84 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 27 Oct 2025 11:29:13 +0530 Subject: [PATCH 153/446] SimpleChatTC:SimpleProxy:ClientUI: Send Authorization bearer User can configure the bearer token to send --- tools/server/public_simplechat/simplechat.js | 1 + tools/server/public_simplechat/toolweb.mjs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 1b61b8451a..21ebb008f8 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1083,6 +1083,7 @@ class Me { this.tools = { enabled: false, proxyUrl: "http://127.0.0.1:3128", + proxyAuthInsecure: "NeverSecure", searchUrl: SearchURLS.duckduckgo, toolNames: /** @type {Array} */([]), /** diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 7e71961cf1..0808c6d0b3 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -46,7 +46,7 @@ function get_gme() { function proxyserver_get_1arg(toolcallid, toolname, obj, path, qkey, qvalue) { if (gToolsWorker.onmessage != null) { let newUrl = `${get_gme().tools.proxyUrl}/${path}?${qkey}=${qvalue}` - fetch(newUrl).then(resp => { + fetch(newUrl, { headers: { 'Authorization': `Bearer ${get_gme().tools.proxyAuthInsecure}` }}).then(resp => { if (!resp.ok) { throw new Error(`${resp.status}:${resp.statusText}`); } @@ -70,7 +70,9 @@ function proxyserver_get_1arg(toolcallid, toolname, obj, path, qkey, qvalue) { * @param {Object>} tcs */ async function proxyserver_tc_setup(tag, tcPath, tcName, tcsData, tcs) { - await fetch(`${get_gme().tools.proxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ + await fetch(`${get_gme().tools.proxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`, { + headers: { 'Authorization': `Bearer ${get_gme().tools.proxyAuthInsecure}` } + }).then(resp=>{ if (resp.statusText != 'bharatavarshe') { console.log(`WARN:ToolWeb:${tag}:Dont forget to run the bundled local.tools/simpleproxy.py to enable me`) return From 84403973cdf0f22eb231c880c0b4a1521a564eb7 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 27 Oct 2025 15:44:17 +0530 Subject: [PATCH 154/446] SimpleChatTC:SimpleProxy: once in a bluemoon transformed bearer instead of using the shared bearer token as is, hash it with current year and use the hash. keep /aum path out of auth check. in future bearer token could be transformed more often, as well as with additional nounce/dynamic token from server got during initial /aum handshake as also running counter and so ... NOTE: All these circus not good enough, given that currently the simpleproxy.py handshakes work over http. However these skeletons put in place, for future, if needed. TODO: There is a once in a bluemoon race when the year transitions between client generating the request and server handling the req. But other wise year transitions dont matter bcas client always creates fresh token, and server checks for year change to genrate fresh token if required. --- .../local.tools/simpleproxy.py | 36 +++++++++++++++---- tools/server/public_simplechat/readme.md | 14 ++++++-- tools/server/public_simplechat/toolweb.mjs | 16 ++++++--- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 67b2741542..8b17d012c0 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -32,6 +32,7 @@ gMe = { '--port': 3128, '--config': '/dev/null', '--debug': False, + 'bearer.transformed.year': "", 'server': None } @@ -46,6 +47,22 @@ gConfigType = { gConfigNeeded = [ '--allowed.domains', '--bearer.insecure' ] +def bearer_transform(): + """ + Transform the raw bearer token to the network handshaked token, + if and when needed. + """ + global gMe + year = str(time.gmtime().tm_year) + if gMe['bearer.transformed.year'] == year: + return + import hashlib + s256 = hashlib.sha256(year.encode('utf-8')) + s256.update(gMe['--bearer.insecure'].encode('utf-8')) + gMe['--bearer.transformed'] = s256.hexdigest() + gMe['bearer.transformed.year'] = year + + class ProxyHandler(http.server.BaseHTTPRequestHandler): """ Implements the logic for handling requests sent to this server. @@ -75,6 +92,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): Simple Bearer authorization ALERT: For multiple reasons, this is a very insecure implementation. """ + bearer_transform() authline = self.headers['Authorization'] if authline == None: return { 'AllOk': False, 'Msg': "No auth line" } @@ -83,7 +101,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): return { 'AllOk': False, 'Msg': "Invalid auth line" } if authlineA[0] != 'Bearer': return { 'AllOk': False, 'Msg': "Invalid auth type" } - if authlineA[1] != gMe['--bearer.insecure']: + if authlineA[1] != gMe['--bearer.transformed']: return { 'AllOk': False, 'Msg': "Invalid auth" } return { 'AllOk': True, 'Msg': "Auth Ok" } @@ -93,17 +111,21 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): """ print(f"\n\n\nDBUG:ProxyHandler:GET:{self.address_string()}:{self.path}") print(f"DBUG:PH:Get:Headers:{self.headers}") - acGot = self.auth_check() - if not acGot['AllOk']: - self.send_error(400, f"WARN:{acGot['Msg']}") - return pr = urllib.parse.urlparse(self.path) print(f"DBUG:ProxyHandler:GET:{pr}") match pr.path: case '/urlraw': - handle_urlraw(self, pr) + acGot = self.auth_check() + if not acGot['AllOk']: + self.send_error(400, f"WARN:{acGot['Msg']}") + else: + handle_urlraw(self, pr) case '/urltext': - handle_urltext(self, pr) + acGot = self.auth_check() + if not acGot['AllOk']: + self.send_error(400, f"WARN:{acGot['Msg']}") + else: + handle_urltext(self, pr) case '/aum': handle_aum(self, pr) case _: diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 973ef77a76..25089be4bc 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -89,7 +89,12 @@ remember to content like head, scripts, styles, headers, footers, ... Be careful when accessing web through this and use it only with known safe sites. - * it allows one to specify a white list of allowed.domains, look into local.tools/simpleproxy.json + * look into local.tools/simpleproxy.json for specifying + + * the white list of allowed.domains + * the shared bearer token between server and client ui + + ### using the front end @@ -228,6 +233,10 @@ It is attached to the document object. Some of these can also be updated using t * proxyUrl - specify the address for the running instance of bundled local.tools/simpleproxy.py + * proxyAuthInsecure - shared token between simpleproxy.py server and client ui, for accessing service provided by it. + + Shared token is currently hashed with the current year and inturn handshaked over the network. In future if required one could also include a dynamic token provided by simpleproxy server during /aum handshake and running counter or so into hashed token. ALERT: However do remember that currently the handshake occurs over http and not https, so others can snoop the network and get token. Per client ui running counter and random dynamic token can help mitigate things to some extent, if required in future. + * searchUrl - specify the search engine's search url template along with the tag SEARCHWORDS in place where the search words should be substituted at runtime. * toolCallResponseTimeoutMS - specifies the time (in msecs) for which the logic should wait for a tool call to respond @@ -419,7 +428,8 @@ The bundled simple proxy * tools/server/public_simplechat/local.tools/simpleproxy.py * it provides for a basic white list of allowed domains to access, to be specified by the end user. - This should help limit web access to a safe set of sites determined by the end user. + This should help limit web access to a safe set of sites determined by the end user. There is also + a provision for shared bearer token to be specified by the end user. * it tries to mimic the client/browser making the request to it by propogating header entries like user-agent, accept and accept-language from the got request to the generated request during proxying diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 0808c6d0b3..909fbdae7e 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -27,6 +27,13 @@ function get_gme() { } +function bearer_transform() { + let data = `${new Date().getUTCFullYear()}${get_gme().tools.proxyAuthInsecure}` + return crypto.subtle.digest('sha-256', new TextEncoder().encode(data)).then(ab=>{ + return Array.from(new Uint8Array(ab)).map(b=>b.toString(16).padStart(2,'0')).join('') + }) +} + /** * Helper http get logic wrt the bundled SimpleProxy server, * which helps execute a given proxy dependent tool call. @@ -43,10 +50,11 @@ function get_gme() { * @param {string} qkey * @param {string} qvalue */ -function proxyserver_get_1arg(toolcallid, toolname, obj, path, qkey, qvalue) { +async function proxyserver_get_1arg(toolcallid, toolname, obj, path, qkey, qvalue) { if (gToolsWorker.onmessage != null) { let newUrl = `${get_gme().tools.proxyUrl}/${path}?${qkey}=${qvalue}` - fetch(newUrl, { headers: { 'Authorization': `Bearer ${get_gme().tools.proxyAuthInsecure}` }}).then(resp => { + let btoken = await bearer_transform() + fetch(newUrl, { headers: { 'Authorization': `Bearer ${btoken}` }}).then(resp => { if (!resp.ok) { throw new Error(`${resp.status}:${resp.statusText}`); } @@ -70,9 +78,7 @@ function proxyserver_get_1arg(toolcallid, toolname, obj, path, qkey, qvalue) { * @param {Object>} tcs */ async function proxyserver_tc_setup(tag, tcPath, tcName, tcsData, tcs) { - await fetch(`${get_gme().tools.proxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`, { - headers: { 'Authorization': `Bearer ${get_gme().tools.proxyAuthInsecure}` } - }).then(resp=>{ + await fetch(`${get_gme().tools.proxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ if (resp.statusText != 'bharatavarshe') { console.log(`WARN:ToolWeb:${tag}:Dont forget to run the bundled local.tools/simpleproxy.py to enable me`) return From e79faebde139e219bc9d4de2401e08aac0d9e6c3 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 00:23:04 +0530 Subject: [PATCH 155/446] SimpleChatTC:ToolTemp and ChatShow Add a new role ToolTemp, which is used to maintain any tool call response on the client ui side, without submitting it to the server ie till user or auto submit triggers the submitting of that tool call response. When ever a tool call response is got, create a ToolTemp role based message in the corresponding chat session. And dont directly update the user query input area, rather leave it to the updated simplechat show and the new multichatui chat_show helper and inturn whether the current chat session active in ui is same as the one for which the tool call response has been recieved. TODO: Currently the response message is added to the current active chat session, but this needs to be changed by tracking chatId/session through the full tool call cycle and then adding the tool call response in the related chat session, and inturn updating or not the ui based on whether that chat session is still the active chat session in ui or not, given that tool call gets handled in a asynchronous way. Now when that tool call response is submitted, promote the equiv tool temp role based message that should be in the session's chat history as the last message into becoming a normal tool response message. SimpleChat.show has been updated to take care of showing any ToolTemp role message in the user query input area. A newer chat_show helper added to MultiChatUI, that takes care of calling SimpleChat.show, provided the chat_show is being requested for the currently active in ui, chat session. As well as to take care of passing both the ChatDiv and elInUser. Converts users of SimpleChat.show to use MultiChatUI.chat_show --- tools/server/public_simplechat/simplechat.js | 82 ++++++++++++++++---- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 21ebb008f8..f68cc12f60 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -12,6 +12,7 @@ class Roles { static User = "user"; static Assistant = "assistant"; static Tool = "tool"; + static ToolTemp = "TOOL.TEMP"; } class ApiEP { @@ -319,7 +320,7 @@ class SimpleChat { this.iLastSys = ods.iLastSys; this.xchat = []; for (const cur of ods.xchat) { - if (cur.ns == undefined) { + if (cur.ns == undefined) { // this relates to the old on-disk-structure/format, needs to be removed later /** @typedef {{role: string, content: string}} OldChatMessage */ let tcur = /** @type {OldChatMessage} */(/** @type {unknown} */(cur)); this.xchat.push(new ChatMessageEx(tcur.role, tcur.content)) @@ -383,6 +384,12 @@ class SimpleChat { let xchat = this.recent_chat(iRecentUserMsgCnt); let chat = []; for (const msg of xchat) { + if (msg.ns.role == Roles.ToolTemp) { + // Skip (temp) tool response which has not yet been accepted by user + // In future need to check that it is the last message + // and not something in between, which shouldnt occur normally. + continue + } let tmsg = ChatMessageEx.newFrom(msg); if (!tmsg.has_toolcall()) { tmsg.ns_delete("tool_calls") @@ -413,25 +420,55 @@ class SimpleChat { return true; } + /** + * Check if the last message in the chat history is a ToolTemp role based one. + * If so, then + * * update that to a regular Tool role based message. + * * also update the content of that message to what is passed. + * @param {string} content + */ + promote_tooltemp(content) { + let lastIndex = this.xchat.length - 1; + if (lastIndex < 0) { + console.error("DBUG:SimpleChat:PromoteToolTemp:No chat messages including ToolTemp") + return + } + if (this.xchat[lastIndex].ns.role != Roles.ToolTemp) { + console.error("DBUG:SimpleChat:PromoteToolTemp:LastChatMsg not ToolTemp") + return + } + this.xchat[lastIndex].ns.role = Roles.Tool; + this.xchat[lastIndex].ns.content = content; + } + /** * Show the chat contents in the specified div. + * Also update the user query input box, with ToolTemp role message, if any. + * * If requested to clear prev stuff and inturn no chat content then show * * usage info * * option to load prev saved chat if any * * as well as settings/info. * @param {HTMLDivElement} div + * @param {HTMLInputElement} elInUser * @param {boolean} bClear * @param {boolean} bShowInfoAll */ - show(div, bClear=true, bShowInfoAll=false) { + show(div, elInUser, bClear=true, bShowInfoAll=false) { if (bClear) { div.replaceChildren(); } let last = undefined; for(const x of this.recent_chat(gMe.chatProps.iRecentUserMsgCnt)) { - let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, div); - entry.className = `role-${x.ns.role}`; - last = entry; + if (x.ns.role != Roles.ToolTemp) { + let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, div); + entry.className = `role-${x.ns.role}`; + last = entry; + } else { + if (elInUser) { + elInUser.value = x.ns.content; + } + } } if (last !== undefined) { last.scrollIntoView(false); @@ -792,6 +829,20 @@ class MultiChatUI { this.elInUser.focus(); } + /** + * Refresh UI wrt given chatId, provided it matches the currently selected chatId + * @param {string} chatId + * @param {boolean} bClear + * @param {boolean} bShowInfoAll + */ + chat_show(chatId, bClear=true, bShowInfoAll=false) { + if (chatId != this.curChatId) { + return + } + let chat = this.simpleChats[this.curChatId]; + chat.show(this.elDivChat, this.elInUser, bClear, bShowInfoAll) + } + /** * Setup the needed callbacks wrt UI, curChatId to defaultChatId and * optionally switch to specified defaultChatId. @@ -839,7 +890,12 @@ class MultiChatUI { tools.setup((id, name, data)=>{ clearTimeout(this.timers.toolcallResponseTimeout) this.timers.toolcallResponseTimeout = undefined - this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(id, name, data); + // TODO: Check for chat id in future so as to + // identify the right chat session to add the tc response to + // as well as to decide whether to show this chat currently or not and same with auto submit + let chat = this.simpleChats[this.curChatId]; // rather we should pick chat based on tool response's chatId + chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(id, name, data))) + this.chat_show(chat.chatId) // one needs to use tool response's chatId this.ui_reset_userinput(false) if (gMe.tools.auto > 0) { this.timers.toolcallResponseSubmitClick = setTimeout(()=>{ @@ -867,7 +923,7 @@ class MultiChatUI { this.elInSystem.value = value.substring(0,value.length-1); let chat = this.simpleChats[this.curChatId]; chat.add_system_anytime(this.elInSystem.value, this.curChatId); - chat.show(this.elDivChat); + this.chat_show(chat.chatId) ev.preventDefault(); } }); @@ -922,11 +978,11 @@ class MultiChatUI { return; } if (content.startsWith("")) { - chat.add(new ChatMessageEx(Roles.Tool, content)) + chat.promote_tooltemp(content) } else { chat.add(new ChatMessageEx(Roles.User, content)) } - chat.show(this.elDivChat); + this.chat_show(chat.chatId); let theUrl = ApiEP.Url(gMe.baseURL, apiEP); let theBody = chat.request_jsonstr(apiEP); @@ -943,7 +999,7 @@ class MultiChatUI { let theResp = await chat.handle_response(resp, apiEP, this.elDivChat); if (chatId == this.curChatId) { - chat.show(this.elDivChat); + this.chat_show(chatId); if (theResp.trimmedContent.length > 0) { let p = ui.el_create_append_p(`TRIMMED:${theResp.trimmedContent}`, this.elDivChat); p.className="role-trim"; @@ -1053,9 +1109,9 @@ class MultiChatUI { } this.elInSystem.value = chat.get_system_latest().ns.content; this.elInUser.value = ""; - chat.show(this.elDivChat, true, true); - this.elInUser.focus(); this.curChatId = chatId; + this.chat_show(chatId, true, true); + this.elInUser.focus(); console.log(`INFO:SimpleChat:MCUI:HandleSessionSwitch:${chatId} entered...`); } @@ -1159,7 +1215,7 @@ class Me { console.log("DBUG:SimpleChat:SC:Load", chat); chat.load(); queueMicrotask(()=>{ - chat.show(div, true, true); + chat.show(div, this.multiChat.elInUser, true, true); this.multiChat.elInSystem.value = chat.get_system_latest().ns.content; }); }); From 4eb3322017ccd45f320cb87dacc7d96e69f95748 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 01:43:52 +0530 Subject: [PATCH 156/446] SimpleChatTC:ToolCallErrPath:ToolTemp and MultiChatUIChatShow Update the immidiate tool call triggering failure and tool call response timeout paths to use the new ToolTemp and MultiChatUI based chat show logics. Actual tool call itself generating errors, is already handled in the previous commit changes. --- tools/server/public_simplechat/simplechat.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index f68cc12f60..ccab26c393 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1027,11 +1027,13 @@ class MultiChatUI { } let toolResult = await chat.handle_toolcall(toolCallId, toolname, this.elInToolArgs.value) if (toolResult !== undefined) { - this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, toolResult); + chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, toolResult))) + this.chat_show(chat.chatId) this.ui_reset_userinput(false) } else { this.timers.toolcallResponseTimeout = setTimeout(() => { - this.elInUser.value = ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, `Tool/Function call ${toolname} taking too much time, aborting...`); + chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, `Tool/Function call ${toolname} taking too much time, aborting...`))) + this.chat_show(chat.chatId) this.ui_reset_userinput(false) }, gMe.tools.toolCallResponseTimeoutMS) } From 13d312fe0d2cd5092917dca458d8fb7b7b7f61b1 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 02:17:15 +0530 Subject: [PATCH 157/446] SimpleChatTC:ToolTemp: Ensure add removes non promoted ToolTemp --- tools/server/public_simplechat/simplechat.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index ccab26c393..b7e8af1603 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -407,11 +407,21 @@ class SimpleChat { /** * Add an entry into xchat. + * If the last message in chat history is a ToolTemp message, discard it + * as the runtime logic is asking for adding new message instead of promoting the tooltemp message. + * * NOTE: A new copy is created and added into xchat. * Also update iLastSys system prompt index tracker * @param {ChatMessageEx} chatMsg */ add(chatMsg) { + if (this.xchat.length > 0) { + let lastIndex = this.xchat.length - 1; + if (this.xchat[lastIndex].ns.role == Roles.ToolTemp) { + console.debug("DBUG:SimpleChat:Add:Discarding prev ToolTemp message...") + this.xchat.pop() + } + } this.xchat.push(ChatMessageEx.newFrom(chatMsg)); if (chatMsg.ns.role == Roles.System) { this.iLastSys = this.xchat.length - 1; From 734beb08f56c4218abb046e261ec9dfd9e0f0d8a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 02:37:34 +0530 Subject: [PATCH 158/446] SimpleChatTC:ChatSessionID through the tool call cycle Pass chatId to tool call, and use chatId in got tool call resp, to decide as to to which chat session the async tool call resp belongs and inturn if auto submit timer should be started if auto is enabled. --- tools/server/public_simplechat/simplechat.js | 27 ++++++++++---------- tools/server/public_simplechat/tools.mjs | 9 ++++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index b7e8af1603..0c074561e6 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -721,7 +721,7 @@ class SimpleChat { return "Tool/Function call name not specified" } try { - return await tools.tool_call(toolcallid, toolname, toolargs) + return await tools.tool_call(this.chatId, toolcallid, toolname, toolargs) } catch (/** @type {any} */error) { return `Tool/Function call raised an exception:${error.name}:${error.message}` } @@ -847,10 +847,11 @@ class MultiChatUI { */ chat_show(chatId, bClear=true, bShowInfoAll=false) { if (chatId != this.curChatId) { - return + return false } let chat = this.simpleChats[this.curChatId]; chat.show(this.elDivChat, this.elInUser, bClear, bShowInfoAll) + return true } /** @@ -897,21 +898,19 @@ class MultiChatUI { }) // Handle messages from Tools web worker - tools.setup((id, name, data)=>{ + tools.setup((cid, tcid, name, data)=>{ clearTimeout(this.timers.toolcallResponseTimeout) this.timers.toolcallResponseTimeout = undefined - // TODO: Check for chat id in future so as to - // identify the right chat session to add the tc response to - // as well as to decide whether to show this chat currently or not and same with auto submit - let chat = this.simpleChats[this.curChatId]; // rather we should pick chat based on tool response's chatId - chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(id, name, data))) - this.chat_show(chat.chatId) // one needs to use tool response's chatId - this.ui_reset_userinput(false) - if (gMe.tools.auto > 0) { - this.timers.toolcallResponseSubmitClick = setTimeout(()=>{ - this.elBtnUser.click() - }, gMe.tools.auto*this.TimePeriods.ToolCallAutoTimeUnit) + let chat = this.simpleChats[cid]; + chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(tcid, name, data))) + if (this.chat_show(cid)) { + if (gMe.tools.auto > 0) { + this.timers.toolcallResponseSubmitClick = setTimeout(()=>{ + this.elBtnUser.click() + }, gMe.tools.auto*this.TimePeriods.ToolCallAutoTimeUnit) + } } + this.ui_reset_userinput(false) }) this.elInUser.addEventListener("keyup", (ev)=> { diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 23eb7e35e8..73a79f460c 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -49,11 +49,11 @@ export function meta() { /** * Setup the callback that will be called when ever message * is recieved from the Tools Web Worker. - * @param {(id: string, name: string, data: string) => void} cb + * @param {(chatId: string, toolCallId: string, name: string, data: string) => void} cb */ export function setup(cb) { gToolsWorker.onmessage = function (ev) { - cb(ev.data.id, ev.data.name, ev.data.data) + cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) } } @@ -62,15 +62,16 @@ export function setup(cb) { * Try call the specified tool/function call. * Returns undefined, if the call was placed successfully * Else some appropriate error message will be returned. + * @param {string} chatid * @param {string} toolcallid * @param {string} toolname * @param {string} toolargs */ -export async function tool_call(toolcallid, toolname, toolargs) { +export async function tool_call(chatid, toolcallid, toolname, toolargs) { for (const fn in tc_switch) { if (fn == toolname) { try { - tc_switch[fn]["handler"](toolcallid, fn, JSON.parse(toolargs)) + tc_switch[fn]["handler"](chatid, toolcallid, fn, JSON.parse(toolargs)) return undefined } catch (/** @type {any} */error) { return `Tool/Function call raised an exception:${error.name}:${error.message}` From 25df32b5533cec44ac8d7bf285249c140f9f8267 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 02:50:28 +0530 Subject: [PATCH 159/446] SimpleChatTC:ChatSessionID: Get all handlers to account for chatid This should ensure that tool call responses can be mapped back to the chat session for which it was triggered. --- tools/server/public_simplechat/readme.md | 2 +- tools/server/public_simplechat/tooljs.mjs | 10 +++++---- .../server/public_simplechat/toolsworker.mjs | 2 +- tools/server/public_simplechat/toolweb.mjs | 22 +++++++++++-------- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 25089be4bc..b8afbb9dd3 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -458,7 +458,7 @@ Provide a handler which Update the tc_switch to include a object entry for the tool, which inturn includes * the meta data wrt the tool call -* a reference to the handler - the handler should take toolCallId, toolName and toolArgs. +* a reference to the handler - handler should take chatSessionId, toolCallId, toolName and toolArgs. It should pass these along to the tools web worker, if used. * the result key (was used previously, may use in future, but for now left as is) diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index 0e9ce61c3e..a30330ab82 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -32,12 +32,13 @@ let js_meta = { /** * Implementation of the javascript interpretor logic. Minimal skeleton for now. * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} chatid * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function js_run(toolcallid, toolname, obj) { - gToolsWorker.postMessage({ id: toolcallid, name: toolname, code: obj["code"]}) +function js_run(chatid, toolcallid, toolname, obj) { + gToolsWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: obj["code"]}) } @@ -63,12 +64,13 @@ let calc_meta = { /** * Implementation of the simple calculator logic. Minimal skeleton for now. * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} chatid * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function calc_run(toolcallid, toolname, obj) { - gToolsWorker.postMessage({ id: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) +function calc_run(chatid, toolcallid, toolname, obj) { + gToolsWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) } diff --git a/tools/server/public_simplechat/toolsworker.mjs b/tools/server/public_simplechat/toolsworker.mjs index b85b83b33b..15675a8df8 100644 --- a/tools/server/public_simplechat/toolsworker.mjs +++ b/tools/server/public_simplechat/toolsworker.mjs @@ -23,6 +23,6 @@ self.onmessage = async function (ev) { console.log(`\n\nTool/Function call "${ev.data.name}" raised an exception:${error.name}:${error.message}\n\n`) } tconsole.console_revert() - self.postMessage({ id: ev.data.id, name: ev.data.name, data: tconsole.gConsoleStr}) + self.postMessage({ cid: ev.data.cid, tcid: ev.data.tcid, name: ev.data.name, data: tconsole.gConsoleStr}) console.info("DBUG:WW:OnMessage done") } diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 909fbdae7e..eeecf846b0 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -43,6 +43,7 @@ function bearer_transform() { * * with a predefined query token and value wrt a predefined path * NOTE: Initial go, handles textual data type. * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid * @param {string} toolcallid * @param {string} toolname * @param {any} obj @@ -50,7 +51,7 @@ function bearer_transform() { * @param {string} qkey * @param {string} qvalue */ -async function proxyserver_get_1arg(toolcallid, toolname, obj, path, qkey, qvalue) { +async function proxyserver_get_1arg(chatid, toolcallid, toolname, obj, path, qkey, qvalue) { if (gToolsWorker.onmessage != null) { let newUrl = `${get_gme().tools.proxyUrl}/${path}?${qkey}=${qvalue}` let btoken = await bearer_transform() @@ -60,9 +61,9 @@ async function proxyserver_get_1arg(toolcallid, toolname, obj, path, qkey, qvalu } return resp.text() }).then(data => { - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: data}})) + message_toolsworker(new MessageEvent('message', {data: {cid: chatid, tcid: toolcallid, name: toolname, data: data}})) }).catch((err)=>{ - message_toolsworker(new MessageEvent('message', {data: {id: toolcallid, name: toolname, data: `Error:${err}`}})) + message_toolsworker(new MessageEvent('message', {data: {cid: chatid, tcid: toolcallid, name: toolname, data: `Error:${err}`}})) }) } } @@ -122,12 +123,13 @@ let fetchweburlraw_meta = { * * with a query token named url wrt the path urlraw * which gives the actual url to fetch * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function fetchweburlraw_run(toolcallid, toolname, obj) { - return proxyserver_get_1arg(toolcallid, toolname, obj, 'urlraw', 'url', encodeURIComponent(obj.url)); +function fetchweburlraw_run(chatid, toolcallid, toolname, obj) { + return proxyserver_get_1arg(chatid, toolcallid, toolname, obj, 'urlraw', 'url', encodeURIComponent(obj.url)); } @@ -179,12 +181,13 @@ let fetchweburltext_meta = { * * strips out head as well as any script, style, header, footer, nav and so blocks in body * before returning remaining body contents. * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function fetchweburltext_run(toolcallid, toolname, obj) { - return proxyserver_get_1arg(toolcallid, toolname, obj, 'urltext', 'url', encodeURIComponent(obj.url)); +function fetchweburltext_run(chatid, toolcallid, toolname, obj) { + return proxyserver_get_1arg(chatid, toolcallid, toolname, obj, 'urltext', 'url', encodeURIComponent(obj.url)); } @@ -237,16 +240,17 @@ let searchwebtext_meta = { * * strips out head as well as any script, style, header, footer, nav and so blocks in body * before returning remaining body contents. * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function searchwebtext_run(toolcallid, toolname, obj) { +function searchwebtext_run(chatid, toolcallid, toolname, obj) { if (gToolsWorker.onmessage != null) { /** @type {string} */ let searchUrl = get_gme().tools.searchUrl; searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)); - return proxyserver_get_1arg(toolcallid, toolname, obj, 'urltext', 'url', encodeURIComponent(searchUrl)); + return proxyserver_get_1arg(chatid, toolcallid, toolname, obj, 'urltext', 'url', encodeURIComponent(searchUrl)); } } From dbb5512b206e575275f48379f9d711a71f5e2cd9 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 03:34:02 +0530 Subject: [PATCH 160/446] SimpleChatTC:Reasoning: Initial Go --- tools/server/public_simplechat/simplechat.js | 40 +++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 0c074561e6..e9cf7d0591 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -45,7 +45,7 @@ class ApiEP { */ /** - * @typedef {{role: string, content: string, tool_calls: Array}} NSChatMessage + * @typedef {{role: string, content: string, reasoning_content: string, tool_calls: Array}} NSChatMessage */ class ChatMessageEx { @@ -54,12 +54,13 @@ class ChatMessageEx { * Represent a Message in the Chat * @param {string} role * @param {string} content + * @param {string} reasoning_content * @param {Array} tool_calls * @param {string} trimmedContent */ - constructor(role = "", content="", tool_calls=[], trimmedContent="") { + constructor(role = "", content="", reasoning_content="", tool_calls=[], trimmedContent="") { /** @type {NSChatMessage} */ - this.ns = { role: role, content: content, tool_calls: tool_calls } + this.ns = { role: role, content: content, tool_calls: tool_calls, reasoning_content: reasoning_content } this.trimmedContent = trimmedContent; } @@ -68,12 +69,13 @@ class ChatMessageEx { * @param {ChatMessageEx} old */ static newFrom(old) { - return new ChatMessageEx(old.ns.role, old.ns.content, old.ns.tool_calls, old.trimmedContent) + return new ChatMessageEx(old.ns.role, old.ns.content, old.ns.reasoning_content, old.ns.tool_calls, old.trimmedContent) } clear() { this.ns.role = ""; this.ns.content = ""; + this.ns.reasoning_content = ""; this.ns.tool_calls = []; this.trimmedContent = ""; } @@ -182,6 +184,7 @@ class ChatMessageEx { } } else { let toolCalls = nwo["choices"][0]["delta"]["tool_calls"]; + let reasoningContent = nwo["choices"][0]["delta"]["reasoning_content"]; if (toolCalls !== undefined) { if (toolCalls[0]["function"]["name"] !== undefined) { this.ns.tool_calls.push(toolCalls[0]); @@ -197,6 +200,9 @@ class ChatMessageEx { } } } + if (reasoningContent !== undefined) { + this.ns.reasoning_content += reasoningContent + } } } } else { @@ -221,6 +227,10 @@ class ChatMessageEx { this.ns.content = curContent; } } + let curRC = nwo["choices"][0]["message"]["reasoning_content"]; + if (curRC != undefined) { + this.ns.reasoning_content = curRC; + } let curTCs = nwo["choices"][0]["message"]["tool_calls"]; if (curTCs != undefined) { this.ns.tool_calls = curTCs; @@ -242,10 +252,21 @@ class ChatMessageEx { } content_equiv() { + let reasoning = "" + if (this.ns.reasoning_content.trim() !== "") { + reasoning = this.ns.reasoning_content.trim() + } if (this.ns.content !== "") { + if (reasoning !== "") { + return `!!!Reasoning: ${reasoning}!!! ${this.ns.content}`; + } return this.ns.content; } else if (this.has_toolcall()) { - return `\n${this.ns.tool_calls[0].function.name}\n${this.ns.tool_calls[0].function.arguments}\n`; + let ret = "" + if (reasoning !== "") { + ret = `!!!Reasoning: ${reasoning}!!!` + } + return `${ret}\n${this.ns.tool_calls[0].function.name}\n${this.ns.tool_calls[0].function.arguments}\n`; } else { return "" } @@ -325,7 +346,7 @@ class SimpleChat { let tcur = /** @type {OldChatMessage} */(/** @type {unknown} */(cur)); this.xchat.push(new ChatMessageEx(tcur.role, tcur.content)) } else { - this.xchat.push(new ChatMessageEx(cur.ns.role, cur.ns.content, cur.ns.tool_calls, cur.trimmedContent)) + this.xchat.push(new ChatMessageEx(cur.ns.role, cur.ns.content, cur.ns.reasoning_content, cur.ns.tool_calls, cur.trimmedContent)) } } } @@ -394,6 +415,9 @@ class SimpleChat { if (!tmsg.has_toolcall()) { tmsg.ns_delete("tool_calls") } + if (tmsg.ns.reasoning_content.trim() === "") { + tmsg.ns_delete("reasoning_content") + } if (tmsg.ns.role == Roles.Tool) { let res = ChatMessageEx.extractToolCallResultAllInOne(tmsg.ns.content) tmsg.ns.content = res.content @@ -471,6 +495,10 @@ class SimpleChat { let last = undefined; for(const x of this.recent_chat(gMe.chatProps.iRecentUserMsgCnt)) { if (x.ns.role != Roles.ToolTemp) { + if (x.ns.reasoning_content.trim() === "") { + let entry = ui.el_create_append_p(`>>${x.ns.role}!<<: ${x.ns.reasoning_content}`, div); + entry.className = `role-${x.ns.role}`; + } let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, div); entry.className = `role-${x.ns.role}`; last = entry; From 47d9550131681844ddf11431d50243a27df4a6e4 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 15:16:14 +0530 Subject: [PATCH 161/446] SimpleChatTC:Reasoning: Cleanup the initial go Rather simplify and make the content_equiv provide a relatively simple and neat representation of the reasoning with content and toolcall as the cases may be. Also remove the partial new para that I had introduced in the initial go for reasoning. --- tools/server/public_simplechat/simplechat.js | 30 +++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index e9cf7d0591..71daf2ad8f 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -251,25 +251,25 @@ class ChatMessageEx { return true } + /** + * Collate all the different parts of a chat message into a single string object. + * + * This currently includes reasoning, content and toolcall parts. + */ content_equiv() { let reasoning = "" + let content = "" + let toolcall = "" if (this.ns.reasoning_content.trim() !== "") { - reasoning = this.ns.reasoning_content.trim() + reasoning = `!!!Reasoning: ${this.ns.reasoning_content.trim()} !!!\n`; } if (this.ns.content !== "") { - if (reasoning !== "") { - return `!!!Reasoning: ${reasoning}!!! ${this.ns.content}`; - } - return this.ns.content; - } else if (this.has_toolcall()) { - let ret = "" - if (reasoning !== "") { - ret = `!!!Reasoning: ${reasoning}!!!` - } - return `${ret}\n${this.ns.tool_calls[0].function.name}\n${this.ns.tool_calls[0].function.arguments}\n`; - } else { - return "" + content = this.ns.content; } + if (this.has_toolcall()) { + toolcall = `\n\n\n${this.ns.tool_calls[0].function.name}\n${this.ns.tool_calls[0].function.arguments}\n\n`; + } + return `${reasoning} ${content} ${toolcall}`; } } @@ -495,10 +495,6 @@ class SimpleChat { let last = undefined; for(const x of this.recent_chat(gMe.chatProps.iRecentUserMsgCnt)) { if (x.ns.role != Roles.ToolTemp) { - if (x.ns.reasoning_content.trim() === "") { - let entry = ui.el_create_append_p(`>>${x.ns.role}!<<: ${x.ns.reasoning_content}`, div); - entry.className = `role-${x.ns.role}`; - } let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, div); entry.className = `role-${x.ns.role}`; last = entry; From aa17edfa7828c2c99456798ddca8aca3d7b0a082 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 16:14:00 +0530 Subject: [PATCH 162/446] SimpleChatTC:SimpleProxy: Include some news sites in allowed domains --- .../local.tools/simpleproxy.json | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index a4ea4305f0..fce3fb1fb0 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -11,7 +11,33 @@ ".*\\.duckduckgo\\.com$", "^duckduckgo\\.com$", ".*\\.google\\.com$", - "^google\\.com$" + "^google\\.com$", + "^arxiv\\.org$", + ".*\\.nature\\.com$", + ".*\\.science\\.org$", + ".*\\.reuters\\.com$", + ".*\\.bloomberg\\.com$", + ".*\\.forbes\\.com$", + ".*\\.npr\\.org$", + ".*\\.cnn\\.com$", + ".*\\.theguardian\\.com$", + ".*\\.bbc\\.com$", + ".*\\.france24\\.com$", + ".*\\.dw\\.com$", + ".*\\.jpost\\.com$", + ".*\\.aljazeera\\.com$", + ".*\\.alarabiya\\.net$", + ".*\\.rt\\.com$", + "^tass\\.com$", + ".*\\.channelnewsasia\\.com$", + ".*\\.scmp\\.com$", + ".*\\.nikkei\\.com$", + ".*\\.nhk\\.or\\.jp$", + ".*\\.indiatoday\\.in$", + "^theprint\\.in$", + ".*\\.ndtv\\.com$", + "^lwn\\.net$", + "^arstechnica\\.com$" ], "bearer.insecure": "NeverSecure" } From 62bce9ebfbf8b52ca1ec5bd00c5f9d2032f66e20 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 17:05:58 +0530 Subject: [PATCH 163/446] SimpleChatTC:Show: Cleanup Update existing flow so that next Tool Role message is handled directly from within --- tools/server/public_simplechat/simplechat.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 71daf2ad8f..a241f3acb5 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -367,7 +367,7 @@ class SimpleChat { if (iRecentUserMsgCnt == 0) { console.warn("WARN:SimpleChat:SC:RecentChat:iRecentUsermsgCnt of 0 means no user message/query sent"); } - /** @type{ChatMessages} */ + /** @type {ChatMessages} */ let rchat = []; let sysMsg = this.get_system_latest(); if (sysMsg.ns.content.length != 0) { @@ -493,16 +493,16 @@ class SimpleChat { div.replaceChildren(); } let last = undefined; - for(const x of this.recent_chat(gMe.chatProps.iRecentUserMsgCnt)) { - if (x.ns.role != Roles.ToolTemp) { - let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, div); - entry.className = `role-${x.ns.role}`; - last = entry; - } else { - if (elInUser) { + for(const [i, x] of this.recent_chat(gMe.chatProps.iRecentUserMsgCnt).entries()) { + if (x.ns.role === Roles.ToolTemp) { + if (i == (this.xchat.length - 1)) { elInUser.value = x.ns.content; } + continue } + let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, div); + entry.className = `role-${x.ns.role}`; + last = entry; } if (last !== undefined) { last.scrollIntoView(false); From 2cc10f6705778f3c406cdaad30a4acfc0b11ac0a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 17:55:45 +0530 Subject: [PATCH 164/446] SimpleChatTC:MultiChatUI.ChatShow: Mov SimpleChat.Show in -initial Also take care of updating the toolcall ui if needed from within this. --- tools/server/public_simplechat/simplechat.js | 45 ++++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index a241f3acb5..237c58d163 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -488,7 +488,7 @@ class SimpleChat { * @param {boolean} bClear * @param {boolean} bShowInfoAll */ - show(div, elInUser, bClear=true, bShowInfoAll=false) { + showTOREMOVE(div, elInUser, bClear=true, bShowInfoAll=false) { if (bClear) { div.replaceChildren(); } @@ -865,6 +865,17 @@ class MultiChatUI { /** * Refresh UI wrt given chatId, provided it matches the currently selected chatId + * + * Show the chat contents in elDivChat. + * Also update + * * the user query input box, with ToolTemp role message, if last one. + * * the tool call trigger ui, with Tool role message, if last one. + * + * If requested to clear prev stuff and inturn no chat content then show + * * usage info + * * option to load prev saved chat if any + * * as well as settings/info. + * * @param {string} chatId * @param {boolean} bClear * @param {boolean} bShowInfoAll @@ -874,7 +885,34 @@ class MultiChatUI { return false } let chat = this.simpleChats[this.curChatId]; - chat.show(this.elDivChat, this.elInUser, bClear, bShowInfoAll) + if (bClear) { + this.elDivChat.replaceChildren(); + this.ui_reset_toolcall_as_needed(new ChatMessageEx()); + } + let last = undefined; + for(const [i, x] of chat.recent_chat(gMe.chatProps.iRecentUserMsgCnt).entries()) { + if (x.ns.role === Roles.ToolTemp) { + if (i == (chat.xchat.length - 1)) { + this.elInUser.value = x.ns.content; + } + continue + } + let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, this.elDivChat); + entry.className = `role-${x.ns.role}`; + last = entry; + if (x.ns.role === Roles.Tool) { + this.ui_reset_toolcall_as_needed(x); + } + } + if (last !== undefined) { + last.scrollIntoView(false); + } else { + if (bClear) { + this.elDivChat.innerHTML = gUsageMsg; + gMe.setup_load(this.elDivChat, chat); + gMe.show_info(this.elDivChat, bShowInfoAll); + } + } return true } @@ -1040,7 +1078,6 @@ class MultiChatUI { } else { console.debug(`DBUG:SimpleChat:MCUI:HandleUserSubmit:ChatId has changed:[${chatId}] [${this.curChatId}]`); } - this.ui_reset_toolcall_as_needed(theResp); this.ui_reset_userinput(); } @@ -1250,7 +1287,7 @@ class Me { console.log("DBUG:SimpleChat:SC:Load", chat); chat.load(); queueMicrotask(()=>{ - chat.show(div, this.multiChat.elInUser, true, true); + this.multiChat.chat_show(chat.chatId, true, true); this.multiChat.elInSystem.value = chat.get_system_latest().ns.content; }); }); From 937aa57528842525ce833479c0de8168bb0b4940 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 18:14:26 +0530 Subject: [PATCH 165/446] SimpleChatTC:MultiChatUI:ChatShow cleanup of Initial skeleton Fix up the initial skeleton / logic as needed. Remember that we are working with potentially a subset of chat messages from the session, given the sliding window logic of context managing on client ui side, so fix up the logic to use the right subset of messages array and not the global xchat when deciding whether a message is the last or last but one, which need special handling wrt Assistant (with toolcall) and Tool (ie response) messages. Moving tool call ui setup as well as tool call response got ui setup into ChatShow of MultiChatUI ensures that switching between chat sessions handle the ui wrt tool call triggering ui and tool call response submission related ui as needed properly. Rather even loading a previously auto saved chat session if it had tool call or tool call response to be handled, the chat ui will be setup as needed to continue that session properly. --- tools/server/public_simplechat/simplechat.js | 30 ++++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 237c58d163..b4aa6b6d17 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -353,11 +353,11 @@ class SimpleChat { /** * Recent chat messages. - * If iRecentUserMsgCnt < 0 - * Then return the full chat history - * Else - * Return chat messages from latest going back till the last/latest system prompt. - * While keeping track that the number of user queries/messages doesnt exceed iRecentUserMsgCnt. + * + * If iRecentUserMsgCnt < 0, Then return the full chat history + * + * Else Return chat messages from latest going back till the last/latest system prompt. + * While keeping track that the number of user queries/messages doesnt exceed iRecentUserMsgCnt. * @param {number} iRecentUserMsgCnt */ recent_chat(iRecentUserMsgCnt) { @@ -890,9 +890,10 @@ class MultiChatUI { this.ui_reset_toolcall_as_needed(new ChatMessageEx()); } let last = undefined; - for(const [i, x] of chat.recent_chat(gMe.chatProps.iRecentUserMsgCnt).entries()) { + let chatToShow = chat.recent_chat(gMe.chatProps.iRecentUserMsgCnt); + for(const [i, x] of chatToShow.entries()) { if (x.ns.role === Roles.ToolTemp) { - if (i == (chat.xchat.length - 1)) { + if (i == (chatToShow.length - 1)) { this.elInUser.value = x.ns.content; } continue @@ -900,8 +901,19 @@ class MultiChatUI { let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, this.elDivChat); entry.className = `role-${x.ns.role}`; last = entry; - if (x.ns.role === Roles.Tool) { - this.ui_reset_toolcall_as_needed(x); + if (x.ns.role === Roles.Assistant) { + let bTC = false + if (i == (chatToShow.length-1)) { + bTC = true + } + if (i == (chatToShow.length-2)) { + if (chatToShow[i+1].ns.role == Roles.ToolTemp) { + bTC = true + } + } + if (bTC) { + this.ui_reset_toolcall_as_needed(x); + } } } if (last !== undefined) { From cf06c8682bd2bddee794ad0d02239f436cb71d67 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 28 Oct 2025 18:56:24 +0530 Subject: [PATCH 166/446] SimpleChatTC:Reasoning+: Update readme wrt reasoning, flow cleanup Also cleanup the minimal based showing of chat messages a bit And add github.com to allowed list --- .../local.tools/simpleproxy.json | 3 +- tools/server/public_simplechat/readme.md | 91 +++++++++++++++---- tools/server/public_simplechat/simplechat.js | 4 +- 3 files changed, 76 insertions(+), 22 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index fce3fb1fb0..1902890c60 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -37,7 +37,8 @@ "^theprint\\.in$", ".*\\.ndtv\\.com$", "^lwn\\.net$", - "^arstechnica\\.com$" + "^arstechnica\\.com$", + ".*\\.github\\.com$" ], "bearer.insecure": "NeverSecure" } diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index b8afbb9dd3..2dfdf07451 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -14,9 +14,9 @@ Continue reading for the details. ## overview This simple web frontend, allows triggering/testing the server's /completions or /chat/completions endpoints -in a simple way with minimal code from a common code base. Inturn additionally it tries to allow single or -multiple independent back and forth chatting to an extent, with the ai llm model at a basic level, with their -own system prompts. +in a simple way with minimal code from a common code base. Additionally it also allows end users to have +single or multiple independent chat sessions with back and forth chatting to an extent, with the ai llm model +at a basic level, with their own system prompts. This allows seeing the generated text / ai-model response in oneshot at the end, after it is fully generated, or potentially as it is being generated, in a streamed manner from the server/ai-model. @@ -24,7 +24,10 @@ or potentially as it is being generated, in a streamed manner from the server/ai ![Chat and Settings (old) screens](./simplechat_screens.webp "Chat and Settings (old) screens") Auto saves the chat session locally as and when the chat is progressing and inturn at a later time when you -open SimpleChat, option is provided to restore the old chat session, if a matching one exists. +open SimpleChat, option is provided to restore the old chat session, if a matching one exists. In turn if +any of those chat sessions were pending wrt user triggering a tool call or submitting a tool call response, +the ui is setup as needed for end user to continue with those previously saved sessions, from where they +left off. The UI follows a responsive web design so that the layout can adapt to available display space in a usable enough manner, in general. @@ -36,12 +39,17 @@ settings ui. For GenAi/LLM models supporting tool / function calling, allows one to interact with them and explore use of ai driven augmenting of the knowledge used for generating answers as well as for cross checking ai generated answers logically / programatically and by checking with other sources and lot more by making using of the -predefined tools / functions. The end user is provided control over tool calling and response submitting. +simple yet useful predefined tools / functions provided by this client web ui. The end user is provided full +control over tool calling and response submitting. -NOTE: Current web service api doesnt expose the model context length directly, so client logic doesnt provide -any adaptive culling of old messages nor of replacing them with summary of their content etal. However there -is a optional sliding window based chat logic, which provides a simple minded culling of old messages from -the chat history before sending to the ai model. +For GenAi/LLM models which support reasoning, the thinking of the model will be shown to the end user as the +model is running through its reasoning. + +NOTE: As all genai/llm web service apis may or may not expose the model context length directly, and also +as using ai out of band for additional parallel work may not be efficient given the loading of current systems +by genai/llm models, so client logic doesnt provide any adaptive culling of old messages nor of replacing them +with summary of their content etal. However there is a optional sliding window based chat logic, which provides +a simple minded culling of old messages from the chat history before sending to the ai model. NOTE: Wrt options sent with the request, it mainly sets temperature, max_tokens and optionaly stream as well as tool_calls mainly for now. However if someone wants they can update the js file or equivalent member in @@ -110,7 +118,7 @@ Once inside * try trim garbage in response or not * amount of chat history in the context sent to server/ai-model * oneshot or streamed mode. - * use built in tool calling or not + * use built in tool calling or not and its related params. * In completion mode >> note: most recent work has been in chat mode << * one normally doesnt use a system prompt in completion mode. @@ -149,6 +157,9 @@ Once inside * the user input box will be disabled and a working message will be shown in it. * if trim garbage is enabled, the logic will try to trim repeating text kind of garbage to some extent. +* any reasoning / thinking by the model is shown to the end user, as it is occuring, if the ai model + shares the same over the http interface. + * tool calling flow when working with ai models which support tool / function calling * if tool calling is enabled and the user query results in need for one of the builtin tools to be called, then the ai response might include request for tool call. @@ -159,6 +170,9 @@ Once inside ie generated result with meta data * if user is ok with the tool response, they can click submit to send the same to the GenAi/LLM. User can even modify the response generated by the tool, if required, before submitting. + * ALERT: Sometimes the reasoning or chat from ai model may indicate tool call, but you may actually + not get/see a tool call, in such situations, dont forget to cross check that tool calling is + enabled in the settings. * just refresh the page, to reset wrt the chat history and or system prompt and start afresh. This also helps if you had forgotten to start the bundled simpleproxy.py server before hand. @@ -372,8 +386,7 @@ needed to help generate better responses. this can also be used for * searching for specific topics and summarising the results * or so -The tool calling feature has been tested with Gemma3N, Granite4 and GptOss (given that -reasoning is currently unsupported by this client ui, it can mess with things) +The tool calling feature has been tested with Gemma3N, Granite4 and GptOss. ALERT: The simple minded way in which this is implemented, it provides some minimal safety mechanism like running ai generated code in web workers and restricting web access to user @@ -454,7 +467,8 @@ Provide a handler which * rather in some cases constructs the code to be run to get the tool / function call job done, and inturn pass the same to the provided web worker to get it executed. Use console.log while generating any response that should be sent back to the ai model, in your constructed code. -* once the job is done, return the generated result as needed. +* once the job is done, return the generated result as needed, along with tool call related meta + data like chatSessionId, toolCallId, toolName which was passed along with the tool call. Update the tc_switch to include a object entry for the tool, which inturn includes * the meta data wrt the tool call @@ -495,24 +509,63 @@ gets executed, before tool calling returns and thus data / error generated by th get incorporated in result sent to ai engine on the server side. -### ToDo +### Progress + +#### Done + +Tool Calling support added, along with a bunch of useful tool calls as well as a bundled simple proxy +if one wants to access web as part of tool call usage. + +Reasoning / thinking response from Ai Models is shown to the user, as they are being generated/shared. + +Chat Messages/Session and UI handling have been moved into corresponding Classes to an extent, this +helps ensure that +* switching chat sessions or loading a previous auto saved chat session will restore state including + ui such that end user can continue the chat session from where they left it, even if in the middle + of a tool call handshake. +* new fields added to http handshake in oneshot or streaming mode can be handled in a structured way + to an extent. + +#### ToDo Is the tool call promise land trap deep enough, need to think through and explore around this once later. Trap error responses. -Handle reasoning/thinking responses from ai models. - Handle multimodal handshaking with ai models. Add fetch_rss and documents|data_store tool calling, through the simpleproxy.py if and where needed. +Save used config entries along with the auto saved chat sessions and inturn give option to reload the +same when saved chat is loaded. -### Debuging the handshake +MAYBE make the settings in general chat session specific, rather than the current global config flow. -When working with llama.cpp server based GenAi/LLM running locally -sudo tcpdump -i lo -s 0 -vvv -A host 127.0.0.1 and port 8080 | tee /tmp/td.log +### Debuging the handshake and beyond + +When working with llama.cpp server based GenAi/LLM running locally, to look at the handshake directly +from the commandline, you could run something like below + +* sudo tcpdump -i lo -s 0 -vvv -A host 127.0.0.1 and port 8080 | tee /tmp/td.log +* or one could also try look at the network tab in the browser developer console + +One could always remove message entries or manipulate chat sessions by accessing document['gMe'] +in devel console of the browser + +* if you want the last tool call response you submitted to be re-available for tool call execution and + resubmitting of response fresh, for any reason, follow below steps + * remove the assistant response from end of chat session, if any, using + * document['gMe'].multiChat.simpleChats['SessionId'].xchat.pop() + * reset role of Tool response chat message to TOOL.TEMP from tool + * toolMessageIndex = document['gMe'].multiChat.simpleChats['SessionId'].xchat.length - 1 + * document['gMe'].multiChat.simpleChats['SessionId'].xchat[toolMessageIndex].role = "TOOL.TEMP" + * clicking on the SessionId at top in UI, should refresh the chat ui and inturn it should now give + the option to control that tool call again + * this can also help in the case where the chat session fails with context window exceeded + * you restart the GenAi/LLM server after increasing the context window as needed + * edit the chat session history as mentioned above, to the extent needed + * resubmit the last needed user/tool response as needed ## At the end diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index b4aa6b6d17..706cbad032 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -261,7 +261,7 @@ class ChatMessageEx { let content = "" let toolcall = "" if (this.ns.reasoning_content.trim() !== "") { - reasoning = `!!!Reasoning: ${this.ns.reasoning_content.trim()} !!!\n`; + reasoning = `!!!Reasoning: ${this.ns.reasoning_content.trim()} !!!\n\n`; } if (this.ns.content !== "") { content = this.ns.content; @@ -898,7 +898,7 @@ class MultiChatUI { } continue } - let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, this.elDivChat); + let entry = ui.el_create_append_p(`[[ ${x.ns.role} ]]: ${x.content_equiv()}`, this.elDivChat); entry.className = `role-${x.ns.role}`; last = entry; if (x.ns.role === Roles.Assistant) { From 59effa6ea8c729629c1f16389cf0f438a8a481c7 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 29 Oct 2025 03:38:34 +0530 Subject: [PATCH 167/446] SimpleChatTC:Cleanup: tool resp xml, some allowed domains Add a newline between name and content in the xml representation of the tool response, so that it is more easy to distinguish things Add github, linkedin and apnews domains to allowed.domains for simpleproxy.py --- tools/server/public_simplechat/local.tools/simpleproxy.json | 5 +++++ tools/server/public_simplechat/simplechat.js | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index 1902890c60..1bae207341 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -15,6 +15,8 @@ "^arxiv\\.org$", ".*\\.nature\\.com$", ".*\\.science\\.org$", + "^apnews\\.com$", + ".*\\.apnews\\.com$", ".*\\.reuters\\.com$", ".*\\.bloomberg\\.com$", ".*\\.forbes\\.com$", @@ -38,6 +40,9 @@ ".*\\.ndtv\\.com$", "^lwn\\.net$", "^arstechnica\\.com$", + ".*\\.linkedin\\.com$", + ".*\\.github\\.io$", + "^github\\.com$", ".*\\.github\\.com$" ], "bearer.insecure": "NeverSecure" diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 706cbad032..8b8de35e85 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -96,7 +96,9 @@ class ChatMessageEx { el.appendChild(doc.createTextNode(k[1])) doc.documentElement.appendChild(el) } - return new XMLSerializer().serializeToString(doc); + let xmlStr = new XMLSerializer().serializeToString(doc); + xmlStr = xmlStr.replace(/\/name>\n Date: Wed, 29 Oct 2025 14:16:50 +0530 Subject: [PATCH 168/446] SimpleChatTC:Cleanup:Move showing message into ShowMessage --- tools/server/public_simplechat/simplechat.js | 76 +++++++++++++------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 8b8de35e85..53adbb28d4 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -786,6 +786,12 @@ class MultiChatUI { toolcallResponseSubmitClick: undefined } + /** + * Used for tracking presence of any chat message in show related logics + * @type {HTMLElement | null} + */ + this.elLastChatMessage = null + // the ui elements this.elInSystem = /** @type{HTMLInputElement} */(document.getElementById("system-in")); this.elDivChat = /** @type{HTMLDivElement} */(document.getElementById("chat-div")); @@ -865,6 +871,43 @@ class MultiChatUI { this.elInUser.focus(); } + /** + * Handles showing a chat message in UI. + * + * If handling message belonging to role + * * ToolTemp, updates user query input element, if its the last message. + * * Assistant which contains a tool req, shows tool call ui if needed. ie + * * if it is the last message OR + * * if it is the last but one message and there is a ToolTemp message next + * @param {ChatMessageEx} msg + * @param {number} iFromLast + * @param {ChatMessageEx | undefined} nextMsg + */ + show_message(msg, iFromLast, nextMsg) { + if (msg.ns.role === Roles.ToolTemp) { + if (iFromLast == 0) { + this.elInUser.value = msg.ns.content; + } + return + } + let entry = ui.el_create_append_p(`[[ ${msg.ns.role} ]]: ${msg.content_equiv()}`, this.elDivChat); + entry.className = `role-${msg.ns.role}`; + this.elLastChatMessage = entry; + if (msg.ns.role === Roles.Assistant) { + let bTC = false + if (iFromLast == 0) { + bTC = true + } else if ((iFromLast == 1) && (nextMsg != undefined)) { + if (nextMsg.ns.role == Roles.ToolTemp) { + bTC = true + } + } + if (bTC) { + this.ui_reset_toolcall_as_needed(msg); + } + } + } + /** * Refresh UI wrt given chatId, provided it matches the currently selected chatId * @@ -891,35 +934,18 @@ class MultiChatUI { this.elDivChat.replaceChildren(); this.ui_reset_toolcall_as_needed(new ChatMessageEx()); } - let last = undefined; + this.elLastChatMessage = null let chatToShow = chat.recent_chat(gMe.chatProps.iRecentUserMsgCnt); for(const [i, x] of chatToShow.entries()) { - if (x.ns.role === Roles.ToolTemp) { - if (i == (chatToShow.length - 1)) { - this.elInUser.value = x.ns.content; - } - continue - } - let entry = ui.el_create_append_p(`[[ ${x.ns.role} ]]: ${x.content_equiv()}`, this.elDivChat); - entry.className = `role-${x.ns.role}`; - last = entry; - if (x.ns.role === Roles.Assistant) { - let bTC = false - if (i == (chatToShow.length-1)) { - bTC = true - } - if (i == (chatToShow.length-2)) { - if (chatToShow[i+1].ns.role == Roles.ToolTemp) { - bTC = true - } - } - if (bTC) { - this.ui_reset_toolcall_as_needed(x); - } + let iFromLast = (chatToShow.length - 1)-i + let nextMsg = undefined + if (iFromLast == 1) { + nextMsg = chatToShow[i+1] } + this.show_message(x, iFromLast, nextMsg) } - if (last !== undefined) { - last.scrollIntoView(false); + if (this.elLastChatMessage != null) { + /** @type{HTMLElement} */(this.elLastChatMessage).scrollIntoView(false); // Stupid ts-check js-doc intersection ??? } else { if (bClear) { this.elDivChat.innerHTML = gUsageMsg; From 2f07288e4061652622e03ed069377fa78f9158fc Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 29 Oct 2025 15:39:32 +0530 Subject: [PATCH 169/446] SimpleChatTC:ShowMessage: containers, role, contents Seperate out the message ui block into a container containing a role block and contents container block. This will allow themeing of these seperately, if required. As part of same, currently the role has been put to the side of the message with vertical text flow. --- tools/server/public_simplechat/simplechat.css | 17 +++++++++++++ tools/server/public_simplechat/simplechat.js | 25 +++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.css b/tools/server/public_simplechat/simplechat.css index 98e88d99fb..9125771d5b 100644 --- a/tools/server/public_simplechat/simplechat.css +++ b/tools/server/public_simplechat/simplechat.css @@ -28,6 +28,23 @@ background-color: lightpink; } +.chat-message { + border-style: solid; + border-color: grey; + border-width: thin; + border-radius: 2px; + display: flex; +} +.chat-message-role { + border-style: dotted; + border-color: black; + border-width: thin; + border-radius: 4px; + writing-mode: vertical-lr; + padding-inline: 1vmin; +} + + .gridx2 { display: grid; grid-template-columns: repeat(2, 1fr); diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 53adbb28d4..cb0d373cc1 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -879,20 +879,35 @@ class MultiChatUI { * * Assistant which contains a tool req, shows tool call ui if needed. ie * * if it is the last message OR * * if it is the last but one message and there is a ToolTemp message next + * @param {HTMLElement | undefined} elParent * @param {ChatMessageEx} msg * @param {number} iFromLast * @param {ChatMessageEx | undefined} nextMsg */ - show_message(msg, iFromLast, nextMsg) { + show_message(elParent, msg, iFromLast, nextMsg) { + // Handle ToolTemp if (msg.ns.role === Roles.ToolTemp) { if (iFromLast == 0) { this.elInUser.value = msg.ns.content; } return } - let entry = ui.el_create_append_p(`[[ ${msg.ns.role} ]]: ${msg.content_equiv()}`, this.elDivChat); - entry.className = `role-${msg.ns.role}`; - this.elLastChatMessage = entry; + // Create main section + let secMain = document.createElement('section') + secMain.classList.add(`role-${msg.ns.role}`) + secMain.classList.add('chat-message') + elParent?.append(secMain) + this.elLastChatMessage = secMain; + // Create role para + let entry = ui.el_create_append_p(`${msg.ns.role}`, secMain); + entry.className = `chat-message-role`; + // Create content section + let secContent = document.createElement('section') + secContent.classList.add('chat-message-content') + secMain.append(secContent) + // Add the content + entry = ui.el_create_append_p(`${msg.content_equiv()}`, secContent); + // Handle tool call ui, if reqd if (msg.ns.role === Roles.Assistant) { let bTC = false if (iFromLast == 0) { @@ -942,7 +957,7 @@ class MultiChatUI { if (iFromLast == 1) { nextMsg = chatToShow[i+1] } - this.show_message(x, iFromLast, nextMsg) + this.show_message(this.elDivChat, x, iFromLast, nextMsg) } if (this.elLastChatMessage != null) { /** @type{HTMLElement} */(this.elLastChatMessage).scrollIntoView(false); // Stupid ts-check js-doc intersection ??? From ae9f7971a5cfce68224987bcaf194513cbc3f018 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 29 Oct 2025 19:32:44 +0530 Subject: [PATCH 170/446] SimpleChatTC:CSS: Instead of hardcoded btn minwidth use padding --- tools/server/public_simplechat/simplechat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.css b/tools/server/public_simplechat/simplechat.css index 9125771d5b..a26ab4976d 100644 --- a/tools/server/public_simplechat/simplechat.css +++ b/tools/server/public_simplechat/simplechat.css @@ -67,7 +67,7 @@ min-height: 40vh; } button { - min-width: 8vw; + padding-inline: 2vmin; } .sameline { From 41ef449db1a5269adbaa55816bb26a048a8500f3 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 29 Oct 2025 20:11:14 +0530 Subject: [PATCH 171/446] SimpleChatTC:ShowMessage: Seperate out the content parts --- tools/server/public_simplechat/simplechat.css | 12 +++++++ tools/server/public_simplechat/simplechat.js | 33 ++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.css b/tools/server/public_simplechat/simplechat.css index a26ab4976d..e3db6037a8 100644 --- a/tools/server/public_simplechat/simplechat.css +++ b/tools/server/public_simplechat/simplechat.css @@ -43,6 +43,18 @@ writing-mode: vertical-lr; padding-inline: 1vmin; } +.chat-message-toolcall { + border-style: solid; + border-color: grey; + border-width: thin; + border-radius: 2px; +} +.chat-message-toolcall-arg { + border-style: solid; + border-color: grey; + border-width: thin; + border-radius: 2px; +} .gridx2 { diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index cb0d373cc1..1d4b394a77 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -902,14 +902,21 @@ class MultiChatUI { let entry = ui.el_create_append_p(`${msg.ns.role}`, secMain); entry.className = `chat-message-role`; // Create content section - let secContent = document.createElement('section') - secContent.classList.add('chat-message-content') - secMain.append(secContent) + let secContents = document.createElement('section') + secContents.classList.add('chat-message-contents') + secMain.append(secContents) // Add the content - entry = ui.el_create_append_p(`${msg.content_equiv()}`, secContent); + //entry = ui.el_create_append_p(`${msg.content_equiv()}`, secContents); + for (const [name, content] of [['reasoning', msg.ns.reasoning_content], ['content', msg.ns.content]]) { + let cTrimmed = content.trim() + if (cTrimmed.length > 0) { + entry = ui.el_create_append_p(`${cTrimmed}`, secContents); + entry.classList.add(`chat-message-${name}`) + } + } // Handle tool call ui, if reqd + let bTC = false if (msg.ns.role === Roles.Assistant) { - let bTC = false if (iFromLast == 0) { bTC = true } else if ((iFromLast == 1) && (nextMsg != undefined)) { @@ -921,6 +928,22 @@ class MultiChatUI { this.ui_reset_toolcall_as_needed(msg); } } + // Handle tool call non ui + if (msg.has_toolcall() && !bTC) { + let secTC = document.createElement('section') + secTC.classList.add('chat-message-toolcall') + secContents.append(secTC) + entry = ui.el_create_append_p(`name: ${msg.ns.tool_calls[0].function.name}`, secTC); + entry = ui.el_create_append_p(`id: ${msg.ns.tool_calls[0].id}`, secTC); + let oArgs = JSON.parse(msg.ns.tool_calls[0].function.arguments) + for (const k in oArgs) { + entry = ui.el_create_append_p(`arg: ${k}`, secTC); + let secArg = document.createElement('section') + secArg.classList.add('chat-message-toolcall-arg') + secTC.append(secArg) + secArg.innerText = oArgs[k] + } + } } /** From f3593a9611b8f739b3542ea8bb2370f5ac1ee143 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 29 Oct 2025 21:14:21 +0530 Subject: [PATCH 172/446] SimpleChatTC:ShowMessage:Show any number of toolcalls Also make reasoning easily identifiable in the chat --- tools/server/public_simplechat/simplechat.css | 3 ++ tools/server/public_simplechat/simplechat.js | 49 +++++++++++++------ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.css b/tools/server/public_simplechat/simplechat.css index e3db6037a8..0dcb2cf8d8 100644 --- a/tools/server/public_simplechat/simplechat.css +++ b/tools/server/public_simplechat/simplechat.css @@ -43,6 +43,9 @@ writing-mode: vertical-lr; padding-inline: 1vmin; } +.chat-message-reasoning { + border-block-style: dashed; +} .chat-message-toolcall { border-style: solid; border-color: grey; diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 1d4b394a77..8e2a7c37de 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -871,6 +871,27 @@ class MultiChatUI { this.elInUser.focus(); } + /** + * Show the passed function / tool call details in specified parent element. + * @param {HTMLElement} elParent + * @param {NSToolCalls} tc + */ + show_message_toolcall(elParent, tc) { + let secTC = document.createElement('section') + secTC.classList.add('chat-message-toolcall') + elParent.append(secTC) + let entry = ui.el_create_append_p(`name: ${tc.function.name}`, secTC); + entry = ui.el_create_append_p(`id: ${tc.id}`, secTC); + let oArgs = JSON.parse(tc.function.arguments) + for (const k in oArgs) { + entry = ui.el_create_append_p(`arg: ${k}`, secTC); + let secArg = document.createElement('section') + secArg.classList.add('chat-message-toolcall-arg') + secTC.append(secArg) + secArg.innerText = oArgs[k] + } + } + /** * Handles showing a chat message in UI. * @@ -907,10 +928,16 @@ class MultiChatUI { secMain.append(secContents) // Add the content //entry = ui.el_create_append_p(`${msg.content_equiv()}`, secContents); - for (const [name, content] of [['reasoning', msg.ns.reasoning_content], ['content', msg.ns.content]]) { - let cTrimmed = content.trim() - if (cTrimmed.length > 0) { - entry = ui.el_create_append_p(`${cTrimmed}`, secContents); + let showList = [] + if (msg.ns.reasoning_content.trim().length > 0) { + showList.push(['reasoning', `!!!Reasoning: ${msg.ns.reasoning_content.trim()} !!!\n\n`]) + } + if (msg.ns.content.trim().length > 0) { + showList.push(['content', msg.ns.content.trim()]) + } + for (const [name, content] of showList) { + if (content.length > 0) { + entry = ui.el_create_append_p(`${content}`, secContents); entry.classList.add(`chat-message-${name}`) } } @@ -930,18 +957,8 @@ class MultiChatUI { } // Handle tool call non ui if (msg.has_toolcall() && !bTC) { - let secTC = document.createElement('section') - secTC.classList.add('chat-message-toolcall') - secContents.append(secTC) - entry = ui.el_create_append_p(`name: ${msg.ns.tool_calls[0].function.name}`, secTC); - entry = ui.el_create_append_p(`id: ${msg.ns.tool_calls[0].id}`, secTC); - let oArgs = JSON.parse(msg.ns.tool_calls[0].function.arguments) - for (const k in oArgs) { - entry = ui.el_create_append_p(`arg: ${k}`, secTC); - let secArg = document.createElement('section') - secArg.classList.add('chat-message-toolcall-arg') - secTC.append(secArg) - secArg.innerText = oArgs[k] + for (const i in msg.ns.tool_calls) { + this.show_message_toolcall(secContents, msg.ns.tool_calls[i]) } } } From 9e9016f7fea1a159ba0b46262fa709bd4e400090 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 29 Oct 2025 22:06:57 +0530 Subject: [PATCH 173/446] SimpleChatTC:UICleanup: WordBreaks, Print avoid side vertical Define rules to ensure that chat message contents wrap so as to avoid overflowing beyond the size of the screen being viewed. The style used for chat message role to be placed with vertical oriented text adjacent to the actual message content on the side seems to be creating issue with blank pages in some browsers, so avoid that styling when one is printing. --- tools/server/public_simplechat/simplechat.css | 31 +++++++++++++++++++ tools/server/public_simplechat/simplechat.js | 1 + 2 files changed, 32 insertions(+) diff --git a/tools/server/public_simplechat/simplechat.css b/tools/server/public_simplechat/simplechat.css index 0dcb2cf8d8..1f913fa272 100644 --- a/tools/server/public_simplechat/simplechat.css +++ b/tools/server/public_simplechat/simplechat.css @@ -45,6 +45,9 @@ } .chat-message-reasoning { border-block-style: dashed; + overflow-wrap: break-word; + word-break: break-word; + hyphens: auto; } .chat-message-toolcall { border-style: solid; @@ -58,6 +61,16 @@ border-width: thin; border-radius: 2px; } +.chat-message-content { + overflow-wrap: break-word; + word-break: break-word; + hyphens: auto; +} +.chat-message-content-live { + overflow-wrap: break-word; + word-break: break-word; + hyphens: auto; +} .gridx2 { @@ -117,8 +130,26 @@ button { @media print { + #fullbody { height: auto; } + .chat-message { + border-style: solid; + border-color: grey; + border-width: thin; + border-radius: 2px; + display:inherit; + } + .chat-message-role { + border-style: dotted; + border-color: black; + border-width: thin; + border-radius: 4px; + writing-mode:inherit; + padding-inline: 1vmin; + } + + } diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 8e2a7c37de..044e889d2e 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -643,6 +643,7 @@ class SimpleChat { */ async handle_response_multipart(resp, apiEP, elDiv) { let elP = ui.el_create_append_p("", elDiv); + elP.classList.add("chat-message-content-live") if (!resp.body) { throw Error("ERRR:SimpleChat:SC:HandleResponseMultiPart:No body..."); } From 4f857575f5d947d7486cac96bb7c18840e092cc3 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 01:35:09 +0530 Subject: [PATCH 174/446] SimpleChatTC:UICleanup:ShowMessage: Update readme --- tools/server/public_simplechat/readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 2dfdf07451..7f4df25e60 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -526,6 +526,11 @@ helps ensure that * new fields added to http handshake in oneshot or streaming mode can be handled in a structured way to an extent. +Chat message parts seperated out and tagged to allow theming chat message as needed in future. +The default Chat UI theme/look changed to help differentiate between different messages in chat +history as well as the parts of each message in a slightly better manner. Change the theme slightly +between normal and print views (beyond previous infinite height) for better printed chat history. + #### ToDo Is the tool call promise land trap deep enough, need to think through and explore around this once later. From aedffe1df0efafc9bd4c9c029514eef7aa457025 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 01:13:12 +0530 Subject: [PATCH 175/446] SimpleChatTC:DataStore: Initial skeleton of a Db WebWorker Create the DB store Try Get and Set operations The post back to main thread done from asynchronous paths. NOTE: given that it has been ages since indexed db was used, so this is a logical implementation by refering to mdn as needed. --- .../public_simplechat/toolsdbworker.mjs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tools/server/public_simplechat/toolsdbworker.mjs diff --git a/tools/server/public_simplechat/toolsdbworker.mjs b/tools/server/public_simplechat/toolsdbworker.mjs new file mode 100644 index 0000000000..6be6ddb41d --- /dev/null +++ b/tools/server/public_simplechat/toolsdbworker.mjs @@ -0,0 +1,86 @@ +//@ts-check +// STILL DANGER DANGER DANGER - Simple and Stupid - Use from a discardable VM only +// Helpers to handle db related tool/function calling using web worker +// by Humans for All +// + +/** + * Expects to get a message with cid, tcid, (f)name and args + * Posts message with cid, tcid, (f)name and data if any + */ + + +/** + * Allows the db connection to be openned. + */ +function db_open() { + return new Promise((resolve, reject) => { + const dbConn = indexedDB.open('TCDB', 1); + dbConn.onupgradeneeded = (ev) => { + console.debug("DBUG:WWDb:Conn:Upgrade needed...") + dbConn.result.createObjectStore('theDB'); + dbConn.result.onerror = (ev) => { + console.debug(`DBUG:WWDb:Db:Op failed [${ev}]...`) + } + }; + dbConn.onsuccess = (ev) => { + console.debug("DBUG:WWDb:Conn:Opened...") + resolve(dbConn.result); + } + dbConn.onerror = (ev) => { + console.debug(`DBUG:WWDb:Conn:Failed [${ev}]...`) + reject(ev); + } + }); +} + + +self.onmessage = async function (ev) { + try { + console.info(`DBUG:WWDb:${ev.data.name}:OnMessage started...`) + /** @type {IDBDatabase} */ + let db = await db_open(); + let dbTrans = db.transaction('theDB', 'readwrite'); + let dbOS = dbTrans.objectStore('theDB'); + let args = JSON.parse(ev.data.args); + switch (ev.data.name) { + case 'data_store_get': + let reqGet = dbOS.get(args['key']) + reqGet.onsuccess = (evGet) => { + console.info(`DBUG:WWDb:${ev.data.name}:transact success`) + self.postMessage({ + cid: ev.data.cid, + tcid: ev.data.tcid, + name: ev.data.name, + data: { 'status': 'ok', 'data': reqGet.result, 'msg': `DataStoreGet:Ok:${args['key']}:${reqGet.result}`} + }); + } + break; + case 'data_store_set': + let reqSet = dbOS.add(args['value'], args['key']); + reqSet.onsuccess = (evSet) => { + console.info(`DBUG:WWDb:${ev.data.name}:transact success`) + self.postMessage({ + cid: ev.data.cid, + tcid: ev.data.tcid, + name: ev.data.name, + data: { 'status': 'ok', 'msg': `DataStoreSet:Ok:${args['key']}:${reqSet.result}`} + }); + } + break; + default: + console.info(`ERRR:WWDb:${ev.data.name}:OnMessage:Unknown func call...`) + break; + } + console.info(`DBUG:WWDb:${ev.data.name}:OnMessage end`) + } catch (/** @type {any} */error) { + let errMsg = `\n\nTool/Function call "${ev.data.name}" raised an exception:${error.name}:${error.message}\n\n`; + self.postMessage({ + cid: ev.data.cid, + tcid: ev.data.tcid, + name: ev.data.name, + data: {'status': 'error', 'msg': errMsg} + }); + console.info(`ERRR:WWDb:${ev.data.name}:OnMessage end:${error}`) + } +} From 2f58542713d3aa1efd7bfb31732da094213d5e92 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 01:53:28 +0530 Subject: [PATCH 176/446] SimpleChatTC:DataStore: Duplicate tooljs to tooldb initial skel --- tools/server/public_simplechat/tooldb.mjs | 102 ++++++++++++++++++ tools/server/public_simplechat/tooljs.mjs | 1 + tools/server/public_simplechat/tools.mjs | 4 + .../public_simplechat/toolsdbworker.mjs | 2 +- .../server/public_simplechat/toolsworker.mjs | 2 +- 5 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 tools/server/public_simplechat/tooldb.mjs diff --git a/tools/server/public_simplechat/tooldb.mjs b/tools/server/public_simplechat/tooldb.mjs new file mode 100644 index 0000000000..364d92daa4 --- /dev/null +++ b/tools/server/public_simplechat/tooldb.mjs @@ -0,0 +1,102 @@ +//@ts-check +// ALERT - Simple Stupid flow - Using from a discardable VM is better +// Helpers to handle tools/functions calling wrt +// * javascript interpreter +// * simple arithmatic calculator +// using a web worker. +// by Humans for All +// + + +let gToolsWorker = /** @type{Worker} */(/** @type {unknown} */(null)); + + +let js_meta = { + "type": "function", + "function": { + "name": "run_javascript_function_code", + "description": "Runs given code using eval within a web worker context in a browser's javascript environment and returns the console.log outputs of the execution after few seconds", + "parameters": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "The code that will be run using eval within a web worker in the browser's javascript interpreter environment." + } + }, + "required": ["code"] + } + } + } + + +/** + * Implementation of the javascript interpretor logic. Minimal skeleton for now. + * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function js_run(chatid, toolcallid, toolname, obj) { + gToolsWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: obj["code"]}) +} + + +let calc_meta = { + "type": "function", + "function": { + "name": "simple_calculator", + "description": "Calculates the provided arithmatic expression using console.log within a web worker of a browser's javascript interpreter environment and returns the output of the execution once it is done in few seconds", + "parameters": { + "type": "object", + "properties": { + "arithexpr":{ + "type":"string", + "description":"The arithmatic expression that will be calculated by passing it to console.log of a browser's javascript interpreter." + } + }, + "required": ["arithexpr"] + } + } + } + + +/** + * Implementation of the simple calculator logic. Minimal skeleton for now. + * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function calc_run(chatid, toolcallid, toolname, obj) { + gToolsWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) +} + + +/** + * @type {Object>} + */ +export let tc_switch = { + "run_javascript_function_code": { + "handler": js_run, + "meta": js_meta, + "result": "" + }, + "simple_calculator": { + "handler": calc_run, + "meta": calc_meta, + "result": "" + }, +} + + +/** + * Used to get hold of the web worker to use for running tool/function call related code + * Also to setup tool calls, which need to cross check things at runtime + * @param {Worker} toolsWorker + */ +export async function init(toolsWorker) { + gToolsWorker = toolsWorker +} diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index a30330ab82..364d92daa4 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -3,6 +3,7 @@ // Helpers to handle tools/functions calling wrt // * javascript interpreter // * simple arithmatic calculator +// using a web worker. // by Humans for All // diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 73a79f460c..79de29c765 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -10,6 +10,7 @@ import * as tweb from './toolweb.mjs' let gToolsWorker = new Worker('./toolsworker.mjs', { type: 'module' }); +let gToolsDBWorker = new Worker('./toolsdbworker.mjs', { type: 'module' }); /** * Maintain currently available tool/function calls * @type {Object>} @@ -55,6 +56,9 @@ export function setup(cb) { gToolsWorker.onmessage = function (ev) { cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) } + gToolsDBWorker.onmessage = function (ev) { + cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) + } } diff --git a/tools/server/public_simplechat/toolsdbworker.mjs b/tools/server/public_simplechat/toolsdbworker.mjs index 6be6ddb41d..698f281ade 100644 --- a/tools/server/public_simplechat/toolsdbworker.mjs +++ b/tools/server/public_simplechat/toolsdbworker.mjs @@ -1,5 +1,5 @@ //@ts-check -// STILL DANGER DANGER DANGER - Simple and Stupid - Use from a discardable VM only +// STILL DANGER DANGER DANGER - Simple and Stupid - Using from a discardable VM better. // Helpers to handle db related tool/function calling using web worker // by Humans for All // diff --git a/tools/server/public_simplechat/toolsworker.mjs b/tools/server/public_simplechat/toolsworker.mjs index 15675a8df8..6706a44721 100644 --- a/tools/server/public_simplechat/toolsworker.mjs +++ b/tools/server/public_simplechat/toolsworker.mjs @@ -1,5 +1,5 @@ //@ts-check -// STILL DANGER DANGER DANGER - Simple and Stupid - Use from a discardable VM only +// STILL DANGER DANGER DANGER - Simple and Stupid - Using from a discardable VM better. // Helpers to handle tools/functions calling using web worker // by Humans for All // From b080ddf5c3e5c28a19d12eb3a9d96ef9582b3f3a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 02:29:46 +0530 Subject: [PATCH 177/446] SimpleChatTC:DataStore: Remaining plumbing to try this Update tooldb logic to match that needed for the db logic and its web worker. Bring in the remaining aspects of db helpers into tools flow. --- tools/server/public_simplechat/tooldb.mjs | 69 ++++++++++++----------- tools/server/public_simplechat/tools.mjs | 7 +++ 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/tools/server/public_simplechat/tooldb.mjs b/tools/server/public_simplechat/tooldb.mjs index 364d92daa4..1a2e2c71eb 100644 --- a/tools/server/public_simplechat/tooldb.mjs +++ b/tools/server/public_simplechat/tooldb.mjs @@ -1,92 +1,95 @@ //@ts-check // ALERT - Simple Stupid flow - Using from a discardable VM is better -// Helpers to handle tools/functions calling wrt -// * javascript interpreter -// * simple arithmatic calculator +// Helpers to handle tools/functions calling wrt data store // using a web worker. // by Humans for All // -let gToolsWorker = /** @type{Worker} */(/** @type {unknown} */(null)); +let gToolsDBWorker = /** @type{Worker} */(/** @type {unknown} */(null)); -let js_meta = { +let dsget_meta = { "type": "function", "function": { - "name": "run_javascript_function_code", - "description": "Runs given code using eval within a web worker context in a browser's javascript environment and returns the console.log outputs of the execution after few seconds", + "name": "data_store_get", + "description": "Retrieve the value associated with a given key, in few seconds", "parameters": { "type": "object", "properties": { - "code": { + "key": { "type": "string", - "description": "The code that will be run using eval within a web worker in the browser's javascript interpreter environment." + "description": "The key whose value should be returned." } }, - "required": ["code"] + "required": [ "key" ], } } } /** - * Implementation of the javascript interpretor logic. Minimal skeleton for now. - * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * Implementation of the data store get logic. Minimal skeleton for now. + * NOTE: Has access to the javascript web worker environment and can mess with it and beyond * @param {string} chatid * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function js_run(chatid, toolcallid, toolname, obj) { - gToolsWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: obj["code"]}) +function dsget_run(chatid, toolcallid, toolname, obj) { + gToolsDBWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) } -let calc_meta = { +let dsset_meta = { "type": "function", "function": { - "name": "simple_calculator", - "description": "Calculates the provided arithmatic expression using console.log within a web worker of a browser's javascript interpreter environment and returns the output of the execution once it is done in few seconds", + "name": "data_store_set", + "description": "Store a value under a given key, in few seconds using a web worker", "parameters": { "type": "object", "properties": { - "arithexpr":{ - "type":"string", - "description":"The arithmatic expression that will be calculated by passing it to console.log of a browser's javascript interpreter." + "key": { + "type": "string", + "description": "The key under which to store the value." + }, + "value": { + "type": "any", + "description": "The value to store. Can be any JSON-serialisable type." } }, - "required": ["arithexpr"] - } + "required": ["key", "value"] + }, } } /** - * Implementation of the simple calculator logic. Minimal skeleton for now. - * ALERT: Has access to the javascript web worker environment and can mess with it and beyond + * Implementation of the data store set logic. Minimal skeleton for now. + * NOTE: Has access to the javascript web worker environment and can mess with it and beyond * @param {string} chatid * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function calc_run(chatid, toolcallid, toolname, obj) { - gToolsWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) +function dsset_run(chatid, toolcallid, toolname, obj) { + gToolsDBWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) } + /** * @type {Object>} */ export let tc_switch = { - "run_javascript_function_code": { - "handler": js_run, - "meta": js_meta, + "data_store_get": { + "handler": dsget_run, + "meta": dsget_meta, "result": "" }, - "simple_calculator": { - "handler": calc_run, - "meta": calc_meta, + "data_store_set": { + "handler": dsset_run, + "meta": dsset_meta, "result": "" }, } @@ -98,5 +101,5 @@ export let tc_switch = { * @param {Worker} toolsWorker */ export async function init(toolsWorker) { - gToolsWorker = toolsWorker + gToolsDBWorker = toolsWorker } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 79de29c765..256754f5ab 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -7,6 +7,7 @@ import * as tjs from './tooljs.mjs' import * as tweb from './toolweb.mjs' +import * as tdb from './tooldb.mjs' let gToolsWorker = new Worker('./toolsworker.mjs', { type: 'module' }); @@ -29,6 +30,12 @@ export async function init() { toolNames.push(key) } }) + await tdb.init(gToolsDBWorker).then(()=>{ + for (const key in tdb.tc_switch) { + tc_switch[key] = tdb.tc_switch[key] + toolNames.push(key) + } + }) let tNs = await tweb.init(gToolsWorker) for (const key in tNs) { tc_switch[key] = tNs[key] From 797b702251fdfc700a0e25a5be2b3020a9e6784c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 03:13:32 +0530 Subject: [PATCH 178/446] SimpleChatTC:DataStore:FuncCallArgs: Any type not supported So mention that may be ai can send complex objects in stringified form. Rather once type of value is set to string, ai should normally do it, but no harm is hinting. --- tools/server/public_simplechat/tooldb.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/tooldb.mjs b/tools/server/public_simplechat/tooldb.mjs index 1a2e2c71eb..0c348fc4b8 100644 --- a/tools/server/public_simplechat/tooldb.mjs +++ b/tools/server/public_simplechat/tooldb.mjs @@ -22,7 +22,7 @@ let dsget_meta = { "description": "The key whose value should be returned." } }, - "required": [ "key" ], + "required": ["key"], } } } @@ -54,8 +54,8 @@ let dsset_meta = { "description": "The key under which to store the value." }, "value": { - "type": "any", - "description": "The value to store. Can be any JSON-serialisable type." + "type": "string", + "description": "The value to store, complex objects could be passed in JSON Stringified format." } }, "required": ["key", "value"] From 4ad88f0da87226e3e383ec224814e96399ca58d5 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 03:28:07 +0530 Subject: [PATCH 179/446] SimpleChatTC:DataStore:Eagerness to Wrong JSON conversions In the eagerness of initial skeleton, had forgotten that the root/generic tool call router takes care of parsing the json string into a object, before calling the tool call, so no need to try parse again. Fixed the same. Hadnt converted the object based response from data store related calls in the db web worker, into json string before passing to the generic tool response callback, fixed the same. - Rather the though of making the ChatMsgEx.createAllInOne handle string or object set aside for now, to keep things simple and consistant to the greatest extent possible across different flows. And good news - flow is working atleast for the overall happy path Need to check what corner cases are lurking like calling set on same key more than once, seemed to have some flow oddity, which I need to check later. Also maybe change the field name to value from data in the response to get, to match the field name convention of set. GPT-OSS is fine with it. But worst case micro / nano / pico models may trip up, in worst case, so better to keep things consistent. --- tools/server/public_simplechat/readme.md | 6 +++++- tools/server/public_simplechat/tools.mjs | 2 +- tools/server/public_simplechat/toolsdbworker.mjs | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 7f4df25e60..e506837450 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -531,6 +531,10 @@ The default Chat UI theme/look changed to help differentiate between different m history as well as the parts of each message in a slightly better manner. Change the theme slightly between normal and print views (beyond previous infinite height) for better printed chat history. +Initial skeletons of a builtin data store related tool calls, built on browser's indexedDB, without +needing any proxy / additional helper to handle the store. One could use the ai assistant to store +ones (ie end users) own data or data of ai model. + #### ToDo Is the tool call promise land trap deep enough, need to think through and explore around this once later. @@ -539,7 +543,7 @@ Trap error responses. Handle multimodal handshaking with ai models. -Add fetch_rss and documents|data_store tool calling, through the simpleproxy.py if and where needed. +Add fetch_rss and documents|data_store [wip] tool calling, through the simpleproxy.py if and where needed. Save used config entries along with the auto saved chat sessions and inturn give option to reload the same when saved chat is loaded. diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 256754f5ab..b63e94ab34 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -64,7 +64,7 @@ export function setup(cb) { cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) } gToolsDBWorker.onmessage = function (ev) { - cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) + cb(ev.data.cid, ev.data.tcid, ev.data.name, JSON.stringify(ev.data.data)) } } diff --git a/tools/server/public_simplechat/toolsdbworker.mjs b/tools/server/public_simplechat/toolsdbworker.mjs index 698f281ade..c64c85cc68 100644 --- a/tools/server/public_simplechat/toolsdbworker.mjs +++ b/tools/server/public_simplechat/toolsdbworker.mjs @@ -42,7 +42,7 @@ self.onmessage = async function (ev) { let db = await db_open(); let dbTrans = db.transaction('theDB', 'readwrite'); let dbOS = dbTrans.objectStore('theDB'); - let args = JSON.parse(ev.data.args); + let args = ev.data.args; switch (ev.data.name) { case 'data_store_get': let reqGet = dbOS.get(args['key']) From 2dad246d53eafa7a63336edf7f36a95e4ad087fb Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 04:24:10 +0530 Subject: [PATCH 180/446] SimpleChatTC:DataStore: Dont ignore the error paths And indexedDB add isnt the one to be happy with updating existing key. --- .../public_simplechat/toolsdbworker.mjs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/toolsdbworker.mjs b/tools/server/public_simplechat/toolsdbworker.mjs index c64c85cc68..b0a6b5a73d 100644 --- a/tools/server/public_simplechat/toolsdbworker.mjs +++ b/tools/server/public_simplechat/toolsdbworker.mjs @@ -20,15 +20,15 @@ function db_open() { console.debug("DBUG:WWDb:Conn:Upgrade needed...") dbConn.result.createObjectStore('theDB'); dbConn.result.onerror = (ev) => { - console.debug(`DBUG:WWDb:Db:Op failed [${ev}]...`) + console.info(`ERRR:WWDb:Db:Op failed [${ev}]...`) } }; dbConn.onsuccess = (ev) => { - console.debug("DBUG:WWDb:Conn:Opened...") + console.debug("INFO:WWDb:Conn:Opened...") resolve(dbConn.result); } dbConn.onerror = (ev) => { - console.debug(`DBUG:WWDb:Conn:Failed [${ev}]...`) + console.info(`ERRR:WWDb:Conn:Failed [${ev}]...`) reject(ev); } }); @@ -55,9 +55,27 @@ self.onmessage = async function (ev) { data: { 'status': 'ok', 'data': reqGet.result, 'msg': `DataStoreGet:Ok:${args['key']}:${reqGet.result}`} }); } + reqGet.onerror = (evGet) => { + console.info(`ERRR:WWDb:${ev.data.name}:transact failed:${reqGet.error}`) + self.postMessage({ + cid: ev.data.cid, + tcid: ev.data.tcid, + name: ev.data.name, + data: { 'status': 'error', 'msg': `DataStoreGet:Err:${args['key']}:${reqGet.error}`} + }); + } break; case 'data_store_set': let reqSet = dbOS.add(args['value'], args['key']); + reqSet.onerror = (evSet) => { + console.info(`ERRR:WWDb:${ev.data.name}:transact failed:${reqSet.error}`) + self.postMessage({ + cid: ev.data.cid, + tcid: ev.data.tcid, + name: ev.data.name, + data: { 'status': 'error', 'msg': `DataStoreSet:Err:${args['key']}:${reqSet.error}`} + }); + } reqSet.onsuccess = (evSet) => { console.info(`DBUG:WWDb:${ev.data.name}:transact success`) self.postMessage({ From d80e438cfa1201403641cf35ea5f6e43325cb472 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 04:36:47 +0530 Subject: [PATCH 181/446] SimpleChatTC:DataStore:Put, stringify undefined, readme Update the descriptions of set and get to indicate the possible corner cases or rather semantic in such situations. Update the readme also a bit. The auto save and restore mentioned has nothing to do with the new data store mechanism. --- tools/server/public_simplechat/readme.md | 8 +++++++- tools/server/public_simplechat/tooldb.mjs | 4 ++-- tools/server/public_simplechat/tools.mjs | 4 +++- tools/server/public_simplechat/toolsdbworker.mjs | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index e506837450..cb7b26058c 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -102,6 +102,8 @@ remember to * the white list of allowed.domains * the shared bearer token between server and client ui +* other builtin tool / function calls like calcultor, javascript runner, DataStore dont require simpleproxy.py + ### using the front end @@ -178,6 +180,8 @@ Once inside This also helps if you had forgotten to start the bundled simpleproxy.py server before hand. Start the simpleproxy.py server and refresh the client ui page, to get access to web access related tool calls. + * if you refreshed unknowingly, you can use the Restore feature to try load the previous chat + session and resume that session. This uses a basic local auto save logic that is in there. * Using NewChat one can start independent chat sessions. * two independent chat sessions are setup by default. @@ -406,6 +410,8 @@ The following tools/functions are currently provided by default * run_javascript_function_code - which can be used to run some javascript code in the browser context. +* data_store_get/set - allows for a basic data store to be used. + Currently the ai generated code / expression is run through a simple minded eval inside a web worker mechanism. Use of WebWorker helps avoid exposing browser global scope to the generated code directly. However any shared web worker scope isnt isolated. Either way always remember to cross check the tool @@ -450,7 +456,7 @@ The bundled simple proxy a non-browser entity. In future it can be further extended to help with other relatively simple yet useful tool calls like -data / documents_store, fetch_rss and so. +data / documents_store [wip], fetch_rss and so. * for now fetch_rss can be indirectly achieved using fetch_web_url_raw. diff --git a/tools/server/public_simplechat/tooldb.mjs b/tools/server/public_simplechat/tooldb.mjs index 0c348fc4b8..c6bf47e7c8 100644 --- a/tools/server/public_simplechat/tooldb.mjs +++ b/tools/server/public_simplechat/tooldb.mjs @@ -13,7 +13,7 @@ let dsget_meta = { "type": "function", "function": { "name": "data_store_get", - "description": "Retrieve the value associated with a given key, in few seconds", + "description": "Retrieve the value associated with a given key, in few seconds using a web worker. If key doesnt exist, then __UNDEFINED__ is returned as the value.", "parameters": { "type": "object", "properties": { @@ -45,7 +45,7 @@ let dsset_meta = { "type": "function", "function": { "name": "data_store_set", - "description": "Store a value under a given key, in few seconds using a web worker", + "description": "Store a value under a given key, in few seconds using a web worker. If the key already exists, its value will be updated to the new value", "parameters": { "type": "object", "properties": { diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index b63e94ab34..e4f66ea7f3 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -64,7 +64,9 @@ export function setup(cb) { cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) } gToolsDBWorker.onmessage = function (ev) { - cb(ev.data.cid, ev.data.tcid, ev.data.name, JSON.stringify(ev.data.data)) + cb(ev.data.cid, ev.data.tcid, ev.data.name, JSON.stringify(ev.data.data, (k,v)=>{ + return (v === undefined) ? '__UNDEFINED__' : v; + })); } } diff --git a/tools/server/public_simplechat/toolsdbworker.mjs b/tools/server/public_simplechat/toolsdbworker.mjs index b0a6b5a73d..6e17ec4a3b 100644 --- a/tools/server/public_simplechat/toolsdbworker.mjs +++ b/tools/server/public_simplechat/toolsdbworker.mjs @@ -66,7 +66,7 @@ self.onmessage = async function (ev) { } break; case 'data_store_set': - let reqSet = dbOS.add(args['value'], args['key']); + let reqSet = dbOS.put(args['value'], args['key']); reqSet.onerror = (evSet) => { console.info(`ERRR:WWDb:${ev.data.name}:transact failed:${reqSet.error}`) self.postMessage({ From bd7f7cb72a6e322059a7ff657ddeb3ded0271216 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 05:51:38 +0530 Subject: [PATCH 182/446] SimpleChatTC:DataStore: Delete a record - the db web worker side --- .../public_simplechat/toolsdbworker.mjs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tools/server/public_simplechat/toolsdbworker.mjs b/tools/server/public_simplechat/toolsdbworker.mjs index 6e17ec4a3b..9769706a76 100644 --- a/tools/server/public_simplechat/toolsdbworker.mjs +++ b/tools/server/public_simplechat/toolsdbworker.mjs @@ -44,6 +44,7 @@ self.onmessage = async function (ev) { let dbOS = dbTrans.objectStore('theDB'); let args = ev.data.args; switch (ev.data.name) { + case 'data_store_get': let reqGet = dbOS.get(args['key']) reqGet.onsuccess = (evGet) => { @@ -65,6 +66,7 @@ self.onmessage = async function (ev) { }); } break; + case 'data_store_set': let reqSet = dbOS.put(args['value'], args['key']); reqSet.onerror = (evSet) => { @@ -86,9 +88,33 @@ self.onmessage = async function (ev) { }); } break; + + case 'data_store_delete': + let reqDel = dbOS.delete(args['key']) + reqDel.onsuccess = (evDel) => { + console.info(`DBUG:WWDb:${ev.data.name}:transact success`) + self.postMessage({ + cid: ev.data.cid, + tcid: ev.data.tcid, + name: ev.data.name, + data: { 'status': 'ok', 'msg': `DataStoreDelete:Ok:${args['key']}:${reqDel.result}`} + }); + } + reqDel.onerror = (evDel) => { + console.info(`ERRR:WWDb:${ev.data.name}:transact failed:${reqDel.error}`) + self.postMessage({ + cid: ev.data.cid, + tcid: ev.data.tcid, + name: ev.data.name, + data: { 'status': 'error', 'msg': `DataStoreDelete:Err:${args['key']}:${reqDel.error}`} + }); + } + break; + default: console.info(`ERRR:WWDb:${ev.data.name}:OnMessage:Unknown func call...`) break; + } console.info(`DBUG:WWDb:${ev.data.name}:OnMessage end`) } catch (/** @type {any} */error) { From 7f8eb0487568a245f2698947448fe48567216364 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 05:59:45 +0530 Subject: [PATCH 183/446] SimpleChatTC:DataStore:Delete a record - the plumbing side --- tools/server/public_simplechat/tooldb.mjs | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tools/server/public_simplechat/tooldb.mjs b/tools/server/public_simplechat/tooldb.mjs index c6bf47e7c8..2aa07853c5 100644 --- a/tools/server/public_simplechat/tooldb.mjs +++ b/tools/server/public_simplechat/tooldb.mjs @@ -77,6 +77,38 @@ function dsset_run(chatid, toolcallid, toolname, obj) { } +let dsdel_meta = { + "type": "function", + "function": { + "name": "data_store_delete", + "description": "Remove the entry associated with a given key, in few seconds using a web worker.", + "parameters": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The key that should be deleted along with its entry." + } + }, + "required": ["key"], + } + } + } + + +/** + * Implementation of the data store delete logic. Minimal skeleton for now. + * NOTE: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function dsdel_run(chatid, toolcallid, toolname, obj) { + gToolsDBWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) +} + + /** * @type {Object>} @@ -92,6 +124,11 @@ export let tc_switch = { "meta": dsset_meta, "result": "" }, + "data_store_delete": { + "handler": dsdel_run, + "meta": dsdel_meta, + "result": "" + }, } From 2d497069d2cf2f360b1d838481628630df81388b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 06:14:27 +0530 Subject: [PATCH 184/446] SimpleChatTC:DataStore:list - web worker side logic The basic skeleton added on the web worker side for listing keys. TODO: Avoid duplication of similar code to an extent across some of these db ops. --- .../public_simplechat/toolsdbworker.mjs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tools/server/public_simplechat/toolsdbworker.mjs b/tools/server/public_simplechat/toolsdbworker.mjs index 9769706a76..9b47a822ca 100644 --- a/tools/server/public_simplechat/toolsdbworker.mjs +++ b/tools/server/public_simplechat/toolsdbworker.mjs @@ -45,6 +45,28 @@ self.onmessage = async function (ev) { let args = ev.data.args; switch (ev.data.name) { + case 'data_store_list': + let reqList = dbOS.getAllKeys() + reqList.onsuccess = (evList) => { + console.info(`DBUG:WWDb:${ev.data.name}:transact success`) + self.postMessage({ + cid: ev.data.cid, + tcid: ev.data.tcid, + name: ev.data.name, + data: { 'status': 'ok', 'data': reqList.result, 'msg': `DataStoreList:Ok:${args['key']}:${reqList.result}`} + }); + } + reqList.onerror = (evList) => { + console.info(`ERRR:WWDb:${ev.data.name}:transact failed:${reqList.error}`) + self.postMessage({ + cid: ev.data.cid, + tcid: ev.data.tcid, + name: ev.data.name, + data: { 'status': 'error', 'msg': `DataStoreList:Err:${args['key']}:${reqList.error}`} + }); + } + break; + case 'data_store_get': let reqGet = dbOS.get(args['key']) reqGet.onsuccess = (evGet) => { From 57dd22851250835e35a329076340b56c2ba109eb Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 06:24:01 +0530 Subject: [PATCH 185/446] SimpleChatTC:DataStore:List keys - the plumbing --- tools/server/public_simplechat/tooldb.mjs | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tools/server/public_simplechat/tooldb.mjs b/tools/server/public_simplechat/tooldb.mjs index 2aa07853c5..5eefc16e89 100644 --- a/tools/server/public_simplechat/tooldb.mjs +++ b/tools/server/public_simplechat/tooldb.mjs @@ -109,6 +109,33 @@ function dsdel_run(chatid, toolcallid, toolname, obj) { } +let dslist_meta = { + "type": "function", + "function": { + "name": "data_store_list", + "description": "List all keys wrt key-value pairs currently stored in the data store. This will take few seconds and uses a web worker.", + "parameters": { + "type": "object", + "properties": { + }, + } + } + } + + +/** + * Implementation of the data store list logic. Minimal skeleton for now. + * NOTE: Has access to the javascript web worker environment and can mess with it and beyond + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function dslist_run(chatid, toolcallid, toolname, obj) { + gToolsDBWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) +} + + /** * @type {Object>} @@ -129,6 +156,11 @@ export let tc_switch = { "meta": dsdel_meta, "result": "" }, + "data_store_list": { + "handler": dslist_run, + "meta": dslist_meta, + "result": "" + }, } From 5935ecceca46b28d23eeb5d714391a2713c849d3 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 06:28:27 +0530 Subject: [PATCH 186/446] SimpleChatTC:DataStore:Cleanup:Msg, duplicate on routing side Avoid the duplicate plumbing code and use a common ops plumbing helper. Remove args[key] oversight from DataStoreList msg on webworkr --- tools/server/public_simplechat/tooldb.mjs | 51 +++---------------- .../public_simplechat/toolsdbworker.mjs | 4 +- 2 files changed, 8 insertions(+), 47 deletions(-) diff --git a/tools/server/public_simplechat/tooldb.mjs b/tools/server/public_simplechat/tooldb.mjs index 5eefc16e89..f77a4e6985 100644 --- a/tools/server/public_simplechat/tooldb.mjs +++ b/tools/server/public_simplechat/tooldb.mjs @@ -28,19 +28,6 @@ let dsget_meta = { } -/** - * Implementation of the data store get logic. Minimal skeleton for now. - * NOTE: Has access to the javascript web worker environment and can mess with it and beyond - * @param {string} chatid - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function dsget_run(chatid, toolcallid, toolname, obj) { - gToolsDBWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) -} - - let dsset_meta = { "type": "function", "function": { @@ -64,19 +51,6 @@ let dsset_meta = { } -/** - * Implementation of the data store set logic. Minimal skeleton for now. - * NOTE: Has access to the javascript web worker environment and can mess with it and beyond - * @param {string} chatid - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function dsset_run(chatid, toolcallid, toolname, obj) { - gToolsDBWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) -} - - let dsdel_meta = { "type": "function", "function": { @@ -96,19 +70,6 @@ let dsdel_meta = { } -/** - * Implementation of the data store delete logic. Minimal skeleton for now. - * NOTE: Has access to the javascript web worker environment and can mess with it and beyond - * @param {string} chatid - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function dsdel_run(chatid, toolcallid, toolname, obj) { - gToolsDBWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) -} - - let dslist_meta = { "type": "function", "function": { @@ -124,14 +85,14 @@ let dslist_meta = { /** - * Implementation of the data store list logic. Minimal skeleton for now. + * Implementation of the minimal needed plumbing for data store related ops triggering. * NOTE: Has access to the javascript web worker environment and can mess with it and beyond * @param {string} chatid * @param {string} toolcallid * @param {string} toolname * @param {any} obj */ -function dslist_run(chatid, toolcallid, toolname, obj) { +function dsops_run(chatid, toolcallid, toolname, obj) { gToolsDBWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) } @@ -142,22 +103,22 @@ function dslist_run(chatid, toolcallid, toolname, obj) { */ export let tc_switch = { "data_store_get": { - "handler": dsget_run, + "handler": dsops_run, "meta": dsget_meta, "result": "" }, "data_store_set": { - "handler": dsset_run, + "handler": dsops_run, "meta": dsset_meta, "result": "" }, "data_store_delete": { - "handler": dsdel_run, + "handler": dsops_run, "meta": dsdel_meta, "result": "" }, "data_store_list": { - "handler": dslist_run, + "handler": dsops_run, "meta": dslist_meta, "result": "" }, diff --git a/tools/server/public_simplechat/toolsdbworker.mjs b/tools/server/public_simplechat/toolsdbworker.mjs index 9b47a822ca..2075a231b8 100644 --- a/tools/server/public_simplechat/toolsdbworker.mjs +++ b/tools/server/public_simplechat/toolsdbworker.mjs @@ -53,7 +53,7 @@ self.onmessage = async function (ev) { cid: ev.data.cid, tcid: ev.data.tcid, name: ev.data.name, - data: { 'status': 'ok', 'data': reqList.result, 'msg': `DataStoreList:Ok:${args['key']}:${reqList.result}`} + data: { 'status': 'ok', 'data': reqList.result, 'msg': `DataStoreList:Ok:${reqList.result}`} }); } reqList.onerror = (evList) => { @@ -62,7 +62,7 @@ self.onmessage = async function (ev) { cid: ev.data.cid, tcid: ev.data.tcid, name: ev.data.name, - data: { 'status': 'error', 'msg': `DataStoreList:Err:${args['key']}:${reqList.error}`} + data: { 'status': 'error', 'msg': `DataStoreList:Err:${reqList.error}`} }); } break; From 8ab8727f70ab5c75a0daaf2437e344d4cd7eda12 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 30 Oct 2025 07:11:50 +0530 Subject: [PATCH 187/446] SimpleChatTC:DataStore: update readme --- tools/server/public_simplechat/readme.md | 29 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index cb7b26058c..3c873d0a06 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -102,7 +102,8 @@ remember to * the white list of allowed.domains * the shared bearer token between server and client ui -* other builtin tool / function calls like calcultor, javascript runner, DataStore dont require simpleproxy.py +* other builtin tool / function calls like calculator, javascript runner, DataStore dont require the + simpleproxy.py helper. @@ -390,6 +391,15 @@ needed to help generate better responses. this can also be used for * searching for specific topics and summarising the results * or so +* save collated data or generated analysis or more to the provided data store and retrieve +them later to augment the analysis / generation then. Also could be used to summarise chat +session till a given point and inturn save the summary into data store and later retrieve +the summary and continue the chat session using the summary and thus with a reduced context +window to worry about. + +* use your imagination and ai models capabilities as you see fit, without restrictions from +others. + The tool calling feature has been tested with Gemma3N, Granite4 and GptOss. ALERT: The simple minded way in which this is implemented, it provides some minimal safety @@ -410,7 +420,7 @@ The following tools/functions are currently provided by default * run_javascript_function_code - which can be used to run some javascript code in the browser context. -* data_store_get/set - allows for a basic data store to be used. +* data_store_get/set/delete/list - allows for a basic data store to be used. Currently the ai generated code / expression is run through a simple minded eval inside a web worker mechanism. Use of WebWorker helps avoid exposing browser global scope to the generated code directly. @@ -456,7 +466,7 @@ The bundled simple proxy a non-browser entity. In future it can be further extended to help with other relatively simple yet useful tool calls like -data / documents_store [wip], fetch_rss and so. +fetch_rss and so. * for now fetch_rss can be indirectly achieved using fetch_web_url_raw. @@ -482,8 +492,8 @@ Update the tc_switch to include a object entry for the tool, which inturn includ It should pass these along to the tools web worker, if used. * the result key (was used previously, may use in future, but for now left as is) -Look into tooljs.mjs for javascript and inturn web worker based tool calls and toolweb.mjs -for the simpleproxy.py based tool calls. +Look into tooljs.mjs and tooldb.mjs for javascript and inturn web worker based tool calls and +toolweb.mjs for the simpleproxy.py based tool calls. #### OLD: Mapping tool calls and responses to normal assistant - user chat flow @@ -537,9 +547,9 @@ The default Chat UI theme/look changed to help differentiate between different m history as well as the parts of each message in a slightly better manner. Change the theme slightly between normal and print views (beyond previous infinite height) for better printed chat history. -Initial skeletons of a builtin data store related tool calls, built on browser's indexedDB, without -needing any proxy / additional helper to handle the store. One could use the ai assistant to store -ones (ie end users) own data or data of ai model. +A builtin data store related tool calls, inturn built on browser's indexedDB, without needing any +proxy / additional helper to handle the store. One could use the ai assistant to store ones (ie end +users) own data or data of ai model. #### ToDo @@ -549,7 +559,8 @@ Trap error responses. Handle multimodal handshaking with ai models. -Add fetch_rss and documents|data_store [wip] tool calling, through the simpleproxy.py if and where needed. +Add fetch_rss and may be different document formats processing related tool calling, in turn through +the simpleproxy.py if and where needed. Save used config entries along with the auto saved chat sessions and inturn give option to reload the same when saved chat is loaded. From 8d7eece81c2b969c6c377dd4281157c8ac390d76 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 15:01:52 +0530 Subject: [PATCH 188/446] SimpleChatTC:ToolsWorker: Update note to flow with chat session id --- tools/server/public_simplechat/toolsworker.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/toolsworker.mjs b/tools/server/public_simplechat/toolsworker.mjs index 6706a44721..6bd27728d2 100644 --- a/tools/server/public_simplechat/toolsworker.mjs +++ b/tools/server/public_simplechat/toolsworker.mjs @@ -5,8 +5,8 @@ // /** - * Expects to get a message with id, name and code to run - * Posts message with id, name and data captured from console.log outputs + * Expects to get a message with id (session and toolcall), name and code to run + * Posts message with id (session and toolcall), name and data captured from console.log outputs */ From 482517543b1979b957725512bb24cc241d43bbcd Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 15:04:10 +0530 Subject: [PATCH 189/446] SimpleChatTC:Seperate out actual nw handshake - initial go move the actual chat handshake with ai server into a seperate code to an extent. also initial anchor to trap handshake http error responses Rather come to think of it, its better to move this into SimpleChat class. Use finally to ensure any needed cleanup for handle_user_submit occurs within itself. --- tools/server/public_simplechat/simplechat.js | 58 +++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 044e889d2e..e6261c2bd9 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1042,7 +1042,6 @@ class MultiChatUI { let msg = `ERRR:SimpleChat\nMCUI:HandleUserSubmit:${this.curChatId}\n${reason.name}:${reason.message}`; console.error(msg.replace("\n", ":")); alert(msg); - this.ui_reset_userinput(); }); }); @@ -1110,8 +1109,33 @@ class MultiChatUI { } + /** + * @param {SimpleChat} chat + * @param {string} apiEP + */ + async handle_chat_hs(chat, apiEP) { + let theUrl = ApiEP.Url(gMe.baseURL, apiEP); + let theBody = chat.request_jsonstr(apiEP); + console.debug(`DBUG:SimpleChat:MCUI:${chat.chatId}:HandleUserSubmit:${theUrl}:ReqBody:${theBody}`); + + let theHeaders = chat.fetch_headers(apiEP); + let resp = await fetch(theUrl, { + method: "POST", + headers: theHeaders, + body: theBody, + }); + + if (resp.status >= 300) { + + } + + return chat.handle_response(resp, apiEP, this.elDivChat); + } + + /** * Handle user query submit request, wrt specified chat session. + * * NOTE: Currently the user query entry area is used for * * showing and allowing edits by user wrt tool call results * in a predfined simple xml format, @@ -1120,6 +1144,7 @@ class MultiChatUI { * Based on presence of the predefined xml format data at beginning * the logic will treat it has a tool result and if not then as a * normal user query. + * * @param {string} chatId * @param {string} apiEP */ @@ -1151,30 +1176,23 @@ class MultiChatUI { } this.chat_show(chat.chatId); - let theUrl = ApiEP.Url(gMe.baseURL, apiEP); - let theBody = chat.request_jsonstr(apiEP); - this.elInUser.value = "working..."; this.elInUser.disabled = true; - console.debug(`DBUG:SimpleChat:MCUI:${chatId}:HandleUserSubmit:${theUrl}:ReqBody:${theBody}`); - let theHeaders = chat.fetch_headers(apiEP); - let resp = await fetch(theUrl, { - method: "POST", - headers: theHeaders, - body: theBody, - }); - let theResp = await chat.handle_response(resp, apiEP, this.elDivChat); - if (chatId == this.curChatId) { - this.chat_show(chatId); - if (theResp.trimmedContent.length > 0) { - let p = ui.el_create_append_p(`TRIMMED:${theResp.trimmedContent}`, this.elDivChat); - p.className="role-trim"; + try { + let theResp = await this.handle_chat_hs(chat, apiEP) + if (chatId == this.curChatId) { + this.chat_show(chatId); + if (theResp.trimmedContent.length > 0) { + let p = ui.el_create_append_p(`TRIMMED:${theResp.trimmedContent}`, this.elDivChat); + p.className="role-trim"; + } + } else { + console.debug(`DBUG:SimpleChat:MCUI:HandleUserSubmit:ChatId has changed:[${chatId}] [${this.curChatId}]`); } - } else { - console.debug(`DBUG:SimpleChat:MCUI:HandleUserSubmit:ChatId has changed:[${chatId}] [${this.curChatId}]`); + } finally { + this.ui_reset_userinput(); } - this.ui_reset_userinput(); } /** From 91f39b7197ae2a9f80dcf0773b78931142df2dc6 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 15:22:29 +0530 Subject: [PATCH 190/446] SimpleChatTC:Move chat server handshake to SimpleChat --- tools/server/public_simplechat/simplechat.js | 51 ++++++++++---------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index e6261c2bd9..14f69b7ac9 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -735,6 +735,31 @@ class SimpleChat { return theResp; } + /** + * Handle the chat handshake with the ai server + * @param {string} baseURL + * @param {string} apiEP + * @param {HTMLDivElement} elDivChat - used to show chat response as it is being generated/recieved in streaming mode + */ + async handle_chat_hs(baseURL, apiEP, elDivChat) { + let theUrl = ApiEP.Url(baseURL, apiEP); + let theBody = this.request_jsonstr(apiEP); + console.debug(`DBUG:SimpleChat:${this.chatId}:HandleChatHS:${theUrl}:ReqBody:${theBody}`); + + let theHeaders = this.fetch_headers(apiEP); + let resp = await fetch(theUrl, { + method: "POST", + headers: theHeaders, + body: theBody, + }); + + if (resp.status >= 300) { + + } + + return this.handle_response(resp, apiEP, elDivChat); + } + /** * Call the requested tool/function. * Returns undefined, if the call was placed successfully @@ -1109,30 +1134,6 @@ class MultiChatUI { } - /** - * @param {SimpleChat} chat - * @param {string} apiEP - */ - async handle_chat_hs(chat, apiEP) { - let theUrl = ApiEP.Url(gMe.baseURL, apiEP); - let theBody = chat.request_jsonstr(apiEP); - console.debug(`DBUG:SimpleChat:MCUI:${chat.chatId}:HandleUserSubmit:${theUrl}:ReqBody:${theBody}`); - - let theHeaders = chat.fetch_headers(apiEP); - let resp = await fetch(theUrl, { - method: "POST", - headers: theHeaders, - body: theBody, - }); - - if (resp.status >= 300) { - - } - - return chat.handle_response(resp, apiEP, this.elDivChat); - } - - /** * Handle user query submit request, wrt specified chat session. * @@ -1180,7 +1181,7 @@ class MultiChatUI { this.elInUser.disabled = true; try { - let theResp = await this.handle_chat_hs(chat, apiEP) + let theResp = await chat.handle_chat_hs(gMe.baseURL, apiEP, this.elDivChat) if (chatId == this.curChatId) { this.chat_show(chatId); if (theResp.trimmedContent.length > 0) { From 6a8ced244c23d8b0129a059852afe98fa61b448c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 15:48:02 +0530 Subject: [PATCH 191/446] SimpleChatTC:Raise Error on Ai Chat server handshake NotOk resp --- tools/server/public_simplechat/simplechat.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 14f69b7ac9..6e6ffdd581 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -742,6 +742,13 @@ class SimpleChat { * @param {HTMLDivElement} elDivChat - used to show chat response as it is being generated/recieved in streaming mode */ async handle_chat_hs(baseURL, apiEP, elDivChat) { + class ChatHSError extends Error { + constructor(/** @type {string} */message) { + super(message); + this.name = 'ChatHSError' + } + } + let theUrl = ApiEP.Url(baseURL, apiEP); let theBody = this.request_jsonstr(apiEP); console.debug(`DBUG:SimpleChat:${this.chatId}:HandleChatHS:${theUrl}:ReqBody:${theBody}`); @@ -754,7 +761,7 @@ class SimpleChat { }); if (resp.status >= 300) { - + throw new ChatHSError(`HandleChatHS:GotResponse:NotOk:${resp.status}:${resp.statusText}`); } return this.handle_response(resp, apiEP, elDivChat); From 0b2329e5de30daf7d9ed210a24288d0f6c9ba686 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 20:53:01 +0530 Subject: [PATCH 192/446] SimpleChatTC: Update readme --- tools/server/public_simplechat/readme.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 3c873d0a06..b1bfc6007c 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -417,15 +417,14 @@ The following tools/functions are currently provided by default * simple_calculator - which can solve simple arithmatic expressions -* run_javascript_function_code - which can be used to run some javascript code in the browser - context. +* run_javascript_function_code - which can be used to run ai generated or otherwise javascript code using browser's js capabilities. * data_store_get/set/delete/list - allows for a basic data store to be used. -Currently the ai generated code / expression is run through a simple minded eval inside a web worker -mechanism. Use of WebWorker helps avoid exposing browser global scope to the generated code directly. -However any shared web worker scope isnt isolated. Either way always remember to cross check the tool -requests and generated responses when using tool calling. +All of the above are run from inside web worker contexts. Currently the ai generated code / expression +is run through a simple minded eval inside a web worker mechanism. Use of WebWorker helps avoid exposing +browser global scope to the generated code directly. However any shared web worker scope isnt isolated. +Either way always remember to cross check the tool requests and generated responses when using tool calling. ##### using bundled simpleproxy.py (helps bypass browser cors restriction, ...) @@ -551,12 +550,13 @@ A builtin data store related tool calls, inturn built on browser's indexedDB, wi proxy / additional helper to handle the store. One could use the ai assistant to store ones (ie end users) own data or data of ai model. +Trap http response errors and inform user the specific error returned by ai server. + + #### ToDo Is the tool call promise land trap deep enough, need to think through and explore around this once later. -Trap error responses. - Handle multimodal handshaking with ai models. Add fetch_rss and may be different document formats processing related tool calling, in turn through @@ -567,6 +567,8 @@ same when saved chat is loaded. MAYBE make the settings in general chat session specific, rather than the current global config flow. +Provide tool to allow for specified pdf files to be converted to equivalent plain text form, so that ai +can be used to work with the content in those PDFs. ### Debuging the handshake and beyond From da98a961ab7492842dbcf00c78fe259a939fda53 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 21:31:27 +0530 Subject: [PATCH 193/446] SimpleChatTC:SimpleProxy: Enable allowing or not requested feature --- .../public_simplechat/local.tools/simpleproxy.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 8b17d012c0..b6018676f2 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -46,6 +46,8 @@ gConfigType = { gConfigNeeded = [ '--allowed.domains', '--bearer.insecure' ] +gAllowedCalls = [ "urltext", "urlraw" ] + def bearer_transform(): """ @@ -146,6 +148,18 @@ def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): Handle requests to aum path, which is used in a simple way to verify that one is communicating with this proxy server """ + queryParams = urllib.parse.parse_qs(pr.query) + url = queryParams['url'] + print(f"DBUG:HandleAUM:Url:{url}") + url = url[0] + if (not url) or (len(url) == 0): + ph.send_error(400, f"WARN:HandleAUM:MissingUrl/UnknownQuery?!") + return + urlParts = url.split('.',1) + if not (urlParts[0] in gAllowedCalls): + ph.send_error(403, f"WARN:HandleAUM:Forbidded:{urlParts[0]}") + return + print(f"INFO:HandleAUM:Availability ok for:{urlParts[0]}") ph.send_response_only(200, "bharatavarshe") ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() From ecfdb66c940b0b7d31ea027169769899dd486c5d Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 22:06:43 +0530 Subject: [PATCH 194/446] SimpleChatTC:SimpleProxy:Pdf2Text:Initial plumbing Get the pdf2text request for processing. --- .../local.tools/simpleproxy.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index b6018676f2..dd74e539f0 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -46,7 +46,7 @@ gConfigType = { gConfigNeeded = [ '--allowed.domains', '--bearer.insecure' ] -gAllowedCalls = [ "urltext", "urlraw" ] +gAllowedCalls = [ "urltext", "urlraw", "pdf2text" ] def bearer_transform(): @@ -128,6 +128,12 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): self.send_error(400, f"WARN:{acGot['Msg']}") else: handle_urltext(self, pr) + case '/pdf2text': + acGot = self.auth_check() + if not acGot['AllOk']: + self.send_error(400, f"WARN:{acGot['Msg']}") + else: + handle_pdf2text(self, pr) case '/aum': handle_aum(self, pr) case _: @@ -372,6 +378,35 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.send_error(502, f"WARN:UrlTextFailed:{exc}") +def do_pdf2text(fUrl: str): + import pypdf + + + +gAllowedPdfUrlTypes = [ "file", "http", "https" ] + +def handle_pdf2text(ph: ProxyHandler, pr: urllib.parse.ParseResult): + """ + Handle requests to pdf2text path, which is used to extract plain text + from the specified pdf file. + """ + queryParams = urllib.parse.parse_qs(pr.query) + url = queryParams['url'] + print(f"DBUG:HandlePdf2Text:Url:{url}") + url = url[0] + if (not url) or (len(url) == 0): + ph.send_error(400, f"WARN:HandlePdf2Text:MissingUrl!") + return + urlParts = url.split('://',1) + if not (urlParts[0] in gAllowedPdfUrlTypes): + ph.send_error(403, f"WARN:HandlePdf2Text:ForbiddedUrlType:{urlParts[0]}:AllowedUrlTypes:{gAllowedPdfUrlTypes}") + return + print(f"INFO:HandlePdf2Text:Processing:{url}") + ph.send_response_only(200, "Pdf2Text Response follows") + ph.end_headers() + + + def load_config(): """ Allow loading of a json based config file From 5ec29087eaa196366876a5772a716787809944a5 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 22:16:02 +0530 Subject: [PATCH 195/446] SimpleChatTC:SimpleProxy:Pdf2Text: Move handling url to its own --- .../public_simplechat/local.tools/simpleproxy.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index dd74e539f0..57b108c41d 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -378,9 +378,12 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.send_error(502, f"WARN:UrlTextFailed:{exc}") -def do_pdf2text(fUrl: str): +def process_pdf2text(url: str): import pypdf - + urlParts = url.split('://',1) + if not (urlParts[0] in gAllowedPdfUrlTypes): + return { 'status': 403, 'msg': f"WARN:HandlePdf2Text:ForbiddedUrlType:{urlParts[0]}:AllowedUrlTypes:{gAllowedPdfUrlTypes}" } + return { 'status': 500, 'msg': 'Not yet implemented' } gAllowedPdfUrlTypes = [ "file", "http", "https" ] @@ -397,11 +400,11 @@ def handle_pdf2text(ph: ProxyHandler, pr: urllib.parse.ParseResult): if (not url) or (len(url) == 0): ph.send_error(400, f"WARN:HandlePdf2Text:MissingUrl!") return - urlParts = url.split('://',1) - if not (urlParts[0] in gAllowedPdfUrlTypes): - ph.send_error(403, f"WARN:HandlePdf2Text:ForbiddedUrlType:{urlParts[0]}:AllowedUrlTypes:{gAllowedPdfUrlTypes}") - return print(f"INFO:HandlePdf2Text:Processing:{url}") + gotP2T = process_pdf2text(url) + if (gotP2T['status'] != 200): + ph.send_error(gotP2T['status'], gotP2T['msg'] ) + return ph.send_response_only(200, "Pdf2Text Response follows") ph.end_headers() From 6054ddfb6515aecde95debaa22a2a0e7f2ed6651 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 22:24:13 +0530 Subject: [PATCH 196/446] SimpleChatTC:SimpleProxy:Pdf2Text: Initial go --- .../public_simplechat/local.tools/simpleproxy.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 57b108c41d..cb19bc8654 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -380,10 +380,17 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): def process_pdf2text(url: str): import pypdf + import io urlParts = url.split('://',1) if not (urlParts[0] in gAllowedPdfUrlTypes): return { 'status': 403, 'msg': f"WARN:HandlePdf2Text:ForbiddedUrlType:{urlParts[0]}:AllowedUrlTypes:{gAllowedPdfUrlTypes}" } - return { 'status': 500, 'msg': 'Not yet implemented' } + fPdf = open(urlParts[1], 'rb') + dPdf = fPdf.read() + tPdf = "" + oPdf = pypdf.PdfReader(io.BytesIO(dPdf)) + for (pn, pd) in enumerate(oPdf.pages): + tPdf = tPdf + pd.extract_text() + return { 'status': 200, 'msg': "Pdf2Text Response follows", 'data': tPdf } gAllowedPdfUrlTypes = [ "file", "http", "https" ] @@ -405,8 +412,9 @@ def handle_pdf2text(ph: ProxyHandler, pr: urllib.parse.ParseResult): if (gotP2T['status'] != 200): ph.send_error(gotP2T['status'], gotP2T['msg'] ) return - ph.send_response_only(200, "Pdf2Text Response follows") + ph.send_response_only(gotP2T['status'], gotP2T['msg']) ph.end_headers() + ph.wfile.write(gotP2T['data']) From f97efb86e4f376a71597ddac57ded2264e2c8f2a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 22:32:57 +0530 Subject: [PATCH 197/446] SimpleChatTC:SimpleProxy:Pdf2Text: js side initial plumbing Expose pdf2text tool call to ai server and handshake with simple proxy for the same. --- tools/server/public_simplechat/toolweb.mjs | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index eeecf846b0..566b65c1a0 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -269,6 +269,63 @@ async function searchwebtext_setup(tcs) { } +// +// Pdf2Text +// + + +let pdf2text_meta = { + "type": "function", + "function": { + "name": "pdf2text", + "description": "Fetch pdf from requested web / file url through a proxy server and return its text content after converting pdf to text, in few seconds", + "parameters": { + "type": "object", + "properties": { + "url":{ + "type":"string", + "description":"url of the pdf that will be got and inturn converted to text to some extent" + } + }, + "required": ["url"] + } + } + } + + +/** + * Implementation of the pdf to text logic. + * Expects a simple minded proxy server to be running locally + * * listening on a configured port + * * expecting http requests + * * with a query token named url wrt pdf2text path, + * which gives the actual url to fetch + * * gets the requested pdf and converts to text, before returning same. + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function pdf2text_run(chatid, toolcallid, toolname, obj) { + return proxyserver_get_1arg(chatid, toolcallid, toolname, obj, 'pdf2text', 'url', encodeURIComponent(obj.url)); +} + + +/** + * Setup pdf2text for tool calling + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {Object>} tcs + */ +async function pdf2text_setup(tcs) { + return proxyserver_tc_setup('Pdf2Text', 'pdf2text', 'pdf2text', { + "handler": pdf2text_run, + "meta": pdf2text_meta, + "result": "" + }, tcs); +} + + /** * Used to get hold of the web worker to use for running tool/function call related code @@ -284,5 +341,6 @@ export async function init(toolsWorker) { await fetchweburlraw_setup(tc_switch) await fetchweburltext_setup(tc_switch) await searchwebtext_setup(tc_switch) + await pdf2text_setup(tc_switch) return tc_switch } From dfeb94d3f6130993eff60b6fb808b43a87fc2609 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 23:12:27 +0530 Subject: [PATCH 198/446] SimpleChatTC:Pdf2Text: cleanup initial go Make the description bit more explicit with it supporting local file paths as part of the url scheme, as the tested ai model was cribbing about not supporting file url scheme. Need to check if this new description will make things better. Convert the text to bytes for writing to the http pipe. Ensure CORS is kept happy by passing AccessControlAllowOrigin in header. --- .../public_simplechat/local.tools/simpleproxy.py | 10 +++++++--- tools/server/public_simplechat/toolweb.mjs | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index cb19bc8654..99a7004cd2 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -407,14 +407,18 @@ def handle_pdf2text(ph: ProxyHandler, pr: urllib.parse.ParseResult): if (not url) or (len(url) == 0): ph.send_error(400, f"WARN:HandlePdf2Text:MissingUrl!") return - print(f"INFO:HandlePdf2Text:Processing:{url}") + print(f"INFO:HandlePdf2Text:Processing:{url}...") gotP2T = process_pdf2text(url) if (gotP2T['status'] != 200): ph.send_error(gotP2T['status'], gotP2T['msg'] ) return - ph.send_response_only(gotP2T['status'], gotP2T['msg']) + ph.send_response(gotP2T['status'], gotP2T['msg']) + ph.send_header('Content-Type', 'text/text') + # Add CORS for browser fetch, just in case + ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() - ph.wfile.write(gotP2T['data']) + print(f"INFO:HandlePdf2Text:ExtractedText:{url}...") + ph.wfile.write(gotP2T['data'].encode('utf-8')) diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 566b65c1a0..f2cac18967 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -278,13 +278,13 @@ let pdf2text_meta = { "type": "function", "function": { "name": "pdf2text", - "description": "Fetch pdf from requested web / file url through a proxy server and return its text content after converting pdf to text, in few seconds", + "description": "Fetch pdf from requested web / file path url through a proxy server and return its text content after converting pdf to text, in few seconds", "parameters": { "type": "object", "properties": { "url":{ "type":"string", - "description":"url of the pdf that will be got and inturn converted to text to some extent" + "description":"local file / web (http/https) based url of the pdf that will be got and inturn converted to text to an extent" } }, "required": ["url"] From 21544eaf87555cfa4fd36ecc9d3c2dc8d8bef0a2 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 1 Nov 2025 23:52:42 +0530 Subject: [PATCH 199/446] SimpleChatTC:ResultMaxDataLength, Desc Allow user to limit the max amount of result data returned to ai after a tool call. Inturn it is set by default to 2K. Update the pdf2text tool description to try make the local file path support more explicit --- tools/server/public_simplechat/simplechat.js | 11 ++++++++++- tools/server/public_simplechat/toolweb.mjs | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 6e6ffdd581..1af93e2b20 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1091,7 +1091,11 @@ class MultiChatUI { clearTimeout(this.timers.toolcallResponseTimeout) this.timers.toolcallResponseTimeout = undefined let chat = this.simpleChats[cid]; - chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(tcid, name, data))) + let limitedData = data + if (gMe.tools.iResultMaxDataLength > 0) { + limitedData = data.slice(0, gMe.tools.iResultMaxDataLength) + `\n\n\nALERT: Data too long, was chopped ....` + } + chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(tcid, name, limitedData))) if (this.chat_show(cid)) { if (gMe.tools.auto > 0) { this.timers.toolcallResponseSubmitClick = setTimeout(()=>{ @@ -1336,6 +1340,11 @@ class Me { proxyAuthInsecure: "NeverSecure", searchUrl: SearchURLS.duckduckgo, toolNames: /** @type {Array} */([]), + /** + * Control the length of the tool call result data returned to ai after tool call. + * A value of 0 is treated as unlimited data. + */ + iResultMaxDataLength: 2048, /** * Control how many milliseconds to wait for tool call to respond, before generating a timed out * error response and giving control back to end user. diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index f2cac18967..518ccd6f90 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -278,13 +278,13 @@ let pdf2text_meta = { "type": "function", "function": { "name": "pdf2text", - "description": "Fetch pdf from requested web / file path url through a proxy server and return its text content after converting pdf to text, in few seconds", + "description": "Read pdf from requested local file / web url through a proxy server and return its text content after converting pdf to text, in few seconds", "parameters": { "type": "object", "properties": { "url":{ "type":"string", - "description":"local file / web (http/https) based url of the pdf that will be got and inturn converted to text to an extent" + "description":"local file path / web (http/https) based url of the pdf that will be got and inturn converted to text to an extent" } }, "required": ["url"] From e077f23f9e5f6f442f9527a5508d07ef8991fd64 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 02:01:31 +0530 Subject: [PATCH 200/446] SimpleChatTC:Pdf2Text: Refine desc and MaxResultDataLength Needed to tweak the description further for the ai model to be able to understand that its ok to pass file:// scheme based urls Had forgotten how big the web site pages have become as also the need for more ResultDataLength wrt one shot PDF read to get atleast some good enough amount of content in it with large pdfs --- tools/server/public_simplechat/simplechat.js | 2 +- tools/server/public_simplechat/toolweb.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 1af93e2b20..c780373783 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1344,7 +1344,7 @@ class Me { * Control the length of the tool call result data returned to ai after tool call. * A value of 0 is treated as unlimited data. */ - iResultMaxDataLength: 2048, + iResultMaxDataLength: 1024*128, /** * Control how many milliseconds to wait for tool call to respond, before generating a timed out * error response and giving control back to end user. diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 518ccd6f90..34079d04fc 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -278,13 +278,13 @@ let pdf2text_meta = { "type": "function", "function": { "name": "pdf2text", - "description": "Read pdf from requested local file / web url through a proxy server and return its text content after converting pdf to text, in few seconds", + "description": "Read pdf from requested local file path / web url through a proxy server and return its text content after converting pdf to text, in few seconds", "parameters": { "type": "object", "properties": { "url":{ "type":"string", - "description":"local file path / web (http/https) based url of the pdf that will be got and inturn converted to text to an extent" + "description":"local file path (file://) / web (http/https) based url of the pdf that will be got and inturn converted to text to an extent" } }, "required": ["url"] From 61064baa1947ac343cb5b4ee5317a381853dcf14 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 02:29:00 +0530 Subject: [PATCH 201/446] SimpleChatTC:Pdf2Text and otherwise readme update Half asleep as usual ;) --- tools/server/public_simplechat/readme.md | 32 ++++++++++++++++++---- tools/server/public_simplechat/toolweb.mjs | 5 +++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index b1bfc6007c..7b98b832ac 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -88,7 +88,8 @@ remember to * use a GenAi/LLM model which supports tool calling. -* if fetch web page or web search tool call is needed remember to run bundled local.tools/simpleproxy.py +* if fetch web page, web search or pdf-to-text tool call is needed remember to run bundled + local.tools/simpleproxy.py helper along with its config file, before using/loading this client ui through a browser * cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config simpleproxy.json @@ -258,6 +259,10 @@ It is attached to the document object. Some of these can also be updated using t * searchUrl - specify the search engine's search url template along with the tag SEARCHWORDS in place where the search words should be substituted at runtime. + * iResultMaxDataLength - specify what amount of any tool call result should be sent back to the ai engine server. + + * specifying 0 disables this truncating of the results. + * toolCallResponseTimeoutMS - specifies the time (in msecs) for which the logic should wait for a tool call to respond before a default timed out error response is generated and control given back to end user, for them to decide whether to submit the error response or wait for actual tool call response further. @@ -369,11 +374,11 @@ Given that browsers provide a implicit env for not only showing ui, but also run simplechat client ui allows use of tool calling support provided by the newer ai models by end users of llama.cpp's server in a simple way without needing to worry about seperate mcp host / router, tools etal, for basic useful tools/functions like calculator, code execution -(javascript in this case). +(javascript in this case), data store. -Additionally if users want to work with web content as part of their ai chat session, Few -functions related to web access which work with a included python based simple proxy server -have been implemented. +Additionally if users want to work with web content or pdf content as part of their ai chat +session, Few functions related to web access as well as pdf access which work with a included +python based simple proxy server have been implemented. This can allow end users to use some basic yet useful tool calls to enhance their ai chat sessions to some extent. It also provides for a simple minded exploration of tool calling @@ -391,6 +396,8 @@ needed to help generate better responses. this can also be used for * searching for specific topics and summarising the results * or so +* one could also augment additional data / info by accessing text content from pdf files + * save collated data or generated analysis or more to the provided data store and retrieve them later to augment the analysis / generation then. Also could be used to summarise chat session till a given point and inturn save the summary into data store and later retrieve @@ -441,6 +448,17 @@ the above set of web related tool calls work by handshaking with a bundled simpl (/caching in future) server logic, this helps bypass the CORS restrictions applied if trying to directly fetch from the browser js runtime environment. +* pdf2text - fetch/read specified pdf file and extract its textual content + + * local file access is enabled for this feature, so be careful as to where and under which user id + the simple proxy will be run. + + * this depends on the pypdf python based open source library + +Implementing some of the tool calls through the simpleproxy.py server and not directly in the browser +js env, allows one to isolate the core of these logic within a discardable VM or so, by running the +simpleproxy.py in such a vm. + Depending on the path specified wrt the proxy server, it executes the corresponding logic. Like if urltext path is used (and not urlraw), the logic in addition to fetching content from given url, it tries to convert html content into equivalent plain text content to some extent in a simple minded @@ -464,6 +482,8 @@ The bundled simple proxy so that websites will hopefully respect the request rather than blindly rejecting it as coming from a non-browser entity. +* allows getting specified local or web based pdf files and extract their text content for ai to use + In future it can be further extended to help with other relatively simple yet useful tool calls like fetch_rss and so. @@ -552,6 +572,8 @@ users) own data or data of ai model. Trap http response errors and inform user the specific error returned by ai server. +Initial go at a pdf2text tool call. For now it allows local pdf files to be read and their text content +extracted and passed to ai model for further processing, as decided by ai and end user. #### ToDo diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 34079d04fc..7092790a8e 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -1,6 +1,6 @@ //@ts-check // ALERT - Simple Stupid flow - Using from a discardable VM is better -// Helpers to handle tools/functions calling related to web access +// Helpers to handle tools/functions calling related to web access, pdf, etal // which work in sync with the bundled simpleproxy.py server logic. // by Humans for All // @@ -27,6 +27,9 @@ function get_gme() { } +/** + * For now hash the shared secret with the year. + */ function bearer_transform() { let data = `${new Date().getUTCFullYear()}${get_gme().tools.proxyAuthInsecure}` return crypto.subtle.digest('sha-256', new TextEncoder().encode(data)).then(ab=>{ From 63a8ddfbb906a9bd1ccb8b6d05c962c114d2a9e4 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 02:47:46 +0530 Subject: [PATCH 202/446] SimpleChatTC:SimpleProxyHS: make helper work with any num of args This makes the logic more generic, as well as prepares for additional parameters to be passed to the simpleproxy.py helper handshakes. Ex: Restrict extracted contents of a pdf to specified start and end page numbers or so. --- tools/server/public_simplechat/toolweb.mjs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 7092790a8e..a83ce2c38e 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -51,12 +51,11 @@ function bearer_transform() { * @param {string} toolname * @param {any} obj * @param {string} path - * @param {string} qkey - * @param {string} qvalue */ -async function proxyserver_get_1arg(chatid, toolcallid, toolname, obj, path, qkey, qvalue) { +async function proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, path) { if (gToolsWorker.onmessage != null) { - let newUrl = `${get_gme().tools.proxyUrl}/${path}?${qkey}=${qvalue}` + let params = new URLSearchParams(obj) + let newUrl = `${get_gme().tools.proxyUrl}/${path}?${params}` let btoken = await bearer_transform() fetch(newUrl, { headers: { 'Authorization': `Bearer ${btoken}` }}).then(resp => { if (!resp.ok) { @@ -132,7 +131,8 @@ let fetchweburlraw_meta = { * @param {any} obj */ function fetchweburlraw_run(chatid, toolcallid, toolname, obj) { - return proxyserver_get_1arg(chatid, toolcallid, toolname, obj, 'urlraw', 'url', encodeURIComponent(obj.url)); + // maybe filter out any key other than 'url' in obj + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urlraw'); } @@ -190,7 +190,8 @@ let fetchweburltext_meta = { * @param {any} obj */ function fetchweburltext_run(chatid, toolcallid, toolname, obj) { - return proxyserver_get_1arg(chatid, toolcallid, toolname, obj, 'urltext', 'url', encodeURIComponent(obj.url)); + // maybe filter out any key other than 'url' in obj + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urltext'); } @@ -253,7 +254,9 @@ function searchwebtext_run(chatid, toolcallid, toolname, obj) { /** @type {string} */ let searchUrl = get_gme().tools.searchUrl; searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)); - return proxyserver_get_1arg(chatid, toolcallid, toolname, obj, 'urltext', 'url', encodeURIComponent(searchUrl)); + delete(obj.words) + obj['url'] = searchUrl + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urltext'); } } @@ -311,7 +314,7 @@ let pdf2text_meta = { * @param {any} obj */ function pdf2text_run(chatid, toolcallid, toolname, obj) { - return proxyserver_get_1arg(chatid, toolcallid, toolname, obj, 'pdf2text', 'url', encodeURIComponent(obj.url)); + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'pdf2text'); } From 8bc7de441676cc11c60ea69a845285964479d43b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 03:05:30 +0530 Subject: [PATCH 203/446] SimpleChatTC:TC Result truncating only if needed As I was seeing the truncated message even for stripped plain text web acces, relooking at that initial go at truncating, revealed a oversight, which had the truncation logic trigger anytime the iResultMaxDataLength was greater than 0, irrespective of whether the actual result was smaller than the allowed limit or not, thus adding that truncated message to end of result unnecessarily. Have fixed that oversight Also recent any number of args based simpleprox handshake helper in toolweb seems to be working (atleast for the existing single arg based calls). --- tools/server/public_simplechat/readme.md | 2 +- tools/server/public_simplechat/simplechat.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 7b98b832ac..326c9120a3 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -261,7 +261,7 @@ It is attached to the document object. Some of these can also be updated using t * iResultMaxDataLength - specify what amount of any tool call result should be sent back to the ai engine server. - * specifying 0 disables this truncating of the results. + * specifying 0 disables this truncating of the results, and inturn full result will be sent to the ai engine server. * toolCallResponseTimeoutMS - specifies the time (in msecs) for which the logic should wait for a tool call to respond before a default timed out error response is generated and control given back to end user, for them to decide whether diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index c780373783..317637a59e 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1093,7 +1093,9 @@ class MultiChatUI { let chat = this.simpleChats[cid]; let limitedData = data if (gMe.tools.iResultMaxDataLength > 0) { - limitedData = data.slice(0, gMe.tools.iResultMaxDataLength) + `\n\n\nALERT: Data too long, was chopped ....` + if (data.length > gMe.tools.iResultMaxDataLength) { + limitedData = data.slice(0, gMe.tools.iResultMaxDataLength) + `\n\n\nALERT: Data too long, was chopped ....` + } } chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(tcid, name, limitedData))) if (this.chat_show(cid)) { From dd0a7ec500293c0afadabf149d4386b40551262c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 03:38:43 +0530 Subject: [PATCH 204/446] SimpleChatTC:Pdf2Text: Make it work with a subset of pages Initial go, need to review the code flow as well as test it out --- .../local.tools/simpleproxy.py | 21 ++++++++++++++++--- tools/server/public_simplechat/toolweb.mjs | 12 +++++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 99a7004cd2..03b9a330eb 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -378,7 +378,7 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.send_error(502, f"WARN:UrlTextFailed:{exc}") -def process_pdf2text(url: str): +def process_pdf2text(url: str, startPN: int, endPN: int): import pypdf import io urlParts = url.split('://',1) @@ -388,7 +388,12 @@ def process_pdf2text(url: str): dPdf = fPdf.read() tPdf = "" oPdf = pypdf.PdfReader(io.BytesIO(dPdf)) - for (pn, pd) in enumerate(oPdf.pages): + if (startPN < 0): + startPN = 0 + if (endPN < 0) or (endPN >= len(oPdf.pages)): + endPN = len(oPdf.pages)-1 + for i in range(startPN, endPN+1): + pd = oPdf.pages[i] tPdf = tPdf + pd.extract_text() return { 'status': 200, 'msg': "Pdf2Text Response follows", 'data': tPdf } @@ -407,8 +412,18 @@ def handle_pdf2text(ph: ProxyHandler, pr: urllib.parse.ParseResult): if (not url) or (len(url) == 0): ph.send_error(400, f"WARN:HandlePdf2Text:MissingUrl!") return + startP = queryParams['startPageNumber'][0] + if startP: + startP = int(startP) + else: + startP = -1 + endP = queryParams['endPageNumber'][0] + if endP: + endP = int(endP) + else: + endP = -1 print(f"INFO:HandlePdf2Text:Processing:{url}...") - gotP2T = process_pdf2text(url) + gotP2T = process_pdf2text(url, startP, endP) if (gotP2T['status'] != 200): ph.send_error(gotP2T['status'], gotP2T['msg'] ) return diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index a83ce2c38e..56ddd8ae67 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -284,14 +284,22 @@ let pdf2text_meta = { "type": "function", "function": { "name": "pdf2text", - "description": "Read pdf from requested local file path / web url through a proxy server and return its text content after converting pdf to text, in few seconds", + "description": "Read pdf from requested local file path / web url through a proxy server and return its text content after converting pdf to text, in few seconds. One is allowed to get a part of the pdf by specifying the starting and ending page numbers", "parameters": { "type": "object", "properties": { "url":{ "type":"string", "description":"local file path (file://) / web (http/https) based url of the pdf that will be got and inturn converted to text to an extent" - } + }, + "startPageNumber":{ + "type":"integer", + "description":"Specify the starting page number within the pdf, this is optional. If not specified set to first page." + }, + "endPageNumber":{ + "type":"integer", + "description":"Specify the ending page number within the pdf, this is optional. If not specified set to the last page." + }, }, "required": ["url"] } From c21bef4ddd62b8f6549c73b6748742b18260e4ea Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 04:23:47 +0530 Subject: [PATCH 205/446] SimpleChatTC:Fixup auto toolcall wrt newer ChatShow flow This is a initial go wrt the new overall flow, should work, but need to cross check. --- tools/server/public_simplechat/simplechat.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 317637a59e..379851d79a 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -867,15 +867,16 @@ class MultiChatUI { /** * Reset/Setup Tool Call UI parts as needed * @param {ChatMessageEx} ar + * @param {boolean} bAuto */ - ui_reset_toolcall_as_needed(ar) { + ui_reset_toolcall_as_needed(ar, bAuto = false) { if (ar.has_toolcall()) { this.elDivTool.hidden = false this.elInToolName.value = ar.ns.tool_calls[0].function.name this.elInToolName.dataset.tool_call_id = ar.ns.tool_calls[0].id this.elInToolArgs.value = ar.ns.tool_calls[0].function.arguments this.elBtnTool.disabled = false - if (gMe.tools.auto > 0) { + if ((gMe.tools.auto > 0) && (bAuto)) { this.timers.toolcallTriggerClick = setTimeout(()=>{ this.elBtnTool.click() }, gMe.tools.auto*this.TimePeriods.ToolCallAutoTimeUnit) @@ -976,16 +977,18 @@ class MultiChatUI { } // Handle tool call ui, if reqd let bTC = false + let bAuto = false if (msg.ns.role === Roles.Assistant) { if (iFromLast == 0) { bTC = true + bAuto = true } else if ((iFromLast == 1) && (nextMsg != undefined)) { if (nextMsg.ns.role == Roles.ToolTemp) { bTC = true } } if (bTC) { - this.ui_reset_toolcall_as_needed(msg); + this.ui_reset_toolcall_as_needed(msg, bAuto); } } // Handle tool call non ui From d3a893cac94a1b0cc1e4b89c93bb32671bb31958 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 13:06:40 +0530 Subject: [PATCH 206/446] SimpleChatTC:Update notes --- tools/server/public_simplechat/simplechat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 379851d79a..e8c1fc81d9 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -867,7 +867,7 @@ class MultiChatUI { /** * Reset/Setup Tool Call UI parts as needed * @param {ChatMessageEx} ar - * @param {boolean} bAuto + * @param {boolean} bAuto - allows caller to explicitly control whether auto triggering should be setup. */ ui_reset_toolcall_as_needed(ar, bAuto = false) { if (ar.has_toolcall()) { From c8407a1240fafeb4c4de9d48ae5a15d65aa03068 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 13:07:41 +0530 Subject: [PATCH 207/446] SimpleChatTC:SimpleProxy:UrlValidator module initial skeleton Copy validate_url and build initial skeleton --- .../local.tools/urlvalidator.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tools/server/public_simplechat/local.tools/urlvalidator.py diff --git a/tools/server/public_simplechat/local.tools/urlvalidator.py b/tools/server/public_simplechat/local.tools/urlvalidator.py new file mode 100644 index 0000000000..dec5f11c5a --- /dev/null +++ b/tools/server/public_simplechat/local.tools/urlvalidator.py @@ -0,0 +1,45 @@ +# Handle URL validation +# by Humans for All + +import urllib.parse +import re +from dataclasses import dataclass + + +gMe = { +} + + +@dataclass(frozen=True) +class UrlVResponse: + """ + Used to return result wrt urlreq helper below. + """ + callOk: bool + statusCode: int + statusMsg: str = "" + + +def validator_ok(): + pass + + +def validate_url(url: str, tag: str): + """ + Implement a re based filter logic on the specified url. + """ + tag=f"VU:{tag}" + if (not gMe.get('--allowed.domains')): + return UrlVResponse(False, 400, f"DBUG:{tag}:MissingAllowedDomains") + urlParts = urllib.parse.urlparse(url) + print(f"DBUG:ValidateUrl:{urlParts}, {urlParts.hostname}") + urlHName = urlParts.hostname + if not urlHName: + return UrlVResponse(False, 400, f"WARN:{tag}:Missing hostname in Url") + bMatched = False + for filter in gMe['--allowed.domains']: + if re.match(filter, urlHName): + bMatched = True + if not bMatched: + return UrlVResponse(False, 400, f"WARN:{tag}:requested hostname not allowed") + return UrlVResponse(True, 200) From 6cab95657f8481abd33a482bd8de6e6b587fd15a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 13:39:37 +0530 Subject: [PATCH 208/446] SimpleChatTC:SimpleProxy:UrlValidator initial go Check if the specified scheme is allowed or not. If allowed then call corresponding validator to check remaining part of the url is fine or not --- .../local.tools/urlvalidator.py | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/urlvalidator.py b/tools/server/public_simplechat/local.tools/urlvalidator.py index dec5f11c5a..59f796d430 100644 --- a/tools/server/public_simplechat/local.tools/urlvalidator.py +++ b/tools/server/public_simplechat/local.tools/urlvalidator.py @@ -13,26 +13,27 @@ gMe = { @dataclass(frozen=True) class UrlVResponse: """ - Used to return result wrt urlreq helper below. + Used to return detailed results below. """ callOk: bool statusCode: int statusMsg: str = "" -def validator_ok(): - pass - - -def validate_url(url: str, tag: str): - """ - Implement a re based filter logic on the specified url. - """ - tag=f"VU:{tag}" +def validator_ok(tag: str): if (not gMe.get('--allowed.domains')): return UrlVResponse(False, 400, f"DBUG:{tag}:MissingAllowedDomains") - urlParts = urllib.parse.urlparse(url) - print(f"DBUG:ValidateUrl:{urlParts}, {urlParts.hostname}") + if (not gMe.get('--allowed.schemes')): + return UrlVResponse(False, 400, f"DBUG:{tag}:MissingAllowedSchemes") + return UrlVResponse(True, 100) + + +def validate_fileurl(urlParts: urllib.parse.ParseResult, tag: str): + return UrlVResponse(True, 100) + + +def validate_weburl(urlParts: urllib.parse.ParseResult, tag: str): + # Cross check hostname urlHName = urlParts.hostname if not urlHName: return UrlVResponse(False, 400, f"WARN:{tag}:Missing hostname in Url") @@ -43,3 +44,25 @@ def validate_url(url: str, tag: str): if not bMatched: return UrlVResponse(False, 400, f"WARN:{tag}:requested hostname not allowed") return UrlVResponse(True, 200) + + +def validate_url(url: str, tag: str): + """ + Implement a re based filter logic on the specified url. + """ + tag=f"VU:{tag}" + vok = validator_ok(tag) + if (not vok.callOk): + return vok + urlParts = urllib.parse.urlparse(url) + print(f"DBUG:{tag}:{urlParts}, {urlParts.hostname}") + # Cross check scheme + urlScheme = urlParts.scheme + if not urlScheme: + return UrlVResponse(False, 400, f"WARN:{tag}:Missing scheme in Url") + if not (urlScheme in gMe['--allowed.schemes']): + return UrlVResponse(False, 400, f"WARN:{tag}:requested scheme not allowed") + if urlScheme == 'file': + return validate_fileurl(urlParts, tag) + else: + return validate_weburl(urlParts, tag) From c597572e109c24b1810d6a6cbedfb43546c30b33 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 14:15:07 +0530 Subject: [PATCH 209/446] SimpleChatTC:SimpleProxy: Use urlvalidator Add --allowed.schemes config entry as a needed config. Setup the url validator. Use this wrt urltext, urlraw and pdf2text This allows user to control whether local file access is enabled or not. By default in the sample simpleproxy.json config file local file access is allowed. --- .../local.tools/simpleproxy.json | 5 ++ .../local.tools/simpleproxy.py | 52 +++++-------------- .../local.tools/urlvalidator.py | 13 +++++ 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index 1bae207341..72f7f81cf3 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -1,4 +1,9 @@ { + "allowed.schemes": [ + "file", + "http", + "https" + ], "allowed.domains": [ ".*\\.wikipedia\\.org$", ".*\\.bing\\.com$", diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 03b9a330eb..3b2247cbbd 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -26,6 +26,7 @@ from dataclasses import dataclass import html.parser import re import time +import urlvalidator as uv gMe = { @@ -40,11 +41,12 @@ gConfigType = { '--port': 'int', '--config': 'str', '--debug': 'bool', + '--allowed.schemes': 'list', '--allowed.domains': 'list', '--bearer.insecure': 'str' } -gConfigNeeded = [ '--allowed.domains', '--bearer.insecure' ] +gConfigNeeded = [ '--allowed.schemes', '--allowed.domains', '--bearer.insecure' ] gAllowedCalls = [ "urltext", "urlraw", "pdf2text" ] @@ -195,27 +197,6 @@ def debug_dump(meta: dict, data: dict): f.write(f"\n\n\n\n{k}:{data[k]}\n\n\n\n") -def validate_url(url: str, tag: str): - """ - Implement a re based filter logic on the specified url. - """ - tag=f"VU:{tag}" - if (not gMe.get('--allowed.domains')): - return UrlReqResp(False, 400, f"DBUG:{tag}:MissingAllowedDomains") - urlParts = urllib.parse.urlparse(url) - print(f"DBUG:ValidateUrl:{urlParts}, {urlParts.hostname}") - urlHName = urlParts.hostname - if not urlHName: - return UrlReqResp(False, 400, f"WARN:{tag}:Missing hostname in Url") - bMatched = False - for filter in gMe['--allowed.domains']: - if re.match(filter, urlHName): - bMatched = True - if not bMatched: - return UrlReqResp(False, 400, f"WARN:{tag}:requested hostname not allowed") - return UrlReqResp(True, 200) - - def handle_urlreq(ph: ProxyHandler, pr: urllib.parse.ParseResult, tag: str): """ Common part of the url request handling used by both urlraw and urltext. @@ -234,11 +215,9 @@ def handle_urlreq(ph: ProxyHandler, pr: urllib.parse.ParseResult, tag: str): url = queryParams['url'] print(f"DBUG:{tag}:Url:{url}") url = url[0] - if (not url) or (len(url) == 0): - return UrlReqResp(False, 400, f"WARN:{tag}:MissingUrl") - gotVU = validate_url(url, tag) + gotVU = uv.validate_url(url, tag) if not gotVU.callOk: - return gotVU + return UrlReqResp(gotVU.callOk, gotVU.statusCode, gotVU.statusMsg) try: hUA = ph.headers.get('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0') hAL = ph.headers.get('Accept-Language', "en-US,en;q=0.9") @@ -381,10 +360,11 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): def process_pdf2text(url: str, startPN: int, endPN: int): import pypdf import io - urlParts = url.split('://',1) - if not (urlParts[0] in gAllowedPdfUrlTypes): - return { 'status': 403, 'msg': f"WARN:HandlePdf2Text:ForbiddedUrlType:{urlParts[0]}:AllowedUrlTypes:{gAllowedPdfUrlTypes}" } - fPdf = open(urlParts[1], 'rb') + gotVU = uv.validate_url(url, "HandlePdf2Text") + if not gotVU.callOk: + return { 'status': gotVU.statusCode, 'msg': gotVU.statusMsg } + urlParts = urllib.parse.urlparse(url) + fPdf = open(urlParts.path, 'rb') dPdf = fPdf.read() tPdf = "" oPdf = pypdf.PdfReader(io.BytesIO(dPdf)) @@ -398,20 +378,13 @@ def process_pdf2text(url: str, startPN: int, endPN: int): return { 'status': 200, 'msg': "Pdf2Text Response follows", 'data': tPdf } -gAllowedPdfUrlTypes = [ "file", "http", "https" ] - def handle_pdf2text(ph: ProxyHandler, pr: urllib.parse.ParseResult): """ Handle requests to pdf2text path, which is used to extract plain text from the specified pdf file. """ queryParams = urllib.parse.parse_qs(pr.query) - url = queryParams['url'] - print(f"DBUG:HandlePdf2Text:Url:{url}") - url = url[0] - if (not url) or (len(url) == 0): - ph.send_error(400, f"WARN:HandlePdf2Text:MissingUrl!") - return + url = queryParams['url'][0] startP = queryParams['startPageNumber'][0] if startP: startP = int(startP) @@ -422,7 +395,7 @@ def handle_pdf2text(ph: ProxyHandler, pr: urllib.parse.ParseResult): endP = int(endP) else: endP = -1 - print(f"INFO:HandlePdf2Text:Processing:{url}...") + print(f"INFO:HandlePdf2Text:Processing:{url}:{startP}:{endP}...") gotP2T = process_pdf2text(url, startP, endP) if (gotP2T['status'] != 200): ph.send_error(gotP2T['status'], gotP2T['msg'] ) @@ -509,6 +482,7 @@ def process_args(args: list[str]): if gMe.get(k) == None: print(f"ERRR:ProcessArgs:{k}:missing, did you forget to pass the config file...") exit(104) + uv.validator_setup(gMe['--allowed.schemes'], gMe['--allowed.domains']) def run(): diff --git a/tools/server/public_simplechat/local.tools/urlvalidator.py b/tools/server/public_simplechat/local.tools/urlvalidator.py index 59f796d430..e3fb6b1b32 100644 --- a/tools/server/public_simplechat/local.tools/urlvalidator.py +++ b/tools/server/public_simplechat/local.tools/urlvalidator.py @@ -10,6 +10,12 @@ gMe = { } +def validator_setup(allowedSchemes: list[str], allowedDomains: list[str]): + global gMe + gMe['--allowed.schemes'] = allowedSchemes + gMe['--allowed.domains'] = allowedDomains + + @dataclass(frozen=True) class UrlVResponse: """ @@ -21,6 +27,9 @@ class UrlVResponse: def validator_ok(tag: str): + """ + Cross check validator is setup as needed + """ if (not gMe.get('--allowed.domains')): return UrlVResponse(False, 400, f"DBUG:{tag}:MissingAllowedDomains") if (not gMe.get('--allowed.schemes')): @@ -29,6 +38,8 @@ def validator_ok(tag: str): def validate_fileurl(urlParts: urllib.parse.ParseResult, tag: str): + if urlParts.netloc != '': + return UrlVResponse(False, 400, f"WARN:{tag}:Malformed file url") return UrlVResponse(True, 100) @@ -54,6 +65,8 @@ def validate_url(url: str, tag: str): vok = validator_ok(tag) if (not vok.callOk): return vok + if (not url): + return UrlVResponse(False, 400, f"WARN:{tag}:Missing url") urlParts = urllib.parse.urlparse(url) print(f"DBUG:{tag}:{urlParts}, {urlParts.hostname}") # Cross check scheme From b18aed444968ad3f3439b35026a774919ebe14bd Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 16:24:36 +0530 Subject: [PATCH 210/446] SimpleChatTC:SimpleProxy: AuthAndRun hlpr for paths that check auth Also trap any exceptions while handling and send exception info to the client requesting service --- .../local.tools/simpleproxy.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 3b2247cbbd..b3baf76459 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -24,9 +24,9 @@ import urllib.parse import urllib.request from dataclasses import dataclass import html.parser -import re import time import urlvalidator as uv +from typing import Callable gMe = { @@ -109,6 +109,19 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): return { 'AllOk': False, 'Msg': "Invalid auth" } return { 'AllOk': True, 'Msg': "Auth Ok" } + def auth_and_run(self, pr:urllib.parse.ParseResult, handler:Callable[['ProxyHandler', urllib.parse.ParseResult], None]): + """ + If authorisation is ok for the request, run the specified handler. + """ + acGot = self.auth_check() + if not acGot['AllOk']: + self.send_error(400, f"WARN:{acGot['Msg']}") + else: + try: + handler(self, pr) + except Exception as e: + self.send_error(400, f"ERRR:ProxyHandler:{e}") + def do_GET(self): """ Handle GET requests @@ -119,23 +132,11 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): print(f"DBUG:ProxyHandler:GET:{pr}") match pr.path: case '/urlraw': - acGot = self.auth_check() - if not acGot['AllOk']: - self.send_error(400, f"WARN:{acGot['Msg']}") - else: - handle_urlraw(self, pr) + self.auth_and_run(pr, handle_urlraw) case '/urltext': - acGot = self.auth_check() - if not acGot['AllOk']: - self.send_error(400, f"WARN:{acGot['Msg']}") - else: - handle_urltext(self, pr) + self.auth_and_run(pr, handle_urltext) case '/pdf2text': - acGot = self.auth_check() - if not acGot['AllOk']: - self.send_error(400, f"WARN:{acGot['Msg']}") - else: - handle_pdf2text(self, pr) + self.auth_and_run(pr, handle_pdf2text) case '/aum': handle_aum(self, pr) case _: From a7de002fd04a8d11cad553ece6dc39f49857c587 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 16:36:41 +0530 Subject: [PATCH 211/446] SimpleChatTC:SimpleProxy:Move pdf logic into its own module --- .../public_simplechat/local.tools/pdfmagic.py | 58 +++++++++++++++++++ .../local.tools/simpleproxy.py | 55 +----------------- 2 files changed, 60 insertions(+), 53 deletions(-) create mode 100644 tools/server/public_simplechat/local.tools/pdfmagic.py diff --git a/tools/server/public_simplechat/local.tools/pdfmagic.py b/tools/server/public_simplechat/local.tools/pdfmagic.py new file mode 100644 index 0000000000..407674b0f6 --- /dev/null +++ b/tools/server/public_simplechat/local.tools/pdfmagic.py @@ -0,0 +1,58 @@ +# Helper to manage pdf related requests +# by Humans for All + +import urllib.parse +import urlvalidator as uv +import simpleproxy as root + + +def process_pdf2text(url: str, startPN: int, endPN: int): + import pypdf + import io + gotVU = uv.validate_url(url, "HandlePdf2Text") + if not gotVU.callOk: + return { 'status': gotVU.statusCode, 'msg': gotVU.statusMsg } + urlParts = urllib.parse.urlparse(url) + fPdf = open(urlParts.path, 'rb') + dPdf = fPdf.read() + tPdf = "" + oPdf = pypdf.PdfReader(io.BytesIO(dPdf)) + if (startPN < 0): + startPN = 0 + if (endPN < 0) or (endPN >= len(oPdf.pages)): + endPN = len(oPdf.pages)-1 + for i in range(startPN, endPN+1): + pd = oPdf.pages[i] + tPdf = tPdf + pd.extract_text() + return { 'status': 200, 'msg': "Pdf2Text Response follows", 'data': tPdf } + + +def handle_pdf2text(ph: root.ProxyHandler, pr: urllib.parse.ParseResult): + """ + Handle requests to pdf2text path, which is used to extract plain text + from the specified pdf file. + """ + queryParams = urllib.parse.parse_qs(pr.query) + url = queryParams['url'][0] + startP = queryParams['startPageNumber'][0] + if startP: + startP = int(startP) + else: + startP = -1 + endP = queryParams['endPageNumber'][0] + if endP: + endP = int(endP) + else: + endP = -1 + print(f"INFO:HandlePdf2Text:Processing:{url}:{startP}:{endP}...") + gotP2T = process_pdf2text(url, startP, endP) + if (gotP2T['status'] != 200): + ph.send_error(gotP2T['status'], gotP2T['msg'] ) + return + ph.send_response(gotP2T['status'], gotP2T['msg']) + ph.send_header('Content-Type', 'text/text') + # Add CORS for browser fetch, just in case + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + print(f"INFO:HandlePdf2Text:ExtractedText:{url}...") + ph.wfile.write(gotP2T['data'].encode('utf-8')) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index b3baf76459..2c289a45ae 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -27,6 +27,7 @@ import html.parser import time import urlvalidator as uv from typing import Callable +import pdfmagic as mPdf gMe = { @@ -136,7 +137,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): case '/urltext': self.auth_and_run(pr, handle_urltext) case '/pdf2text': - self.auth_and_run(pr, handle_pdf2text) + self.auth_and_run(pr, mPdf.handle_pdf2text) case '/aum': handle_aum(self, pr) case _: @@ -358,58 +359,6 @@ def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.send_error(502, f"WARN:UrlTextFailed:{exc}") -def process_pdf2text(url: str, startPN: int, endPN: int): - import pypdf - import io - gotVU = uv.validate_url(url, "HandlePdf2Text") - if not gotVU.callOk: - return { 'status': gotVU.statusCode, 'msg': gotVU.statusMsg } - urlParts = urllib.parse.urlparse(url) - fPdf = open(urlParts.path, 'rb') - dPdf = fPdf.read() - tPdf = "" - oPdf = pypdf.PdfReader(io.BytesIO(dPdf)) - if (startPN < 0): - startPN = 0 - if (endPN < 0) or (endPN >= len(oPdf.pages)): - endPN = len(oPdf.pages)-1 - for i in range(startPN, endPN+1): - pd = oPdf.pages[i] - tPdf = tPdf + pd.extract_text() - return { 'status': 200, 'msg': "Pdf2Text Response follows", 'data': tPdf } - - -def handle_pdf2text(ph: ProxyHandler, pr: urllib.parse.ParseResult): - """ - Handle requests to pdf2text path, which is used to extract plain text - from the specified pdf file. - """ - queryParams = urllib.parse.parse_qs(pr.query) - url = queryParams['url'][0] - startP = queryParams['startPageNumber'][0] - if startP: - startP = int(startP) - else: - startP = -1 - endP = queryParams['endPageNumber'][0] - if endP: - endP = int(endP) - else: - endP = -1 - print(f"INFO:HandlePdf2Text:Processing:{url}:{startP}:{endP}...") - gotP2T = process_pdf2text(url, startP, endP) - if (gotP2T['status'] != 200): - ph.send_error(gotP2T['status'], gotP2T['msg'] ) - return - ph.send_response(gotP2T['status'], gotP2T['msg']) - ph.send_header('Content-Type', 'text/text') - # Add CORS for browser fetch, just in case - ph.send_header('Access-Control-Allow-Origin', '*') - ph.end_headers() - print(f"INFO:HandlePdf2Text:ExtractedText:{url}...") - ph.wfile.write(gotP2T['data'].encode('utf-8')) - - def load_config(): """ From 350d7d77e06a2edbd37af621cecc61e58e5081b8 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 16:44:40 +0530 Subject: [PATCH 212/446] SimpleChatTC:SimpleProxy: Move web requests to its own module --- .../local.tools/simpleproxy.py | 181 +----------------- .../public_simplechat/local.tools/webmagic.py | 181 ++++++++++++++++++ 2 files changed, 184 insertions(+), 178 deletions(-) create mode 100644 tools/server/public_simplechat/local.tools/webmagic.py diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 2c289a45ae..bd25156349 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -21,13 +21,11 @@ import sys import http.server import urllib.parse -import urllib.request -from dataclasses import dataclass -import html.parser import time import urlvalidator as uv from typing import Callable import pdfmagic as mPdf +import webmagic as mWeb gMe = { @@ -133,9 +131,9 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): print(f"DBUG:ProxyHandler:GET:{pr}") match pr.path: case '/urlraw': - self.auth_and_run(pr, handle_urlraw) + self.auth_and_run(pr, mWeb.handle_urlraw) case '/urltext': - self.auth_and_run(pr, handle_urltext) + self.auth_and_run(pr, mWeb.handle_urltext) case '/pdf2text': self.auth_and_run(pr, mPdf.handle_pdf2text) case '/aum': @@ -175,18 +173,6 @@ def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.end_headers() -@dataclass(frozen=True) -class UrlReqResp: - """ - Used to return result wrt urlreq helper below. - """ - callOk: bool - httpStatus: int - httpStatusMsg: str = "" - contentType: str = "" - contentData: str = "" - - def debug_dump(meta: dict, data: dict): if not gMe['--debug']: return @@ -199,167 +185,6 @@ def debug_dump(meta: dict, data: dict): f.write(f"\n\n\n\n{k}:{data[k]}\n\n\n\n") -def handle_urlreq(ph: ProxyHandler, pr: urllib.parse.ParseResult, tag: str): - """ - Common part of the url request handling used by both urlraw and urltext. - - Verify the url being requested is allowed. - - Include User-Agent, Accept-Language and Accept in the generated request using - equivalent values got in the request being proxied, so as to try mimic the - real client, whose request we are proxying. In case a header is missing in the - got request, fallback to using some possibly ok enough defaults. - - Fetch the requested url. - """ - tag=f"UrlReq:{tag}" - queryParams = urllib.parse.parse_qs(pr.query) - url = queryParams['url'] - print(f"DBUG:{tag}:Url:{url}") - url = url[0] - gotVU = uv.validate_url(url, tag) - if not gotVU.callOk: - return UrlReqResp(gotVU.callOk, gotVU.statusCode, gotVU.statusMsg) - try: - hUA = ph.headers.get('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0') - hAL = ph.headers.get('Accept-Language', "en-US,en;q=0.9") - hA = ph.headers.get('Accept', "text/html,*/*") - headers = { - 'User-Agent': hUA, - 'Accept': hA, - 'Accept-Language': hAL - } - req = urllib.request.Request(url, headers=headers) - # Get requested url - print(f"DBUG:{tag}:Req:{req.full_url}:{req.headers}") - with urllib.request.urlopen(req, timeout=10) as response: - contentData = response.read().decode('utf-8') - statusCode = response.status or 200 - contentType = response.getheader('Content-Type') or 'text/html' - debug_dump({ 'url': req.full_url, 'headers': req.headers, 'ctype': contentType }, { 'cdata': contentData }) - return UrlReqResp(True, statusCode, "", contentType, contentData) - except Exception as exc: - return UrlReqResp(False, 502, f"WARN:{tag}:Failed:{exc}") - - -def handle_urlraw(ph: ProxyHandler, pr: urllib.parse.ParseResult): - try: - # Get requested url - got = handle_urlreq(ph, pr, "HandleUrlRaw") - if not got.callOk: - ph.send_error(got.httpStatus, got.httpStatusMsg) - return - # Send back to client - ph.send_response(got.httpStatus) - ph.send_header('Content-Type', got.contentType) - # Add CORS for browser fetch, just in case - ph.send_header('Access-Control-Allow-Origin', '*') - ph.end_headers() - ph.wfile.write(got.contentData.encode('utf-8')) - except Exception as exc: - ph.send_error(502, f"WARN:UrlRawFailed:{exc}") - - -class TextHtmlParser(html.parser.HTMLParser): - """ - A simple minded logic used to strip html content of - * all the html tags as well as - * all the contents belonging to below predefined tags like script, style, header, ... - - NOTE: if the html content/page uses any javascript for client side manipulation/generation of - html content, that logic wont be triggered, so also such client side dynamic content wont be - got. - - This helps return a relatively clean textual representation of the html file/content being parsed. - """ - - def __init__(self): - super().__init__() - self.inside = { - 'body': False, - 'script': False, - 'style': False, - 'header': False, - 'footer': False, - 'nav': False - } - self.monitored = [ 'body', 'script', 'style', 'header', 'footer', 'nav' ] - self.bCapture = False - self.text = "" - self.textStripped = "" - - def do_capture(self): - """ - Helps decide whether to capture contents or discard them. - """ - if self.inside['body'] and not (self.inside['script'] or self.inside['style'] or self.inside['header'] or self.inside['footer'] or self.inside['nav']): - return True - return False - - def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]): - if tag in self.monitored: - self.inside[tag] = True - - def handle_endtag(self, tag: str): - if tag in self.monitored: - self.inside[tag] = False - - def handle_data(self, data: str): - if self.do_capture(): - self.text += f"{data}\n" - - def syncup(self): - self.textStripped = self.text - - def strip_adjacent_newlines(self): - oldLen = -99 - newLen = len(self.textStripped) - aStripped = self.textStripped; - while oldLen != newLen: - oldLen = newLen - aStripped = aStripped.replace("\n\n\n","\n") - newLen = len(aStripped) - self.textStripped = aStripped - - def strip_whitespace_lines(self): - aLines = self.textStripped.splitlines() - self.textStripped = "" - for line in aLines: - if (len(line.strip())==0): - self.textStripped += "\n" - continue - self.textStripped += f"{line}\n" - - def get_stripped_text(self): - self.syncup() - self.strip_whitespace_lines() - self.strip_adjacent_newlines() - return self.textStripped - - -def handle_urltext(ph: ProxyHandler, pr: urllib.parse.ParseResult): - try: - # Get requested url - got = handle_urlreq(ph, pr, "HandleUrlText") - if not got.callOk: - ph.send_error(got.httpStatus, got.httpStatusMsg) - return - # Extract Text - textHtml = TextHtmlParser() - textHtml.feed(got.contentData) - # Send back to client - ph.send_response(got.httpStatus) - ph.send_header('Content-Type', got.contentType) - # Add CORS for browser fetch, just in case - ph.send_header('Access-Control-Allow-Origin', '*') - ph.end_headers() - ph.wfile.write(textHtml.get_stripped_text().encode('utf-8')) - debug_dump({ 'RawText': 'yes', 'StrippedText': 'yes' }, { 'RawText': textHtml.text, 'StrippedText': textHtml.get_stripped_text() }) - except Exception as exc: - ph.send_error(502, f"WARN:UrlTextFailed:{exc}") - - - def load_config(): """ Allow loading of a json based config file diff --git a/tools/server/public_simplechat/local.tools/webmagic.py b/tools/server/public_simplechat/local.tools/webmagic.py new file mode 100644 index 0000000000..a4f82f5448 --- /dev/null +++ b/tools/server/public_simplechat/local.tools/webmagic.py @@ -0,0 +1,181 @@ +# Helper to manage web related requests +# by Humans for All + +import urllib.parse +import urllib.request +import simpleproxy as root +import urlvalidator as uv +from dataclasses import dataclass +import html.parser + + +@dataclass(frozen=True) +class UrlReqResp: + """ + Used to return result wrt urlreq helper below. + """ + callOk: bool + httpStatus: int + httpStatusMsg: str = "" + contentType: str = "" + contentData: str = "" + + +def handle_urlreq(ph: root.ProxyHandler, pr: urllib.parse.ParseResult, tag: str): + """ + Common part of the url request handling used by both urlraw and urltext. + + Verify the url being requested is allowed. + + Include User-Agent, Accept-Language and Accept in the generated request using + equivalent values got in the request being proxied, so as to try mimic the + real client, whose request we are proxying. In case a header is missing in the + got request, fallback to using some possibly ok enough defaults. + + Fetch the requested url. + """ + tag=f"UrlReq:{tag}" + queryParams = urllib.parse.parse_qs(pr.query) + url = queryParams['url'] + print(f"DBUG:{tag}:Url:{url}") + url = url[0] + gotVU = uv.validate_url(url, tag) + if not gotVU.callOk: + return UrlReqResp(gotVU.callOk, gotVU.statusCode, gotVU.statusMsg) + try: + hUA = ph.headers.get('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0') + hAL = ph.headers.get('Accept-Language', "en-US,en;q=0.9") + hA = ph.headers.get('Accept', "text/html,*/*") + headers = { + 'User-Agent': hUA, + 'Accept': hA, + 'Accept-Language': hAL + } + req = urllib.request.Request(url, headers=headers) + # Get requested url + print(f"DBUG:{tag}:Req:{req.full_url}:{req.headers}") + with urllib.request.urlopen(req, timeout=10) as response: + contentData = response.read().decode('utf-8') + statusCode = response.status or 200 + contentType = response.getheader('Content-Type') or 'text/html' + root.debug_dump({ 'url': req.full_url, 'headers': req.headers, 'ctype': contentType }, { 'cdata': contentData }) + return UrlReqResp(True, statusCode, "", contentType, contentData) + except Exception as exc: + return UrlReqResp(False, 502, f"WARN:{tag}:Failed:{exc}") + + +def handle_urlraw(ph: root.ProxyHandler, pr: urllib.parse.ParseResult): + try: + # Get requested url + got = handle_urlreq(ph, pr, "HandleUrlRaw") + if not got.callOk: + ph.send_error(got.httpStatus, got.httpStatusMsg) + return + # Send back to client + ph.send_response(got.httpStatus) + ph.send_header('Content-Type', got.contentType) + # Add CORS for browser fetch, just in case + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + ph.wfile.write(got.contentData.encode('utf-8')) + except Exception as exc: + ph.send_error(502, f"WARN:UrlRawFailed:{exc}") + + +class TextHtmlParser(html.parser.HTMLParser): + """ + A simple minded logic used to strip html content of + * all the html tags as well as + * all the contents belonging to below predefined tags like script, style, header, ... + + NOTE: if the html content/page uses any javascript for client side manipulation/generation of + html content, that logic wont be triggered, so also such client side dynamic content wont be + got. + + This helps return a relatively clean textual representation of the html file/content being parsed. + """ + + def __init__(self): + super().__init__() + self.inside = { + 'body': False, + 'script': False, + 'style': False, + 'header': False, + 'footer': False, + 'nav': False + } + self.monitored = [ 'body', 'script', 'style', 'header', 'footer', 'nav' ] + self.bCapture = False + self.text = "" + self.textStripped = "" + + def do_capture(self): + """ + Helps decide whether to capture contents or discard them. + """ + if self.inside['body'] and not (self.inside['script'] or self.inside['style'] or self.inside['header'] or self.inside['footer'] or self.inside['nav']): + return True + return False + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]): + if tag in self.monitored: + self.inside[tag] = True + + def handle_endtag(self, tag: str): + if tag in self.monitored: + self.inside[tag] = False + + def handle_data(self, data: str): + if self.do_capture(): + self.text += f"{data}\n" + + def syncup(self): + self.textStripped = self.text + + def strip_adjacent_newlines(self): + oldLen = -99 + newLen = len(self.textStripped) + aStripped = self.textStripped; + while oldLen != newLen: + oldLen = newLen + aStripped = aStripped.replace("\n\n\n","\n") + newLen = len(aStripped) + self.textStripped = aStripped + + def strip_whitespace_lines(self): + aLines = self.textStripped.splitlines() + self.textStripped = "" + for line in aLines: + if (len(line.strip())==0): + self.textStripped += "\n" + continue + self.textStripped += f"{line}\n" + + def get_stripped_text(self): + self.syncup() + self.strip_whitespace_lines() + self.strip_adjacent_newlines() + return self.textStripped + + +def handle_urltext(ph: root.ProxyHandler, pr: urllib.parse.ParseResult): + try: + # Get requested url + got = handle_urlreq(ph, pr, "HandleUrlText") + if not got.callOk: + ph.send_error(got.httpStatus, got.httpStatusMsg) + return + # Extract Text + textHtml = TextHtmlParser() + textHtml.feed(got.contentData) + # Send back to client + ph.send_response(got.httpStatus) + ph.send_header('Content-Type', got.contentType) + # Add CORS for browser fetch, just in case + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + ph.wfile.write(textHtml.get_stripped_text().encode('utf-8')) + root.debug_dump({ 'RawText': 'yes', 'StrippedText': 'yes' }, { 'RawText': textHtml.text, 'StrippedText': textHtml.get_stripped_text() }) + except Exception as exc: + ph.send_error(502, f"WARN:UrlTextFailed:{exc}") From d012d127bf7e19bb7bdb8a9787633707459bf1e8 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 17:24:36 +0530 Subject: [PATCH 213/446] SimpleChatTC:SimpleProxy: Avoid circular deps wrt Type Checking also move debug dump helper to its own module also remember to specify the Class name in quotes, similar to refering to a class within a member of th class wrt python type checking. --- .../public_simplechat/local.tools/pdfmagic.py | 7 +++++-- .../local.tools/simpleproxy.py | 12 ------------ .../public_simplechat/local.tools/webmagic.py | 17 +++++++++++------ tools/server/public_simplechat/readme.md | 5 +++++ 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/pdfmagic.py b/tools/server/public_simplechat/local.tools/pdfmagic.py index 407674b0f6..29e78e6f0d 100644 --- a/tools/server/public_simplechat/local.tools/pdfmagic.py +++ b/tools/server/public_simplechat/local.tools/pdfmagic.py @@ -3,7 +3,10 @@ import urllib.parse import urlvalidator as uv -import simpleproxy as root +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from simpleproxy import ProxyHandler def process_pdf2text(url: str, startPN: int, endPN: int): @@ -27,7 +30,7 @@ def process_pdf2text(url: str, startPN: int, endPN: int): return { 'status': 200, 'msg': "Pdf2Text Response follows", 'data': tPdf } -def handle_pdf2text(ph: root.ProxyHandler, pr: urllib.parse.ParseResult): +def handle_pdf2text(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): """ Handle requests to pdf2text path, which is used to extract plain text from the specified pdf file. diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index bd25156349..eda88750d0 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -173,18 +173,6 @@ def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.end_headers() -def debug_dump(meta: dict, data: dict): - if not gMe['--debug']: - return - timeTag = f"{time.time():0.12f}" - with open(f"/tmp/simpleproxy.{timeTag}.meta", '+w') as f: - for k in meta: - f.write(f"\n\n\n\n{k}:{meta[k]}\n\n\n\n") - with open(f"/tmp/simpleproxy.{timeTag}.data", '+w') as f: - for k in data: - f.write(f"\n\n\n\n{k}:{data[k]}\n\n\n\n") - - def load_config(): """ Allow loading of a json based config file diff --git a/tools/server/public_simplechat/local.tools/webmagic.py b/tools/server/public_simplechat/local.tools/webmagic.py index a4f82f5448..18944b8711 100644 --- a/tools/server/public_simplechat/local.tools/webmagic.py +++ b/tools/server/public_simplechat/local.tools/webmagic.py @@ -3,10 +3,15 @@ import urllib.parse import urllib.request -import simpleproxy as root import urlvalidator as uv from dataclasses import dataclass import html.parser +import debug +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from simpleproxy import ProxyHandler + @dataclass(frozen=True) @@ -21,7 +26,7 @@ class UrlReqResp: contentData: str = "" -def handle_urlreq(ph: root.ProxyHandler, pr: urllib.parse.ParseResult, tag: str): +def handle_urlreq(ph: 'ProxyHandler', pr: urllib.parse.ParseResult, tag: str): """ Common part of the url request handling used by both urlraw and urltext. @@ -58,13 +63,13 @@ def handle_urlreq(ph: root.ProxyHandler, pr: urllib.parse.ParseResult, tag: str) contentData = response.read().decode('utf-8') statusCode = response.status or 200 contentType = response.getheader('Content-Type') or 'text/html' - root.debug_dump({ 'url': req.full_url, 'headers': req.headers, 'ctype': contentType }, { 'cdata': contentData }) + debug.dump({ 'url': req.full_url, 'headers': req.headers, 'ctype': contentType }, { 'cdata': contentData }) return UrlReqResp(True, statusCode, "", contentType, contentData) except Exception as exc: return UrlReqResp(False, 502, f"WARN:{tag}:Failed:{exc}") -def handle_urlraw(ph: root.ProxyHandler, pr: urllib.parse.ParseResult): +def handle_urlraw(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): try: # Get requested url got = handle_urlreq(ph, pr, "HandleUrlRaw") @@ -159,7 +164,7 @@ class TextHtmlParser(html.parser.HTMLParser): return self.textStripped -def handle_urltext(ph: root.ProxyHandler, pr: urllib.parse.ParseResult): +def handle_urltext(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): try: # Get requested url got = handle_urlreq(ph, pr, "HandleUrlText") @@ -176,6 +181,6 @@ def handle_urltext(ph: root.ProxyHandler, pr: urllib.parse.ParseResult): ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() ph.wfile.write(textHtml.get_stripped_text().encode('utf-8')) - root.debug_dump({ 'RawText': 'yes', 'StrippedText': 'yes' }, { 'RawText': textHtml.text, 'StrippedText': textHtml.get_stripped_text() }) + debug.dump({ 'RawText': 'yes', 'StrippedText': 'yes' }, { 'RawText': textHtml.text, 'StrippedText': textHtml.get_stripped_text() }) except Exception as exc: ph.send_error(502, f"WARN:UrlTextFailed:{exc}") diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 326c9120a3..b64a146b23 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -575,6 +575,11 @@ Trap http response errors and inform user the specific error returned by ai serv Initial go at a pdf2text tool call. For now it allows local pdf files to be read and their text content extracted and passed to ai model for further processing, as decided by ai and end user. +SimpleProxy +* Convert from a single monolithic file into a collection of modules. +* UrlValidator to cross check scheme and domain of requested urls, + the whitelist inturn picked from config json + #### ToDo Is the tool call promise land trap deep enough, need to think through and explore around this once later. From a3beacf16aa9670e1dd7ba22f4028bf82d035a8d Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 18:30:11 +0530 Subject: [PATCH 214/446] SimpleChatTC:SimpleProxy:Pdf2Text cleanup page number handling Its not necessary to request a page number range always. Take care of page number starting from 1 and underlying data having 0 as the starting index --- .../public_simplechat/local.tools/pdfmagic.py | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/pdfmagic.py b/tools/server/public_simplechat/local.tools/pdfmagic.py index 29e78e6f0d..d89496e366 100644 --- a/tools/server/public_simplechat/local.tools/pdfmagic.py +++ b/tools/server/public_simplechat/local.tools/pdfmagic.py @@ -10,6 +10,16 @@ if TYPE_CHECKING: def process_pdf2text(url: str, startPN: int, endPN: int): + """ + Extract textual content from given pdf. + + * Validate the got url. + * Extract textual contents of the pdf from given start page number to end page number (inclusive). + * if -1 | 0 is specified wrt startPN, the actual starting page number (rather 1) will be used. + * if -1 | 0 is specified wrt endPN, the actual ending page number will be used. + + NOTE: Page numbers start from 1, while the underlying list data structure index starts from 0 + """ import pypdf import io gotVU = uv.validate_url(url, "HandlePdf2Text") @@ -20,12 +30,12 @@ def process_pdf2text(url: str, startPN: int, endPN: int): dPdf = fPdf.read() tPdf = "" oPdf = pypdf.PdfReader(io.BytesIO(dPdf)) - if (startPN < 0): - startPN = 0 - if (endPN < 0) or (endPN >= len(oPdf.pages)): - endPN = len(oPdf.pages)-1 + if (startPN <= 0): + startPN = 1 + if (endPN <= 0) or (endPN > len(oPdf.pages)): + endPN = len(oPdf.pages) for i in range(startPN, endPN+1): - pd = oPdf.pages[i] + pd = oPdf.pages[i-1] tPdf = tPdf + pd.extract_text() return { 'status': 200, 'msg': "Pdf2Text Response follows", 'data': tPdf } @@ -37,16 +47,12 @@ def handle_pdf2text(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): """ queryParams = urllib.parse.parse_qs(pr.query) url = queryParams['url'][0] - startP = queryParams['startPageNumber'][0] - if startP: - startP = int(startP) - else: - startP = -1 - endP = queryParams['endPageNumber'][0] - if endP: - endP = int(endP) - else: - endP = -1 + startP = queryParams.get('startPageNumber', -1) + if isinstance(startP, list): + startP = int(startP[0]) + endP = queryParams.get('endPageNumber', -1) + if isinstance(endP, list): + endP = int(endP[0]) print(f"INFO:HandlePdf2Text:Processing:{url}:{startP}:{endP}...") gotP2T = process_pdf2text(url, startP, endP) if (gotP2T['status'] != 200): From 494d063657207eb09377f10cd2c9edde8fdac992 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 20:13:10 +0530 Subject: [PATCH 215/446] SimpleChatTC:SimpleProxy: getting local / web file module ++ Added logic to help get a file from either the local file system or from the web, based on the url specified. Update pdfmagic module to use the same, so that it can support both local as well as web based pdf. Bring in the debug module, which I had forgotten to commit, after moving debug helper code from simpleproxy.py to the debug module --- .../public_simplechat/local.tools/debug.py | 24 ++++++ .../local.tools/filemagic.py | 79 +++++++++++++++++++ .../public_simplechat/local.tools/pdfmagic.py | 9 ++- .../local.tools/simpleproxy.py | 3 + 4 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 tools/server/public_simplechat/local.tools/debug.py create mode 100644 tools/server/public_simplechat/local.tools/filemagic.py diff --git a/tools/server/public_simplechat/local.tools/debug.py b/tools/server/public_simplechat/local.tools/debug.py new file mode 100644 index 0000000000..bf42be631c --- /dev/null +++ b/tools/server/public_simplechat/local.tools/debug.py @@ -0,0 +1,24 @@ +# Helpers for debugging +# by Humans for All + + +import time + +gMe = { '--debug' : False } + + +def setup(bEnable): + global gMe + gMe['--debug'] = bEnable + + +def dump(meta: dict, data: dict): + if not gMe['--debug']: + return + timeTag = f"{time.time():0.12f}" + with open(f"/tmp/simpleproxy.{timeTag}.meta", '+w') as f: + for k in meta: + f.write(f"\n\n\n\n{k}:{meta[k]}\n\n\n\n") + with open(f"/tmp/simpleproxy.{timeTag}.data", '+w') as f: + for k in data: + f.write(f"\n\n\n\n{k}:{data[k]}\n\n\n\n") diff --git a/tools/server/public_simplechat/local.tools/filemagic.py b/tools/server/public_simplechat/local.tools/filemagic.py new file mode 100644 index 0000000000..dfadb08095 --- /dev/null +++ b/tools/server/public_simplechat/local.tools/filemagic.py @@ -0,0 +1,79 @@ +# Handle file related helpers, be it a local file or one on the internet +# by Humans for All + +import urllib.request +import urllib.parse +import debug +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Response: + """ + Used to return result wrt urlreq helper below. + """ + callOk: bool + statusCode: int + statusMsg: str = "" + contentType: str = "" + contentData: bytes = b"" + + + +def get_from_web(url: str, tag: str, inContentType: str, inHeaders: dict[str, str]): + """ + Get the url specified from web. + + If passed header doesnt contain certain useful http header entries, + some predefined defaults will be used in place. + """ + try: + hUA = inHeaders.get('User-Agent', None) + if not hUA: + hUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0' + hAL = inHeaders.get('Accept-Language', None) + if not hAL: + hAL = "en-US,en;q=0.9" + hA = inHeaders.get('Accept', None) + if not hA: + hA = "text/html,*/*" + headers = { + 'User-Agent': hUA, + 'Accept': hA, + 'Accept-Language': hAL + } + req = urllib.request.Request(url, headers=headers) + # Get requested url + print(f"DBUG:{tag}:Req:{req.full_url}:{req.headers}") + with urllib.request.urlopen(req, timeout=10) as response: + contentData = response.read() + statusCode = response.status or 200 + contentType = response.getheader('Content-Type') or inContentType + debug.dump({ 'url': req.full_url, 'headers': req.headers, 'ctype': contentType }, { 'cdata': contentData }) + return Response(True, statusCode, "", contentType, contentData) + except Exception as exc: + return Response(False, 502, f"WARN:{tag}:Failed:{exc}") + + +def get_from_local(urlParts: urllib.parse.ParseResult, tag: str, inContentType: str): + """ + Get the requested file from the local filesystem + """ + try: + fPdf = open(urlParts.path, 'rb') + dPdf = fPdf.read() + return Response(True, 200, "", inContentType, dPdf) + except Exception as exc: + return Response(False, 502, f"WARN:{tag}:Failed:{exc}") + + +def get_file(url: str, tag: str, inContentType: str, inHeaders: dict[str, str]={}): + """ + Based on the scheme specified in the passed url, + either get from local file system or from the web. + """ + urlParts = urllib.parse.urlparse(url) + if urlParts.scheme == "file": + return get_from_local(urlParts, tag, inContentType) + else: + return get_from_web(url, tag, inContentType, inHeaders) diff --git a/tools/server/public_simplechat/local.tools/pdfmagic.py b/tools/server/public_simplechat/local.tools/pdfmagic.py index d89496e366..336a61250a 100644 --- a/tools/server/public_simplechat/local.tools/pdfmagic.py +++ b/tools/server/public_simplechat/local.tools/pdfmagic.py @@ -3,6 +3,7 @@ import urllib.parse import urlvalidator as uv +import filemagic as mFile from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -25,11 +26,11 @@ def process_pdf2text(url: str, startPN: int, endPN: int): gotVU = uv.validate_url(url, "HandlePdf2Text") if not gotVU.callOk: return { 'status': gotVU.statusCode, 'msg': gotVU.statusMsg } - urlParts = urllib.parse.urlparse(url) - fPdf = open(urlParts.path, 'rb') - dPdf = fPdf.read() + gotFile = mFile.get_file(url, "ProcessPdf2Text", "application/pdf", {}) + if not gotFile.callOk: + return { 'status': gotFile.statusCode, 'msg': gotFile.statusMsg, 'data': gotFile.contentData} tPdf = "" - oPdf = pypdf.PdfReader(io.BytesIO(dPdf)) + oPdf = pypdf.PdfReader(io.BytesIO(gotFile.contentData)) if (startPN <= 0): startPN = 1 if (endPN <= 0) or (endPN > len(oPdf.pages)): diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index eda88750d0..4a74c6a254 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -26,6 +26,7 @@ import urlvalidator as uv from typing import Callable import pdfmagic as mPdf import webmagic as mWeb +import debug as mDebug gMe = { @@ -245,9 +246,11 @@ def process_args(args: list[str]): if gMe.get(k) == None: print(f"ERRR:ProcessArgs:{k}:missing, did you forget to pass the config file...") exit(104) + mDebug.setup(gMe['--debug']) uv.validator_setup(gMe['--allowed.schemes'], gMe['--allowed.domains']) + def run(): try: gMe['serverAddr'] = ('', gMe['--port']) From e1cf2bae7ef15a0aabe65018f634db42a052cc60 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 22:11:07 +0530 Subject: [PATCH 216/446] SimpleChatTC:SimpleProxy:Pdf2Text update /cleanup readme --- tools/server/public_simplechat/readme.md | 45 ++++++++++++++---------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index b64a146b23..9a8b586e6e 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -94,13 +94,17 @@ remember to * cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config simpleproxy.json - * remember that this is a relatively minimal dumb proxy logic along with optional stripping of non textual - content like head, scripts, styles, headers, footers, ... Be careful when accessing web through this and - use it only with known safe sites. + * remember that this is a relatively minimal dumb proxy logic which can fetch html or pdf content and + inturn optionally provide plain text version of the content by stripping off non textual/core contents. + Be careful when accessing web through this and use it only with known safe sites. * look into local.tools/simpleproxy.json for specifying + * the white list of allowed.schemes + * you may want to use this to disable local file access and or disable http access, + and inturn retaining only https based urls or so. * the white list of allowed.domains + * review and update this to match your needs. * the shared bearer token between server and client ui * other builtin tool / function calls like calculator, javascript runner, DataStore dont require the @@ -389,15 +393,15 @@ like sessions by getting it to also create and execute mathematical expressions or code to verify such stuff and so. -* access content from internet and augment the ai model's context with additional data as -needed to help generate better responses. this can also be used for +* access content (including html, pdf, text based...) from local file system or the internet +and augment the ai model's context with additional data as needed to help generate better +responses. This can also be used for * generating the latest news summary by fetching from news aggregator sites and collating organising and summarising the same - * searching for specific topics and summarising the results + * searching for specific topics and summarising the search results and or fetching and + analysing found data to generate summary or to explore / answer queries around that data ... * or so -* one could also augment additional data / info by accessing text content from pdf files - * save collated data or generated analysis or more to the provided data store and retrieve them later to augment the analysis / generation then. Also could be used to summarise chat session till a given point and inturn save the summary into data store and later retrieve @@ -444,16 +448,18 @@ Either way always remember to cross check the tool requests and generated respon * search_web_text - search for the specified words using the configured search engine and return the plain textual content from the search result page. +* pdf2text - fetch/read specified pdf file and extract its textual content + * this depends on the pypdf python based open source library + the above set of web related tool calls work by handshaking with a bundled simple local web proxy (/caching in future) server logic, this helps bypass the CORS restrictions applied if trying to directly fetch from the browser js runtime environment. -* pdf2text - fetch/read specified pdf file and extract its textual content +Local file access is also enabled for web fetch and pdf tool calls, if one uses the file:/// scheme +in the url, so be careful as to where and under which user id the simple proxy will be run. - * local file access is enabled for this feature, so be careful as to where and under which user id - the simple proxy will be run. - - * this depends on the pypdf python based open source library +* one can always disable local file access by removing 'file' from the list of allowed.schemes in +simpleproxy.json config file. Implementing some of the tool calls through the simpleproxy.py server and not directly in the browser js env, allows one to isolate the core of these logic within a discardable VM or so, by running the @@ -463,7 +469,7 @@ Depending on the path specified wrt the proxy server, it executes the correspond urltext path is used (and not urlraw), the logic in addition to fetching content from given url, it tries to convert html content into equivalent plain text content to some extent in a simple minded manner by dropping head block as well as all scripts/styles/footers/headers/nav blocks and inturn -dropping the html tags. +also dropping the html tags. Similarly for pdf2text. The client ui logic does a simple check to see if the bundled simpleproxy is running at specified proxyUrl before enabling these web and related tool calls. @@ -475,7 +481,8 @@ The bundled simple proxy * it provides for a basic white list of allowed domains to access, to be specified by the end user. This should help limit web access to a safe set of sites determined by the end user. There is also - a provision for shared bearer token to be specified by the end user. + a provision for shared bearer token to be specified by the end user. One could even control what + schemes are supported wrt the urls. * it tries to mimic the client/browser making the request to it by propogating header entries like user-agent, accept and accept-language from the got request to the generated request during proxying @@ -572,13 +579,15 @@ users) own data or data of ai model. Trap http response errors and inform user the specific error returned by ai server. -Initial go at a pdf2text tool call. For now it allows local pdf files to be read and their text content -extracted and passed to ai model for further processing, as decided by ai and end user. +Initial go at a pdf2text tool call. It allows web / local pdf files to be read and their text content +extracted and passed to ai model for further processing, as decided by ai and end user. One could +either work with the full pdf or a subset of adjacent pages. SimpleProxy * Convert from a single monolithic file into a collection of modules. * UrlValidator to cross check scheme and domain of requested urls, the whitelist inturn picked from config json +* Helpers to fetch file from local file system or the web, transparently #### ToDo @@ -594,8 +603,6 @@ same when saved chat is loaded. MAYBE make the settings in general chat session specific, rather than the current global config flow. -Provide tool to allow for specified pdf files to be converted to equivalent plain text form, so that ai -can be used to work with the content in those PDFs. ### Debuging the handshake and beyond From 3b929f934fd7eeecc11a843cb11b9eab2cafd5b7 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 2 Nov 2025 22:33:57 +0530 Subject: [PATCH 217/446] SimpleChatTC:SimpleProxy:Switch web flow to use file helpers This also indirectly adds support for local file system access through the web / fetch (ie urlraw and urltext) service request paths. --- .../public_simplechat/local.tools/filemagic.py | 4 ++-- .../public_simplechat/local.tools/webmagic.py | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/filemagic.py b/tools/server/public_simplechat/local.tools/filemagic.py index dfadb08095..54e067e258 100644 --- a/tools/server/public_simplechat/local.tools/filemagic.py +++ b/tools/server/public_simplechat/local.tools/filemagic.py @@ -20,7 +20,7 @@ class Response: -def get_from_web(url: str, tag: str, inContentType: str, inHeaders: dict[str, str]): +def get_from_web(url: str, tag: str, inContentType: str, inHeaders: dict[str, str|None]): """ Get the url specified from web. @@ -67,7 +67,7 @@ def get_from_local(urlParts: urllib.parse.ParseResult, tag: str, inContentType: return Response(False, 502, f"WARN:{tag}:Failed:{exc}") -def get_file(url: str, tag: str, inContentType: str, inHeaders: dict[str, str]={}): +def get_file(url: str, tag: str, inContentType: str, inHeaders: dict[str, str|None]={}): """ Based on the scheme specified in the passed url, either get from local file system or from the web. diff --git a/tools/server/public_simplechat/local.tools/webmagic.py b/tools/server/public_simplechat/local.tools/webmagic.py index 18944b8711..9d910a02a5 100644 --- a/tools/server/public_simplechat/local.tools/webmagic.py +++ b/tools/server/public_simplechat/local.tools/webmagic.py @@ -7,6 +7,7 @@ import urlvalidator as uv from dataclasses import dataclass import html.parser import debug +import filemagic as mFile from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -48,23 +49,17 @@ def handle_urlreq(ph: 'ProxyHandler', pr: urllib.parse.ParseResult, tag: str): if not gotVU.callOk: return UrlReqResp(gotVU.callOk, gotVU.statusCode, gotVU.statusMsg) try: - hUA = ph.headers.get('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0') - hAL = ph.headers.get('Accept-Language', "en-US,en;q=0.9") - hA = ph.headers.get('Accept', "text/html,*/*") + hUA = ph.headers.get('User-Agent', None) + hAL = ph.headers.get('Accept-Language', None) + hA = ph.headers.get('Accept', None) headers = { 'User-Agent': hUA, 'Accept': hA, 'Accept-Language': hAL } - req = urllib.request.Request(url, headers=headers) # Get requested url - print(f"DBUG:{tag}:Req:{req.full_url}:{req.headers}") - with urllib.request.urlopen(req, timeout=10) as response: - contentData = response.read().decode('utf-8') - statusCode = response.status or 200 - contentType = response.getheader('Content-Type') or 'text/html' - debug.dump({ 'url': req.full_url, 'headers': req.headers, 'ctype': contentType }, { 'cdata': contentData }) - return UrlReqResp(True, statusCode, "", contentType, contentData) + gotFile = mFile.get_file(url, tag, "text/html", headers) + return UrlReqResp(gotFile.callOk, gotFile.statusCode, gotFile.statusMsg, gotFile.contentType, gotFile.contentData.decode('utf-8')) except Exception as exc: return UrlReqResp(False, 502, f"WARN:{tag}:Failed:{exc}") From 9efab6270252fd7b1ac5a324aa0a44554921a009 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 01:11:35 +0530 Subject: [PATCH 218/446] SimpleChatTC:SimpleProxy:Add generic arxiv.org entry to allowed --- tools/server/public_simplechat/local.tools/simpleproxy.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index 72f7f81cf3..77261bd1a2 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -17,6 +17,7 @@ "^duckduckgo\\.com$", ".*\\.google\\.com$", "^google\\.com$", + ".*\\.arxiv\\.org$", "^arxiv\\.org$", ".*\\.nature\\.com$", ".*\\.science\\.org$", From e10a826273de2d7c0ff45eadcecc948cfc4a33d0 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 01:45:38 +0530 Subject: [PATCH 219/446] SimpleChatTC: Cleanup - remove older now unused show chat logic --- tools/server/public_simplechat/simplechat.js | 41 -------------------- 1 file changed, 41 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index e8c1fc81d9..1bbb0890f5 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -477,47 +477,6 @@ class SimpleChat { this.xchat[lastIndex].ns.content = content; } - /** - * Show the chat contents in the specified div. - * Also update the user query input box, with ToolTemp role message, if any. - * - * If requested to clear prev stuff and inturn no chat content then show - * * usage info - * * option to load prev saved chat if any - * * as well as settings/info. - * @param {HTMLDivElement} div - * @param {HTMLInputElement} elInUser - * @param {boolean} bClear - * @param {boolean} bShowInfoAll - */ - showTOREMOVE(div, elInUser, bClear=true, bShowInfoAll=false) { - if (bClear) { - div.replaceChildren(); - } - let last = undefined; - for(const [i, x] of this.recent_chat(gMe.chatProps.iRecentUserMsgCnt).entries()) { - if (x.ns.role === Roles.ToolTemp) { - if (i == (this.xchat.length - 1)) { - elInUser.value = x.ns.content; - } - continue - } - let entry = ui.el_create_append_p(`${x.ns.role}: ${x.content_equiv()}`, div); - entry.className = `role-${x.ns.role}`; - last = entry; - } - if (last !== undefined) { - last.scrollIntoView(false); - } else { - if (bClear) { - div.innerHTML = gUsageMsg; - gMe.setup_load(div, this); - gMe.show_info(div, bShowInfoAll); - } - } - return last; - } - /** * Setup the fetch headers. * It picks the headers from gMe.headers. From a4483e3bc79868f69f7a365fd023e19484611033 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 03:24:07 +0530 Subject: [PATCH 220/446] SimpleChatTC:Cleanup Usage Note and its presentation a bit Make it a details block and update the content a bit --- tools/server/public_simplechat/simplechat.css | 4 ++++ tools/server/public_simplechat/simplechat.js | 11 +++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.css b/tools/server/public_simplechat/simplechat.css index 1f913fa272..b0edd777da 100644 --- a/tools/server/public_simplechat/simplechat.css +++ b/tools/server/public_simplechat/simplechat.css @@ -28,6 +28,10 @@ background-color: lightpink; } +#UsageNote { + margin: 0.0vmin; +} + .chat-message { border-style: solid; border-color: grey; diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 1bbb0890f5..d578001e82 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -278,16 +278,18 @@ class ChatMessageEx { let gUsageMsg = ` -

      Usage

      +
      + Usage Note
        -
      • System prompt above, to try control ai response characteristics.
      • +
      • System prompt above, helps control ai response characteristics.
        • Completion mode - no system prompt normally.
      • Use shift+enter for inserting enter/newline.
      • -
      • Enter your query to ai assistant in textarea provided below.
      • -
      • If ai assistant requests a tool call, varify same before triggering it.
      • +
      • Enter your query/response to ai assistant in textarea provided below.
      • +
      • settings-tools-enable should be true to enable tool calling.
        • +
        • If ai assistant requests a tool call, verify same before triggering.
        • submit tool response placed into user query textarea
      • Default ContextWindow = [System, Last9 Query+Resp, Cur Query].
      • @@ -295,6 +297,7 @@ let gUsageMsg = `
      • ChatHistInCtxt, MaxTokens, ModelCtxt window to expand
    + `; From 8501759f60a737225d8a2cd2d670c6532266f764 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 12:37:59 +0530 Subject: [PATCH 221/446] SimpleChatTC:Cleanup:UsageNote, Initial SettingsInfo shown Usage Note * Cleanup / fix some wording. * Pick chat history handshaked len from config Ensure the settings info is uptodate wrt available tool names by chaining a reshowing with tools manager initialisation. --- tools/server/public_simplechat/simplechat.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index d578001e82..297f76366b 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -277,7 +277,8 @@ class ChatMessageEx { } -let gUsageMsg = ` +function usage_note() { + let sUsageNote = `
    Usage Note
      @@ -286,19 +287,20 @@ let gUsageMsg = `
    • Completion mode - no system prompt normally.
  • Use shift+enter for inserting enter/newline.
  • -
  • Enter your query/response to ai assistant in textarea provided below.
  • -
  • settings-tools-enable should be true to enable tool calling.
  • +
  • Enter your query/response to ai assistant in text area provided below.
  • +
  • settings-tools-enabled should be true to enable tool calling.
    • If ai assistant requests a tool call, verify same before triggering.
    • -
    • submit tool response placed into user query textarea
    • +
    • submit tool response placed into user query/response text area
    -
  • Default ContextWindow = [System, Last9 Query+Resp, Cur Query].
  • +
  • ContextWindow = [System, Last[${gMe.chatProps.iRecentUserMsgCnt-1}] User Query/Resp, Cur Query].
    • ChatHistInCtxt, MaxTokens, ModelCtxt window to expand
    -
    -`; + `; + return sUsageNote; +} /** @typedef {ChatMessageEx[]} ChatMessages */ @@ -1001,7 +1003,7 @@ class MultiChatUI { /** @type{HTMLElement} */(this.elLastChatMessage).scrollIntoView(false); // Stupid ts-check js-doc intersection ??? } else { if (bClear) { - this.elDivChat.innerHTML = gUsageMsg; + this.elDivChat.innerHTML = usage_note(); gMe.setup_load(this.elDivChat, chat); gMe.show_info(this.elDivChat, bShowInfoAll); } @@ -1457,7 +1459,7 @@ function startme() { document["du"] = du; // @ts-ignore document["tools"] = tools; - tools.init().then((toolNames)=>gMe.tools.toolNames=toolNames) + tools.init().then((toolNames)=>gMe.tools.toolNames=toolNames).then(()=>gMe.multiChat.chat_show(gMe.multiChat.curChatId)) for (let cid of gMe.defaultChatIds) { gMe.multiChat.new_chat_session(cid); } From 1d1894ad144bfe46701f52ae3ce474f2f4fb30bb Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 12:12:41 +0530 Subject: [PATCH 222/446] SimpleChatTC:PdfText:Cleanup rename to follow a common convention Rename path and tags/identifiers from Pdf2Text to PdfText Rename the function call to pdf_to_text, this should also help indicate semantic more unambiguously, just in case, especially for smaller models. --- .../public_simplechat/local.tools/pdfmagic.py | 19 ++++++++------- .../local.tools/simpleproxy.py | 6 ++--- tools/server/public_simplechat/readme.md | 6 ++--- tools/server/public_simplechat/toolweb.mjs | 24 +++++++++---------- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/pdfmagic.py b/tools/server/public_simplechat/local.tools/pdfmagic.py index 336a61250a..971ba2c796 100644 --- a/tools/server/public_simplechat/local.tools/pdfmagic.py +++ b/tools/server/public_simplechat/local.tools/pdfmagic.py @@ -10,11 +10,12 @@ if TYPE_CHECKING: from simpleproxy import ProxyHandler -def process_pdf2text(url: str, startPN: int, endPN: int): +def process_pdftext(url: str, startPN: int, endPN: int): """ Extract textual content from given pdf. * Validate the got url. + * Get the pdf file. * Extract textual contents of the pdf from given start page number to end page number (inclusive). * if -1 | 0 is specified wrt startPN, the actual starting page number (rather 1) will be used. * if -1 | 0 is specified wrt endPN, the actual ending page number will be used. @@ -23,10 +24,10 @@ def process_pdf2text(url: str, startPN: int, endPN: int): """ import pypdf import io - gotVU = uv.validate_url(url, "HandlePdf2Text") + gotVU = uv.validate_url(url, "HandlePdfText") if not gotVU.callOk: return { 'status': gotVU.statusCode, 'msg': gotVU.statusMsg } - gotFile = mFile.get_file(url, "ProcessPdf2Text", "application/pdf", {}) + gotFile = mFile.get_file(url, "ProcessPdfText", "application/pdf", {}) if not gotFile.callOk: return { 'status': gotFile.statusCode, 'msg': gotFile.statusMsg, 'data': gotFile.contentData} tPdf = "" @@ -38,12 +39,12 @@ def process_pdf2text(url: str, startPN: int, endPN: int): for i in range(startPN, endPN+1): pd = oPdf.pages[i-1] tPdf = tPdf + pd.extract_text() - return { 'status': 200, 'msg': "Pdf2Text Response follows", 'data': tPdf } + return { 'status': 200, 'msg': "PdfText Response follows", 'data': tPdf } -def handle_pdf2text(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): +def handle_pdftext(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): """ - Handle requests to pdf2text path, which is used to extract plain text + Handle requests to pdftext path, which is used to extract plain text from the specified pdf file. """ queryParams = urllib.parse.parse_qs(pr.query) @@ -54,8 +55,8 @@ def handle_pdf2text(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): endP = queryParams.get('endPageNumber', -1) if isinstance(endP, list): endP = int(endP[0]) - print(f"INFO:HandlePdf2Text:Processing:{url}:{startP}:{endP}...") - gotP2T = process_pdf2text(url, startP, endP) + print(f"INFO:HandlePdfText:Processing:{url}:{startP}:{endP}...") + gotP2T = process_pdftext(url, startP, endP) if (gotP2T['status'] != 200): ph.send_error(gotP2T['status'], gotP2T['msg'] ) return @@ -64,5 +65,5 @@ def handle_pdf2text(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): # Add CORS for browser fetch, just in case ph.send_header('Access-Control-Allow-Origin', '*') ph.end_headers() - print(f"INFO:HandlePdf2Text:ExtractedText:{url}...") + print(f"INFO:HandlePdfText:ExtractedText:{url}...") ph.wfile.write(gotP2T['data'].encode('utf-8')) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 4a74c6a254..862951f56a 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -48,7 +48,7 @@ gConfigType = { gConfigNeeded = [ '--allowed.schemes', '--allowed.domains', '--bearer.insecure' ] -gAllowedCalls = [ "urltext", "urlraw", "pdf2text" ] +gAllowedCalls = [ "urltext", "urlraw", "pdftext" ] def bearer_transform(): @@ -135,8 +135,8 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): self.auth_and_run(pr, mWeb.handle_urlraw) case '/urltext': self.auth_and_run(pr, mWeb.handle_urltext) - case '/pdf2text': - self.auth_and_run(pr, mPdf.handle_pdf2text) + case '/pdftext': + self.auth_and_run(pr, mPdf.handle_pdftext) case '/aum': handle_aum(self, pr) case _: diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 9a8b586e6e..e3a835df0d 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -448,7 +448,7 @@ Either way always remember to cross check the tool requests and generated respon * search_web_text - search for the specified words using the configured search engine and return the plain textual content from the search result page. -* pdf2text - fetch/read specified pdf file and extract its textual content +* pdf_to_text - fetch/read specified pdf file and extract its textual content * this depends on the pypdf python based open source library the above set of web related tool calls work by handshaking with a bundled simple local web proxy @@ -469,7 +469,7 @@ Depending on the path specified wrt the proxy server, it executes the correspond urltext path is used (and not urlraw), the logic in addition to fetching content from given url, it tries to convert html content into equivalent plain text content to some extent in a simple minded manner by dropping head block as well as all scripts/styles/footers/headers/nav blocks and inturn -also dropping the html tags. Similarly for pdf2text. +also dropping the html tags. Similarly for pdftext. The client ui logic does a simple check to see if the bundled simpleproxy is running at specified proxyUrl before enabling these web and related tool calls. @@ -579,7 +579,7 @@ users) own data or data of ai model. Trap http response errors and inform user the specific error returned by ai server. -Initial go at a pdf2text tool call. It allows web / local pdf files to be read and their text content +Initial go at a pdftext tool call. It allows web / local pdf files to be read and their text content extracted and passed to ai model for further processing, as decided by ai and end user. One could either work with the full pdf or a subset of adjacent pages. diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 56ddd8ae67..ba9ad93bfb 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -276,14 +276,14 @@ async function searchwebtext_setup(tcs) { // -// Pdf2Text +// PdfText // -let pdf2text_meta = { +let pdftext_meta = { "type": "function", "function": { - "name": "pdf2text", + "name": "pdf_to_text", "description": "Read pdf from requested local file path / web url through a proxy server and return its text content after converting pdf to text, in few seconds. One is allowed to get a part of the pdf by specifying the starting and ending page numbers", "parameters": { "type": "object", @@ -312,7 +312,7 @@ let pdf2text_meta = { * Expects a simple minded proxy server to be running locally * * listening on a configured port * * expecting http requests - * * with a query token named url wrt pdf2text path, + * * with a query token named url wrt pdftext path, * which gives the actual url to fetch * * gets the requested pdf and converts to text, before returning same. * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful @@ -321,20 +321,20 @@ let pdf2text_meta = { * @param {string} toolname * @param {any} obj */ -function pdf2text_run(chatid, toolcallid, toolname, obj) { - return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'pdf2text'); +function pdftext_run(chatid, toolcallid, toolname, obj) { + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'pdftext'); } /** - * Setup pdf2text for tool calling + * Setup pdftext for tool calling * NOTE: Currently the logic is setup for the bundled simpleproxy.py * @param {Object>} tcs */ -async function pdf2text_setup(tcs) { - return proxyserver_tc_setup('Pdf2Text', 'pdf2text', 'pdf2text', { - "handler": pdf2text_run, - "meta": pdf2text_meta, +async function pdftext_setup(tcs) { + return proxyserver_tc_setup('PdfText', 'pdftext', 'pdf_to_text', { + "handler": pdftext_run, + "meta": pdftext_meta, "result": "" }, tcs); } @@ -355,6 +355,6 @@ export async function init(toolsWorker) { await fetchweburlraw_setup(tc_switch) await fetchweburltext_setup(tc_switch) await searchwebtext_setup(tc_switch) - await pdf2text_setup(tc_switch) + await pdftext_setup(tc_switch) return tc_switch } From e6fd0ed05aacaae823cd5bf17dd2a6327d589658 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 13:02:56 +0530 Subject: [PATCH 223/446] SimpleChatTC: ToolCalling enabled, Sliding window adjust Chances are for ai models which dont support tool calling, things will be such that the tool calls meta data shared will be silently ignored without much issue. So enabling tool calling feature by default, so that in case one is using a ai model with tool calling the feature is readily available for use. Revert SlidingWindow ChatHistory in Context from last 10 to last 5 (2 more then origianl, given more context support in todays models) by default, given that now tool handshakes go through the tools related side channel in the http handshake and arent morphed into normal user-assistant channel of the handshake. --- tools/server/public_simplechat/readme.md | 10 ++++++++++ tools/server/public_simplechat/simplechat.js | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index e3a835df0d..6a237918ee 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -589,6 +589,16 @@ SimpleProxy the whitelist inturn picked from config json * Helpers to fetch file from local file system or the web, transparently +Chances are for ai models which dont support tool calling, things will be such that the tool calls +meta data shared will be silently ignored without much issue. So enabling tool calling feature by default, +so that in case one is using a ai model with tool calling the feature is readily available for use. + +Revert SlidingWindow ChatHistory in Context from last 10 to last 5 (rather 2 more then origianl, +given more context support in todays models) by default, given that now tool handshakes go through +the tools related side channel in the http handshake and arent morphed into normal user-assistant +channel of the handshake. + + #### ToDo Is the tool call promise land trap deep enough, need to think through and explore around this once later. diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 297f76366b..b145938af6 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1304,7 +1304,7 @@ class Me { this.defaultChatIds = [ "Default", "Other" ]; this.multiChat = new MultiChatUI(); this.tools = { - enabled: false, + enabled: true, proxyUrl: "http://127.0.0.1:3128", proxyAuthInsecure: "NeverSecure", searchUrl: SearchURLS.duckduckgo, @@ -1328,7 +1328,7 @@ class Me { this.chatProps = { apiEP: ApiEP.Type.Chat, stream: true, - iRecentUserMsgCnt: 10, + iRecentUserMsgCnt: 5, bCompletionFreshChatAlways: true, bCompletionInsertStandardRolePrefix: false, bTrimGarbage: true, From 2cdf3f574cc785397126ffcd627ae736690a08e9 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 14:35:23 +0530 Subject: [PATCH 224/446] SimpleChatTC:SimpleProxy: Validate deps wrt enabled service paths helps ensure only service paths that can be serviced are enabled Use same to check for pypdf wrt pdftext --- .../local.tools/simpleproxy.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 862951f56a..50a5691703 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -48,7 +48,11 @@ gConfigType = { gConfigNeeded = [ '--allowed.schemes', '--allowed.domains', '--bearer.insecure' ] -gAllowedCalls = [ "urltext", "urlraw", "pdftext" ] +gAllowedCalls = { + "urltext": [], + "urlraw": [], + "pdftext": [ "pypdf" ] + } def bearer_transform(): @@ -157,6 +161,7 @@ def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): Handle requests to aum path, which is used in a simple way to verify that one is communicating with this proxy server """ + import importlib queryParams = urllib.parse.parse_qs(pr.query) url = queryParams['url'] print(f"DBUG:HandleAUM:Url:{url}") @@ -165,9 +170,15 @@ def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.send_error(400, f"WARN:HandleAUM:MissingUrl/UnknownQuery?!") return urlParts = url.split('.',1) - if not (urlParts[0] in gAllowedCalls): - ph.send_error(403, f"WARN:HandleAUM:Forbidded:{urlParts[0]}") + if gAllowedCalls.get(urlParts[0], None) == None: + ph.send_error(403, f"WARN:HandleAUM:Forbidden:{urlParts[0]}") return + for dep in gAllowedCalls[urlParts[0]]: + try: + importlib.import_module(dep) + except ImportError as exc: + ph.send_error(400, f"WARN:HandleAUM:{urlParts[0]}:Support module [{dep}] missing or has issues") + return print(f"INFO:HandleAUM:Availability ok for:{urlParts[0]}") ph.send_response_only(200, "bharatavarshe") ph.send_header('Access-Control-Allow-Origin', '*') From 7fce3eeb2acbfb0476aa0dc7b82ce3106c5ee08a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 18:57:29 +0530 Subject: [PATCH 225/446] SimpleChatTC:SettingsDefault:Enable cache prompt api option --- tools/server/public_simplechat/readme.md | 19 ++++++++++++++----- tools/server/public_simplechat/simplechat.js | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 6a237918ee..9244a30645 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -583,21 +583,30 @@ Initial go at a pdftext tool call. It allows web / local pdf files to be read an extracted and passed to ai model for further processing, as decided by ai and end user. One could either work with the full pdf or a subset of adjacent pages. -SimpleProxy +SimpleProxy updates * Convert from a single monolithic file into a collection of modules. * UrlValidator to cross check scheme and domain of requested urls, the whitelist inturn picked from config json * Helpers to fetch file from local file system or the web, transparently +* Help check for needed modules before a particular service path is acknowledged as available + through /aum service path -Chances are for ai models which dont support tool calling, things will be such that the tool calls -meta data shared will be silently ignored without much issue. So enabling tool calling feature by default, -so that in case one is using a ai model with tool calling the feature is readily available for use. +Settings/Config default changes -Revert SlidingWindow ChatHistory in Context from last 10 to last 5 (rather 2 more then origianl, +* Chances are for ai models which dont support tool calling, things will be such that the tool calls +meta data shared will be silently ignored without much issue. So enabling tool calling feature by +default, so that in case one is using a ai model with tool calling the feature is readily available +for use. + +* Revert SlidingWindow ChatHistory in Context from last 10 to last 5 (rather 2 more then origianl, given more context support in todays models) by default, given that now tool handshakes go through the tools related side channel in the http handshake and arent morphed into normal user-assistant channel of the handshake. +* Enable CachePrompt api option given that tool calling based interactions could involve chat sessions +having ai responses built over multiple steps of tool callings etal. So independent of our client side +sliding window based drop off or even before they kick in, this can help in many cases. + #### ToDo diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index b145938af6..9fecef808d 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1356,7 +1356,7 @@ class Me { "temperature": 0.7, "max_tokens": 2048, "n_predict": 2048, - "cache_prompt": false, + "cache_prompt": true, //"frequency_penalty": 1.2, //"presence_penalty": 1.2, }; From f75bdb0e00088f5b18c71a89edfa9fee135fc421 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 20:02:56 +0530 Subject: [PATCH 226/446] SimpleChatTC:WebTools And Search - headers and search drops - js Allow the web tools handshake helper to pass additional header entries provided by its caller. Make use of this to send a list of tag and id pairs wrt web search tool. Which will be used to drop div's matching the specified id. --- tools/server/public_simplechat/simplechat.js | 20 +++++++++++++++----- tools/server/public_simplechat/toolweb.mjs | 14 +++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 9fecef808d..26e33904ff 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1290,10 +1290,19 @@ class MultiChatUI { * The SEARCHWORDS keyword will get replaced by the actual user specified search words at runtime. */ const SearchURLS = { - duckduckgo: "https://duckduckgo.com/html/?q=SEARCHWORDS", - bing: "https://www.bing.com/search?q=SEARCHWORDS", // doesnt seem to like google chrome clients in particular - brave: "https://search.brave.com/search?q=SEARCHWORDS", - google: "https://www.google.com/search?q=SEARCHWORDS", // doesnt seem to like any client in general + duckduckgo: { + 'template': "https://duckduckgo.com/html/?q=SEARCHWORDS", + 'drop': [ { 'tag': 'div', 'id': "header" } ] + }, + bing: { + 'template': "https://www.bing.com/search?q=SEARCHWORDS", // doesnt seem to like google chrome clients in particular + }, + brave: { + 'template': "https://search.brave.com/search?q=SEARCHWORDS", + }, + google: { + 'template': "https://www.google.com/search?q=SEARCHWORDS", // doesnt seem to like any client in general + }, } @@ -1307,7 +1316,8 @@ class Me { enabled: true, proxyUrl: "http://127.0.0.1:3128", proxyAuthInsecure: "NeverSecure", - searchUrl: SearchURLS.duckduckgo, + searchUrl: SearchURLS.duckduckgo.template, + searchDrops: SearchURLS.duckduckgo.drop, toolNames: /** @type {Array} */([]), /** * Control the length of the tool call result data returned to ai after tool call. diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index ba9ad93bfb..e473e250dd 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -49,15 +49,18 @@ function bearer_transform() { * @param {string} chatid * @param {string} toolcallid * @param {string} toolname - * @param {any} obj + * @param {any} objSearchParams * @param {string} path + * @param {any} objHeaders */ -async function proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, path) { +async function proxyserver_get_anyargs(chatid, toolcallid, toolname, objSearchParams, path, objHeaders={}) { if (gToolsWorker.onmessage != null) { - let params = new URLSearchParams(obj) + let params = new URLSearchParams(objSearchParams) let newUrl = `${get_gme().tools.proxyUrl}/${path}?${params}` + let headers = new Headers(objHeaders) let btoken = await bearer_transform() - fetch(newUrl, { headers: { 'Authorization': `Bearer ${btoken}` }}).then(resp => { + headers.append('Authorization', `Bearer ${btoken}`) + fetch(newUrl, { headers: headers}).then(resp => { if (!resp.ok) { throw new Error(`${resp.status}:${resp.statusText}`); } @@ -256,7 +259,8 @@ function searchwebtext_run(chatid, toolcallid, toolname, obj) { searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)); delete(obj.words) obj['url'] = searchUrl - return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urltext'); + let headers = { 'Search-Drops': get_gme().tools.searchDrops } + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urltext', headers); } } From 06fd41a88eeae7de26e7bb60d816d5a648de5924 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 20:59:18 +0530 Subject: [PATCH 227/446] SimpleChatTC:WebTools: urltext-tag-drops python side - skel Rename search-drops to urltext-tag-drops, to indicate its more generic semantic. Rather search drops specified in UI by user will be mapped to urltext-tag-drops header entry of a urltext web fetch request. Implement a crude urltext-tag-drops logic in TextHtmlParser. If there is any mismatch with opening and closing tags in the html being parsed and inturn wrt the type of tag being targetted for dropping, things can mess up. --- .../public_simplechat/local.tools/webmagic.py | 30 ++++++++++++++++--- tools/server/public_simplechat/toolweb.mjs | 2 +- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/webmagic.py b/tools/server/public_simplechat/local.tools/webmagic.py index 9d910a02a5..d371fa736e 100644 --- a/tools/server/public_simplechat/local.tools/webmagic.py +++ b/tools/server/public_simplechat/local.tools/webmagic.py @@ -8,6 +8,7 @@ from dataclasses import dataclass import html.parser import debug import filemagic as mFile +import json from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -95,36 +96,52 @@ class TextHtmlParser(html.parser.HTMLParser): This helps return a relatively clean textual representation of the html file/content being parsed. """ - def __init__(self): + def __init__(self, tagDrops: dict): super().__init__() + self.tagDrops = tagDrops self.inside = { 'body': False, 'script': False, 'style': False, 'header': False, 'footer': False, - 'nav': False + 'nav': False, } self.monitored = [ 'body', 'script', 'style', 'header', 'footer', 'nav' ] self.bCapture = False self.text = "" self.textStripped = "" + self.droptagType = None + self.droptagCount = 0 def do_capture(self): """ Helps decide whether to capture contents or discard them. """ - if self.inside['body'] and not (self.inside['script'] or self.inside['style'] or self.inside['header'] or self.inside['footer'] or self.inside['nav']): + if self.inside['body'] and not (self.inside['script'] or self.inside['style'] or self.inside['header'] or self.inside['footer'] or self.inside['nav'] or (self.droptagCount > 0)): return True return False def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]): if tag in self.monitored: self.inside[tag] = True + for tagMeta in self.tagDrops: + if tag != tagMeta.tag: + continue + for attr in attrs: + if attr[0] != 'id': + continue + if attr[1] == tagMeta.id: + self.droptagCount += 1 + self.droptagType = tag def handle_endtag(self, tag: str): if tag in self.monitored: self.inside[tag] = False + if tag == self.droptagType: + self.droptagCount -= 1 + if self.droptagCount < 0: + self.droptagCount = 0 def handle_data(self, data: str): if self.do_capture(): @@ -167,7 +184,12 @@ def handle_urltext(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): ph.send_error(got.httpStatus, got.httpStatusMsg) return # Extract Text - textHtml = TextHtmlParser() + tagDrops = ph.headers.get('urltext-tag-drops') + if not tagDrops: + tagDrops = {} + else: + tagDrops = json.loads(tagDrops) + textHtml = TextHtmlParser(tagDrops) textHtml.feed(got.contentData) # Send back to client ph.send_response(got.httpStatus) diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index e473e250dd..f52aca4357 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -259,7 +259,7 @@ function searchwebtext_run(chatid, toolcallid, toolname, obj) { searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)); delete(obj.words) obj['url'] = searchUrl - let headers = { 'Search-Drops': get_gme().tools.searchDrops } + let headers = { 'urltext-tag-drops': get_gme().tools.searchDrops } return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urltext', headers); } } From c316f5a2bdc304b2502c45c2202d50993a965125 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 21:42:48 +0530 Subject: [PATCH 228/446] SimpleChatTC:WebTools:UrlText:HtmlParser: tag drops - refine Update the initial skeleton wrt the tag drops logic * had forgotten to convert object to json string at the client end * had confused between js and python and tried accessing the dict elements using . notation rather than [] notation in python. * if the id filtered tag to be dropped is found, from then on track all other tags of the same type (independent of id), so that start and end tags can be matched. bcas end tag call wont have attribute, so all other tags of same type need to be tracked, for proper winding and unwinding to try find matching end tag * remember to reset the tracked drop tag type to None once matching end tag at same depth is found. should avoid some unnecessary unwinding. * set/fix the type wrt tagDrops explicitly to needed depth and ensure the dummy one and any explicitly got one is of right type. Tested with duckduckgo search engine and now the div based unneeded header is avoided in returned search result. --- .../public_simplechat/local.tools/webmagic.py | 30 ++++++++++++++----- tools/server/public_simplechat/readme.md | 7 +++++ tools/server/public_simplechat/toolweb.mjs | 2 +- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/webmagic.py b/tools/server/public_simplechat/local.tools/webmagic.py index d371fa736e..2e3d5811cc 100644 --- a/tools/server/public_simplechat/local.tools/webmagic.py +++ b/tools/server/public_simplechat/local.tools/webmagic.py @@ -9,7 +9,7 @@ import html.parser import debug import filemagic as mFile import json -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: from simpleproxy import ProxyHandler @@ -93,12 +93,21 @@ class TextHtmlParser(html.parser.HTMLParser): html content, that logic wont be triggered, so also such client side dynamic content wont be got. + Supports one to specify a list of tags and their corresponding id attributes, so that contents + within such specified blocks will be dropped. + + * this works properly only if the html being processed has proper opening and ending tags + around the area of interest. + * remember to specify non overlapping tag blocks, if more than one specified for dropping. + * this path not tested, but should logically work + This helps return a relatively clean textual representation of the html file/content being parsed. """ - def __init__(self, tagDrops: dict): + def __init__(self, tagDrops: list[dict[str, Any]]): super().__init__() self.tagDrops = tagDrops + print(f"DBUG:TextHtmlParser:{self.tagDrops}") self.inside = { 'body': False, 'script': False, @@ -126,20 +135,27 @@ class TextHtmlParser(html.parser.HTMLParser): if tag in self.monitored: self.inside[tag] = True for tagMeta in self.tagDrops: - if tag != tagMeta.tag: + if tag != tagMeta['tag']: + continue + if (self.droptagCount > 0) and (self.droptagType == tag): + self.droptagCount += 1 continue for attr in attrs: if attr[0] != 'id': continue - if attr[1] == tagMeta.id: + if attr[1] == tagMeta['id']: self.droptagCount += 1 self.droptagType = tag + print(f"DBUG:THP:Start:Tag found [{tag}:{attr[1]}]...") def handle_endtag(self, tag: str): if tag in self.monitored: self.inside[tag] = False - if tag == self.droptagType: + if self.droptagType and (tag == self.droptagType): self.droptagCount -= 1 + if self.droptagCount == 0: + self.droptagType = None + print("DBUG:THP:End:Tag found...") if self.droptagCount < 0: self.droptagCount = 0 @@ -186,9 +202,9 @@ def handle_urltext(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): # Extract Text tagDrops = ph.headers.get('urltext-tag-drops') if not tagDrops: - tagDrops = {} + tagDrops = [] else: - tagDrops = json.loads(tagDrops) + tagDrops = cast(list[dict[str,Any]], json.loads(tagDrops)) textHtml = TextHtmlParser(tagDrops) textHtml.feed(got.contentData) # Send back to client diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 9244a30645..13c2f57872 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -590,6 +590,13 @@ SimpleProxy updates * Helpers to fetch file from local file system or the web, transparently * Help check for needed modules before a particular service path is acknowledged as available through /aum service path +* urltext and related - logic to drop contents of specified tag with a given id + * allow its use for the web search tool flow + * setup wrt default duckduckgo search result urltext plain text cleanup and found working. + * this works properly only if the html being processed has proper opening and ending tags + around the area of interest. + * remember to specify non overlapping tag blocks, if more than one specified for dropping. + * this path not tested, but should logically work Settings/Config default changes diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index f52aca4357..d4c2788340 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -259,7 +259,7 @@ function searchwebtext_run(chatid, toolcallid, toolname, obj) { searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)); delete(obj.words) obj['url'] = searchUrl - let headers = { 'urltext-tag-drops': get_gme().tools.searchDrops } + let headers = { 'urltext-tag-drops': JSON.stringify(get_gme().tools.searchDrops) } return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urltext', headers); } } From c5ff065ad2bc5b82c917db4c3686e4fbe202d0f2 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 3 Nov 2025 23:13:12 +0530 Subject: [PATCH 229/446] SimpleChatTC:Cleanup in general Update readme wrt searchDrops, auto settings ui creation Rename tools-auto to tools-autoSecs, to make it easy to realise that the value represents seconds. --- tools/server/public_simplechat/readme.md | 8 ++++++-- tools/server/public_simplechat/simplechat.js | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 13c2f57872..a3c7c3ec3a 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -263,6 +263,10 @@ It is attached to the document object. Some of these can also be updated using t * searchUrl - specify the search engine's search url template along with the tag SEARCHWORDS in place where the search words should be substituted at runtime. + * searchDrops - allows one to drop contents of html tags with specified id from the plain text search result. + + * specify a list of dicts, where each dict should contain a 'tag' entry specifying the tag to filter like div or p or ... and also a 'id' entry which specifies the id of interest. + * iResultMaxDataLength - specify what amount of any tool call result should be sent back to the ai engine server. * specifying 0 disables this truncating of the results, and inturn full result will be sent to the ai engine server. @@ -271,7 +275,7 @@ It is attached to the document object. Some of these can also be updated using t before a default timed out error response is generated and control given back to end user, for them to decide whether to submit the error response or wait for actual tool call response further. - * auto - the amount of time in seconds to wait before the tool call request is auto triggered and generated response is auto submitted back. + * autoSecs - the amount of time in seconds to wait before the tool call request is auto triggered and generated response is auto submitted back. setting this value to 0 (default), disables auto logic, so that end user can review the tool calls requested by ai and if needed even modify them, before triggering/executing them as well as review and modify results generated by the tool call, before submitting them back to the ai. @@ -285,7 +289,7 @@ It is attached to the document object. Some of these can also be updated using t If you want to add additional options/fields to send to the server/ai-model, and or modify the existing options value or remove them, for now you can update this global var using browser's development-tools/console. - For string, numeric and boolean fields in apiRequestOptions, including even those added by a user at runtime by directly modifying gMe.apiRequestOptions, setting ui entries will be auto created. + For string, numeric, boolean, object fields in apiRequestOptions, including even those added by a user at runtime by directly modifying gMe.apiRequestOptions, setting ui entries will be auto created. cache_prompt option supported by example/server is allowed to be controlled by user, so that any caching supported wrt system-prompt and chat history, if usable can get used. When chat history sliding window is enabled, cache_prompt logic may or may not kick in at the backend wrt same, based on aspects related to model, positional encoding, attention mechanism etal. However system prompt should ideally get the benefit of caching. diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 26e33904ff..4e9ed06201 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -762,7 +762,7 @@ class MultiChatUI { this.curChatId = ""; this.TimePeriods = { - ToolCallAutoTimeUnit: 1000 + ToolCallAutoSecsTimeUnit: 1000 } this.timers = { @@ -840,10 +840,10 @@ class MultiChatUI { this.elInToolName.dataset.tool_call_id = ar.ns.tool_calls[0].id this.elInToolArgs.value = ar.ns.tool_calls[0].function.arguments this.elBtnTool.disabled = false - if ((gMe.tools.auto > 0) && (bAuto)) { + if ((gMe.tools.autoSecs > 0) && (bAuto)) { this.timers.toolcallTriggerClick = setTimeout(()=>{ this.elBtnTool.click() - }, gMe.tools.auto*this.TimePeriods.ToolCallAutoTimeUnit) + }, gMe.tools.autoSecs*this.TimePeriods.ToolCallAutoSecsTimeUnit) } } else { this.elDivTool.hidden = true @@ -1066,10 +1066,10 @@ class MultiChatUI { } chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(tcid, name, limitedData))) if (this.chat_show(cid)) { - if (gMe.tools.auto > 0) { + if (gMe.tools.autoSecs > 0) { this.timers.toolcallResponseSubmitClick = setTimeout(()=>{ this.elBtnUser.click() - }, gMe.tools.auto*this.TimePeriods.ToolCallAutoTimeUnit) + }, gMe.tools.autoSecs*this.TimePeriods.ToolCallAutoSecsTimeUnit) } } this.ui_reset_userinput(false) @@ -1333,7 +1333,7 @@ class Me { * Control how many seconds to wait before auto triggering tool call or its response submission. * A value of 0 is treated as auto triggering disable. */ - auto: 0 + autoSecs: 0 }; this.chatProps = { apiEP: ApiEP.Type.Chat, From 2394d38d5873935f26cab990f0e3dad4581b291a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 4 Nov 2025 22:25:39 +0530 Subject: [PATCH 230/446] SimpleChatTC:Cleanup: General T2 Pretty print SimpleProxy gMe config Dont ignore the got http response status text. Update readme wrt why autoSecs --- tools/server/public_simplechat/local.tools/filemagic.py | 4 +++- tools/server/public_simplechat/local.tools/simpleproxy.py | 3 ++- tools/server/public_simplechat/local.tools/webmagic.py | 1 - tools/server/public_simplechat/readme.md | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/filemagic.py b/tools/server/public_simplechat/local.tools/filemagic.py index 54e067e258..8ba6695a0f 100644 --- a/tools/server/public_simplechat/local.tools/filemagic.py +++ b/tools/server/public_simplechat/local.tools/filemagic.py @@ -48,9 +48,11 @@ def get_from_web(url: str, tag: str, inContentType: str, inHeaders: dict[str, st with urllib.request.urlopen(req, timeout=10) as response: contentData = response.read() statusCode = response.status or 200 + statusMsg = response.msg or "" contentType = response.getheader('Content-Type') or inContentType + print(f"DBUG:FM:GFW:Resp:{response.status}:{response.msg}") debug.dump({ 'url': req.full_url, 'headers': req.headers, 'ctype': contentType }, { 'cdata': contentData }) - return Response(True, statusCode, "", contentType, contentData) + return Response(True, statusCode, statusMsg, contentType, contentData) except Exception as exc: return Response(False, 502, f"WARN:{tag}:Failed:{exc}") diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 50a5691703..bcb11bf2c1 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -228,6 +228,7 @@ def process_args(args: list[str]): retained for literal_eval """ import ast + import json global gMe iArg = 1 while iArg < len(args): @@ -252,7 +253,7 @@ def process_args(args: list[str]): except KeyError: print(f"ERRR:ProcessArgs:{iArg}:{cArg}:UnknownCommand") exit(103) - print(gMe) + print(json.dumps(gMe, indent=4)) for k in gConfigNeeded: if gMe.get(k) == None: print(f"ERRR:ProcessArgs:{k}:missing, did you forget to pass the config file...") diff --git a/tools/server/public_simplechat/local.tools/webmagic.py b/tools/server/public_simplechat/local.tools/webmagic.py index 2e3d5811cc..dc0d45a789 100644 --- a/tools/server/public_simplechat/local.tools/webmagic.py +++ b/tools/server/public_simplechat/local.tools/webmagic.py @@ -2,7 +2,6 @@ # by Humans for All import urllib.parse -import urllib.request import urlvalidator as uv from dataclasses import dataclass import html.parser diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index a3c7c3ec3a..198628ba6c 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -279,6 +279,8 @@ It is attached to the document object. Some of these can also be updated using t setting this value to 0 (default), disables auto logic, so that end user can review the tool calls requested by ai and if needed even modify them, before triggering/executing them as well as review and modify results generated by the tool call, before submitting them back to the ai. + this is specified in seconds, so that users by default will normally not overload any website through the proxy server. + the builtin tools' meta data is sent to the ai model in the requests sent to it. inturn if the ai model requests a tool call to be made, the same will be done and the response sent back to the ai model, under user control, by default. From 0fcb13257c20f9e61bf70eb258f948d3b62d8124 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 4 Nov 2025 23:55:51 +0530 Subject: [PATCH 231/446] SimpleChatTC:UI:ClearChat, Unicode icons for Clear, settings Allow user to clear the existing chat. The user does have the option to load the just cleared chat, if required. Add icons wrt clearing chat and settings. --- tools/server/public_simplechat/index.html | 5 ++++- tools/server/public_simplechat/readme.md | 18 ++++++++++++------ tools/server/public_simplechat/simplechat.js | 6 ++++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/tools/server/public_simplechat/index.html b/tools/server/public_simplechat/index.html index 3cd840569c..797530d72f 100644 --- a/tools/server/public_simplechat/index.html +++ b/tools/server/public_simplechat/index.html @@ -24,7 +24,10 @@

    SimpleChat

    - +
    + + +
    diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 198628ba6c..44437e0d10 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -182,11 +182,14 @@ Once inside not get/see a tool call, in such situations, dont forget to cross check that tool calling is enabled in the settings. -* just refresh the page, to reset wrt the chat history and or system prompt and start afresh. - This also helps if you had forgotten to start the bundled simpleproxy.py server before hand. - Start the simpleproxy.py server and refresh the client ui page, to get access to web access - related tool calls. - * if you refreshed unknowingly, you can use the Restore feature to try load the previous chat +* ClearChat/Refresh + * use the clearchat button to clear the currently active chat session. + * just refresh the page, to reset wrt the chat history and system prompts across chat sessions + and start afresh. + * This also helps if you had forgotten to start the bundled simpleproxy.py server before hand. + Start the simpleproxy.py server and refresh the client ui page, to get access to web access + related tool calls. + * if you refreshed/cleared unknowingly, you can use the Restore feature to try load previous chat session and resume that session. This uses a basic local auto save logic that is in there. * Using NewChat one can start independent chat sessions. @@ -434,7 +437,8 @@ The following tools/functions are currently provided by default * simple_calculator - which can solve simple arithmatic expressions -* run_javascript_function_code - which can be used to run ai generated or otherwise javascript code using browser's js capabilities. +* run_javascript_function_code - which can be used to run ai generated or otherwise javascript code + using browser's js capabilities. * data_store_get/set/delete/list - allows for a basic data store to be used. @@ -620,6 +624,8 @@ channel of the handshake. having ai responses built over multiple steps of tool callings etal. So independent of our client side sliding window based drop off or even before they kick in, this can help in many cases. +* UI - add ClearChat button and logic. Also add unicode icons for same as well as for Settings. + #### ToDo diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 4e9ed06201..17c27579ff 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -326,6 +326,7 @@ class SimpleChat { clear() { this.xchat = []; this.iLastSys = -1; + this.latestResponse = new ChatMessageEx(); } ods_key() { @@ -797,6 +798,7 @@ class MultiChatUI { this.elDivHeading = /** @type{HTMLSelectElement} */(document.getElementById("heading")); this.elDivSessions = /** @type{HTMLDivElement} */(document.getElementById("sessions-div")); this.elBtnSettings = /** @type{HTMLButtonElement} */(document.getElementById("settings")); + this.elBtnClearChat = /** @type{HTMLButtonElement} */(document.getElementById("clearchat")); this.elDivTool = /** @type{HTMLDivElement} */(document.getElementById("tool-div")); this.elBtnTool = /** @type{HTMLButtonElement} */(document.getElementById("tool-btn")); this.elInToolName = /** @type{HTMLInputElement} */(document.getElementById("toolname-in")); @@ -1030,6 +1032,10 @@ class MultiChatUI { this.elDivChat.replaceChildren(); gMe.show_settings(this.elDivChat); }); + this.elBtnClearChat.addEventListener("click", (ev)=>{ + this.simpleChats[this.curChatId].clear() + this.chat_show(this.curChatId) + }); this.elBtnUser.addEventListener("click", (ev)=>{ clearTimeout(this.timers.toolcallResponseSubmitClick) From d6fd4ea5337e849aa437ab99be425752976c570e Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 5 Nov 2025 19:29:45 +0530 Subject: [PATCH 232/446] SimpleChatTC:FetchPdfAsText: Renamed function call Some ai's dont seem to be prefering to use this direct helper provided for fetching pdf as text, on its own. Instead ai (gptoss) seems to be keen on fetching raw pdf and extract text etal, so now renaming the function call to try and make its semantic more readily obivious hopefully. It sometimes (not always) seem to assum fetch_web_url_text, can convert pdf to text and return it. Maybe I need to place the specific fetch pdf as text before the generic fetch web url text and so... With the rename, the pdf specific fetch seems to be getting used more. --- tools/server/public_simplechat/readme.md | 4 +++- tools/server/public_simplechat/toolweb.mjs | 26 +++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 44437e0d10..27696c04bf 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -458,7 +458,7 @@ Either way always remember to cross check the tool requests and generated respon * search_web_text - search for the specified words using the configured search engine and return the plain textual content from the search result page. -* pdf_to_text - fetch/read specified pdf file and extract its textual content +* fetch_pdf_as_text - fetch/read specified pdf file and extract its textual content * this depends on the pypdf python based open source library the above set of web related tool calls work by handshaking with a bundled simple local web proxy @@ -626,6 +626,8 @@ sliding window based drop off or even before they kick in, this can help in many * UI - add ClearChat button and logic. Also add unicode icons for same as well as for Settings. +* renamed pdf_to_text to fetch_pdf_as_text so that ai model can understand the semantic better. + #### ToDo diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index d4c2788340..0163d880a6 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -280,21 +280,21 @@ async function searchwebtext_setup(tcs) { // -// PdfText +// FetchPdfText // -let pdftext_meta = { +let fetchpdftext_meta = { "type": "function", "function": { - "name": "pdf_to_text", - "description": "Read pdf from requested local file path / web url through a proxy server and return its text content after converting pdf to text, in few seconds. One is allowed to get a part of the pdf by specifying the starting and ending page numbers", + "name": "fetch_pdf_as_text", + "description": "Fetch pdf from requested local file path / web url through a proxy server and return its text content after converting pdf to text, in few seconds. One is allowed to get a part of the pdf by specifying the starting and ending page numbers", "parameters": { "type": "object", "properties": { "url":{ "type":"string", - "description":"local file path (file://) / web (http/https) based url of the pdf that will be got and inturn converted to text to an extent" + "description":"local file path (file://) / web (http/https) based url of the pdf that will be got and inturn converted to text" }, "startPageNumber":{ "type":"integer", @@ -312,7 +312,7 @@ let pdftext_meta = { /** - * Implementation of the pdf to text logic. + * Implementation of the fetch pdf as text logic. * Expects a simple minded proxy server to be running locally * * listening on a configured port * * expecting http requests @@ -325,20 +325,20 @@ let pdftext_meta = { * @param {string} toolname * @param {any} obj */ -function pdftext_run(chatid, toolcallid, toolname, obj) { +function fetchpdftext_run(chatid, toolcallid, toolname, obj) { return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'pdftext'); } /** - * Setup pdftext for tool calling + * Setup fetchpdftext for tool calling * NOTE: Currently the logic is setup for the bundled simpleproxy.py * @param {Object>} tcs */ -async function pdftext_setup(tcs) { - return proxyserver_tc_setup('PdfText', 'pdftext', 'pdf_to_text', { - "handler": pdftext_run, - "meta": pdftext_meta, +async function fetchpdftext_setup(tcs) { + return proxyserver_tc_setup('FetchPdfAsText', 'pdftext', 'fetch_pdf_as_text', { + "handler": fetchpdftext_run, + "meta": fetchpdftext_meta, "result": "" }, tcs); } @@ -359,6 +359,6 @@ export async function init(toolsWorker) { await fetchweburlraw_setup(tc_switch) await fetchweburltext_setup(tc_switch) await searchwebtext_setup(tc_switch) - await pdftext_setup(tc_switch) + await fetchpdftext_setup(tc_switch) return tc_switch } From 83a4b1a3fa1cad36715aaa1a702598dea1db3c12 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 5 Nov 2025 21:47:18 +0530 Subject: [PATCH 233/446] SimpleChatTC:System Date and Time --- tools/server/public_simplechat/datautils.mjs | 3 + tools/server/public_simplechat/readme.md | 8 ++- tools/server/public_simplechat/tooljs.mjs | 75 ++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/datautils.mjs b/tools/server/public_simplechat/datautils.mjs index 75159d6b16..a4b305c6cc 100644 --- a/tools/server/public_simplechat/datautils.mjs +++ b/tools/server/public_simplechat/datautils.mjs @@ -125,6 +125,9 @@ export function trim_hist_garbage_at_end(sIn, maxType, maxUniq, maxMatchLenThres let iNum = 0; let iOth = 0; // Learn + /** + * @type {Object} + */ let hist = {}; let iUniq = 0; for(let i=0; i>} */ export let tc_switch = { + "sys_date_time": { + "handler": sysdatetime_run, + "meta": sysdatetime_meta, + "result": "" + }, "run_javascript_function_code": { "handler": js_run, "meta": js_meta, From d56a4a06b04ceeeb6a7e61a596c9430eb31f7577 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 5 Nov 2025 14:42:39 +0530 Subject: [PATCH 234/446] SimpleChatTC:Cleanup:Make main chat related classes importable Have main classes defined independent of and away from runtime flow Move out the entry point including runtime instantiation of the core Me class (which inturn brings other class instances as neede) into its own main.js file. With this one should be able to import simplechat.js into other files, where one might need the SimpleChat or MultiChat or Me class definitions. --- tools/server/public_simplechat/index.html | 3 +- tools/server/public_simplechat/main.js | 33 ++++++ tools/server/public_simplechat/simplechat.js | 104 ++++++++----------- 3 files changed, 81 insertions(+), 59 deletions(-) create mode 100644 tools/server/public_simplechat/main.js diff --git a/tools/server/public_simplechat/index.html b/tools/server/public_simplechat/index.html index 797530d72f..d34a80dfb8 100644 --- a/tools/server/public_simplechat/index.html +++ b/tools/server/public_simplechat/index.html @@ -11,12 +11,13 @@ - + diff --git a/tools/server/public_simplechat/main.js b/tools/server/public_simplechat/main.js new file mode 100644 index 0000000000..05efb4d061 --- /dev/null +++ b/tools/server/public_simplechat/main.js @@ -0,0 +1,33 @@ +// @ts-check +// A simple completions and chat/completions test related web front end logic +// by Humans for All + + +import * as mChatMagic from './simplechat.js' +import * as tools from "./tools.mjs" +import * as du from "./datautils.mjs"; + + + +/** @type {mChatMagic.Me} */ +let gMe; + +function startme() { + console.log("INFO:SimpleChat:StartMe:Starting..."); + gMe = new mChatMagic.Me(); + gMe.debug_disable(); + // @ts-ignore + document["gMe"] = gMe; + // @ts-ignore + document["du"] = du; + // @ts-ignore + document["tools"] = tools; + tools.init().then((toolNames)=>gMe.tools.toolNames=toolNames).then(()=>gMe.multiChat.chat_show(gMe.multiChat.curChatId)) + for (let cid of gMe.defaultChatIds) { + gMe.multiChat.new_chat_session(cid); + } + gMe.multiChat.setup_ui(gMe.defaultChatIds[0], true); + gMe.multiChat.show_sessions(); +} + +document.addEventListener("DOMContentLoaded", startme); diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 17c27579ff..da4766ef43 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1,5 +1,6 @@ // @ts-check -// A simple completions and chat/completions test related web front end logic +// Core classes which provide a simple implementation of handshake with ai server's completions and chat/completions endpoints +// as well as related web front end logic for basic usage and testing. // by Humans for All import * as du from "./datautils.mjs"; @@ -277,7 +278,10 @@ class ChatMessageEx { } -function usage_note() { +/** + * @param {number} iRecentUserMsgCnt + */ +function usage_note(iRecentUserMsgCnt) { let sUsageNote = `
    Usage Note @@ -293,7 +297,7 @@ function usage_note() {
  • If ai assistant requests a tool call, verify same before triggering.
  • submit tool response placed into user query/response text area
  • -
  • ContextWindow = [System, Last[${gMe.chatProps.iRecentUserMsgCnt-1}] User Query/Resp, Cur Query].
  • +
  • ContextWindow = [System, Last[${iRecentUserMsgCnt}] User Query/Resp, Cur Query].
    • ChatHistInCtxt, MaxTokens, ModelCtxt window to expand
    @@ -311,8 +315,9 @@ class SimpleChat { /** * @param {string} chatId + * @param {Me} me */ - constructor(chatId) { + constructor(chatId, me) { this.chatId = chatId; /** * Maintain in a form suitable for common LLM web service chat/completions' messages entry @@ -321,6 +326,7 @@ class SimpleChat { this.xchat = []; this.iLastSys = -1; this.latestResponse = new ChatMessageEx(); + this.me = me; } clear() { @@ -485,14 +491,14 @@ class SimpleChat { /** * Setup the fetch headers. - * It picks the headers from gMe.headers. + * It picks the headers from this.me.headers. * It inserts Authorization only if its non-empty. * @param {string} apiEP */ fetch_headers(apiEP) { let headers = new Headers(); - for(let k in gMe.headers) { - let v = gMe.headers[k]; + for(let k in this.me.headers) { + let v = this.me.headers[k]; if ((k == "Authorization") && (v.trim() == "")) { continue; } @@ -509,13 +515,13 @@ class SimpleChat { * @param {Object} obj */ request_jsonstr_extend(obj) { - for(let k in gMe.apiRequestOptions) { - obj[k] = gMe.apiRequestOptions[k]; + for(let k in this.me.apiRequestOptions) { + obj[k] = this.me.apiRequestOptions[k]; } - if (gMe.chatProps.stream) { + if (this.me.chatProps.stream) { obj["stream"] = true; } - if (gMe.tools.enabled) { + if (this.me.tools.enabled) { obj["tools"] = tools.meta(); } return JSON.stringify(obj); @@ -526,7 +532,7 @@ class SimpleChat { */ request_messages_jsonstr() { let req = { - messages: this.recent_chat_ns(gMe.chatProps.iRecentUserMsgCnt), + messages: this.recent_chat_ns(this.me.chatProps.iRecentUserMsgCnt), } return this.request_jsonstr_extend(req); } @@ -538,7 +544,7 @@ class SimpleChat { request_prompt_jsonstr(bInsertStandardRolePrefix) { let prompt = ""; let iCnt = 0; - for(const msg of this.recent_chat(gMe.chatProps.iRecentUserMsgCnt)) { + for(const msg of this.recent_chat(this.me.chatProps.iRecentUserMsgCnt)) { iCnt += 1; if (iCnt > 1) { prompt += "\n"; @@ -562,7 +568,7 @@ class SimpleChat { if (apiEP == ApiEP.Type.Chat) { return this.request_messages_jsonstr(); } else { - return this.request_prompt_jsonstr(gMe.chatProps.bCompletionInsertStandardRolePrefix); + return this.request_prompt_jsonstr(this.me.chatProps.bCompletionInsertStandardRolePrefix); } } @@ -676,7 +682,7 @@ class SimpleChat { */ async handle_response(resp, apiEP, elDiv) { let theResp = null; - if (gMe.chatProps.stream) { + if (this.me.chatProps.stream) { try { theResp = await this.handle_response_multipart(resp, apiEP, elDiv); this.latestResponse.clear(); @@ -690,7 +696,7 @@ class SimpleChat { } else { theResp = await this.handle_response_oneshot(resp, apiEP); } - if (gMe.chatProps.bTrimGarbage) { + if (this.me.chatProps.bTrimGarbage) { let origMsg = theResp.ns.content; theResp.ns.content = du.trim_garbage_at_end(origMsg); theResp.trimmedContent = origMsg.substring(theResp.ns.content.length); @@ -756,7 +762,11 @@ class SimpleChat { class MultiChatUI { - constructor() { + /** + * @param {Me} me + */ + constructor(me) { + this.me = me /** @type {Object} */ this.simpleChats = {}; /** @type {string} */ @@ -842,10 +852,10 @@ class MultiChatUI { this.elInToolName.dataset.tool_call_id = ar.ns.tool_calls[0].id this.elInToolArgs.value = ar.ns.tool_calls[0].function.arguments this.elBtnTool.disabled = false - if ((gMe.tools.autoSecs > 0) && (bAuto)) { + if ((this.me.tools.autoSecs > 0) && (bAuto)) { this.timers.toolcallTriggerClick = setTimeout(()=>{ this.elBtnTool.click() - }, gMe.tools.autoSecs*this.TimePeriods.ToolCallAutoSecsTimeUnit) + }, this.me.tools.autoSecs*this.TimePeriods.ToolCallAutoSecsTimeUnit) } } else { this.elDivTool.hidden = true @@ -992,7 +1002,7 @@ class MultiChatUI { this.ui_reset_toolcall_as_needed(new ChatMessageEx()); } this.elLastChatMessage = null - let chatToShow = chat.recent_chat(gMe.chatProps.iRecentUserMsgCnt); + let chatToShow = chat.recent_chat(this.me.chatProps.iRecentUserMsgCnt); for(const [i, x] of chatToShow.entries()) { let iFromLast = (chatToShow.length - 1)-i let nextMsg = undefined @@ -1005,9 +1015,9 @@ class MultiChatUI { /** @type{HTMLElement} */(this.elLastChatMessage).scrollIntoView(false); // Stupid ts-check js-doc intersection ??? } else { if (bClear) { - this.elDivChat.innerHTML = usage_note(); - gMe.setup_load(this.elDivChat, chat); - gMe.show_info(this.elDivChat, bShowInfoAll); + this.elDivChat.innerHTML = usage_note(this.me.chatProps.iRecentUserMsgCnt-1); + this.me.setup_load(this.elDivChat, chat); + this.me.show_info(this.elDivChat, bShowInfoAll); } } return true @@ -1030,7 +1040,7 @@ class MultiChatUI { this.elBtnSettings.addEventListener("click", (ev)=>{ this.elDivChat.replaceChildren(); - gMe.show_settings(this.elDivChat); + this.me.show_settings(this.elDivChat); }); this.elBtnClearChat.addEventListener("click", (ev)=>{ this.simpleChats[this.curChatId].clear() @@ -1043,7 +1053,7 @@ class MultiChatUI { if (this.elInUser.disabled) { return; } - this.handle_user_submit(this.curChatId, gMe.chatProps.apiEP).catch((/** @type{Error} */reason)=>{ + this.handle_user_submit(this.curChatId, this.me.chatProps.apiEP).catch((/** @type{Error} */reason)=>{ let msg = `ERRR:SimpleChat\nMCUI:HandleUserSubmit:${this.curChatId}\n${reason.name}:${reason.message}`; console.error(msg.replace("\n", ":")); alert(msg); @@ -1065,17 +1075,17 @@ class MultiChatUI { this.timers.toolcallResponseTimeout = undefined let chat = this.simpleChats[cid]; let limitedData = data - if (gMe.tools.iResultMaxDataLength > 0) { - if (data.length > gMe.tools.iResultMaxDataLength) { - limitedData = data.slice(0, gMe.tools.iResultMaxDataLength) + `\n\n\nALERT: Data too long, was chopped ....` + if (this.me.tools.iResultMaxDataLength > 0) { + if (data.length > this.me.tools.iResultMaxDataLength) { + limitedData = data.slice(0, this.me.tools.iResultMaxDataLength) + `\n\n\nALERT: Data too long, was chopped ....` } } chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(tcid, name, limitedData))) if (this.chat_show(cid)) { - if (gMe.tools.autoSecs > 0) { + if (this.me.tools.autoSecs > 0) { this.timers.toolcallResponseSubmitClick = setTimeout(()=>{ this.elBtnUser.click() - }, gMe.tools.autoSecs*this.TimePeriods.ToolCallAutoSecsTimeUnit) + }, this.me.tools.autoSecs*this.TimePeriods.ToolCallAutoSecsTimeUnit) } } this.ui_reset_userinput(false) @@ -1113,7 +1123,7 @@ class MultiChatUI { * @param {boolean} bSwitchSession */ new_chat_session(chatId, bSwitchSession=false) { - this.simpleChats[chatId] = new SimpleChat(chatId); + this.simpleChats[chatId] = new SimpleChat(chatId, this.me); if (bSwitchSession) { this.handle_session_switch(chatId); } @@ -1143,7 +1153,7 @@ class MultiChatUI { // So if user wants to simulate a multi-chat based completion query, // they will have to enter the full thing, as a suitable multiline // user input/query. - if ((apiEP == ApiEP.Type.Completion) && (gMe.chatProps.bCompletionFreshChatAlways)) { + if ((apiEP == ApiEP.Type.Completion) && (this.me.chatProps.bCompletionFreshChatAlways)) { chat.clear(); } @@ -1167,7 +1177,7 @@ class MultiChatUI { this.elInUser.disabled = true; try { - let theResp = await chat.handle_chat_hs(gMe.baseURL, apiEP, this.elDivChat) + let theResp = await chat.handle_chat_hs(this.me.baseURL, apiEP, this.elDivChat) if (chatId == this.curChatId) { this.chat_show(chatId); if (theResp.trimmedContent.length > 0) { @@ -1206,7 +1216,7 @@ class MultiChatUI { chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, `Tool/Function call ${toolname} taking too much time, aborting...`))) this.chat_show(chat.chatId) this.ui_reset_userinput(false) - }, gMe.tools.toolCallResponseTimeoutMS) + }, this.me.tools.toolCallResponseTimeoutMS) } } @@ -1312,12 +1322,12 @@ const SearchURLS = { } -class Me { +export class Me { constructor() { this.baseURL = "http://127.0.0.1:8080"; this.defaultChatIds = [ "Default", "Other" ]; - this.multiChat = new MultiChatUI(); + this.multiChat = new MultiChatUI(this); this.tools = { enabled: true, proxyUrl: "http://127.0.0.1:3128", @@ -1462,25 +1472,3 @@ class Me { } -/** @type {Me} */ -let gMe; - -function startme() { - console.log("INFO:SimpleChat:StartMe:Starting..."); - gMe = new Me(); - gMe.debug_disable(); - // @ts-ignore - document["gMe"] = gMe; - // @ts-ignore - document["du"] = du; - // @ts-ignore - document["tools"] = tools; - tools.init().then((toolNames)=>gMe.tools.toolNames=toolNames).then(()=>gMe.multiChat.chat_show(gMe.multiChat.curChatId)) - for (let cid of gMe.defaultChatIds) { - gMe.multiChat.new_chat_session(cid); - } - gMe.multiChat.setup_ui(gMe.defaultChatIds[0], true); - gMe.multiChat.show_sessions(); -} - -document.addEventListener("DOMContentLoaded", startme); From 85c2779579e77903cd9d4f4c3e5c0807ecf3bdb9 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 5 Nov 2025 17:32:34 +0530 Subject: [PATCH 235/446] SimpleChatTC:Cleanup:MeInTools: update tools, toolweb Now gMe can be used in toolweb with proper knowledge of available members and can also be cross checked by tools --- tools/server/public_simplechat/main.js | 2 +- tools/server/public_simplechat/readme.md | 4 ++ tools/server/public_simplechat/tools.mjs | 8 +++- tools/server/public_simplechat/toolweb.mjs | 47 ++++++++++------------ 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/tools/server/public_simplechat/main.js b/tools/server/public_simplechat/main.js index 05efb4d061..0da0ea508a 100644 --- a/tools/server/public_simplechat/main.js +++ b/tools/server/public_simplechat/main.js @@ -22,7 +22,7 @@ function startme() { document["du"] = du; // @ts-ignore document["tools"] = tools; - tools.init().then((toolNames)=>gMe.tools.toolNames=toolNames).then(()=>gMe.multiChat.chat_show(gMe.multiChat.curChatId)) + tools.init(gMe).then((toolNames)=>gMe.tools.toolNames=toolNames).then(()=>gMe.multiChat.chat_show(gMe.multiChat.curChatId)) for (let cid of gMe.defaultChatIds) { gMe.multiChat.new_chat_session(cid); } diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 1fc680532f..65f77897ec 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -632,6 +632,10 @@ sliding window based drop off or even before they kick in, this can help in many * sys_date_time tool call has been added. +* SimpleChat - Move the main chat related classes into its own js module file, independent of the +main runtime entry point. This allows these classes to be referenced from other modules like tools +related modules with full access to their details for developers and static check tools. + #### ToDo diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index e4f66ea7f3..7fb9a8c2fa 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -8,6 +8,7 @@ import * as tjs from './tooljs.mjs' import * as tweb from './toolweb.mjs' import * as tdb from './tooldb.mjs' +import * as mChatMagic from './simplechat.js' let gToolsWorker = new Worker('./toolsworker.mjs', { type: 'module' }); @@ -19,7 +20,10 @@ let gToolsDBWorker = new Worker('./toolsdbworker.mjs', { type: 'module' }); export let tc_switch = {} -export async function init() { +/** + * @param {mChatMagic.Me} me + */ +export async function init(me) { /** * @type {string[]} */ @@ -36,7 +40,7 @@ export async function init() { toolNames.push(key) } }) - let tNs = await tweb.init(gToolsWorker) + let tNs = await tweb.init(gToolsWorker, me) for (const key in tNs) { tc_switch[key] = tNs[key] toolNames.push(key) diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 0163d880a6..1a0000a25f 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -5,8 +5,14 @@ // by Humans for All // +import * as mChatMagic from './simplechat.js' + let gToolsWorker = /** @type{Worker} */(/** @type {unknown} */(null)); +/** + * @type {mChatMagic.Me} + */ +let gMe = /** @type{mChatMagic.Me} */(/** @type {unknown} */(null)); /** @@ -19,22 +25,13 @@ function message_toolsworker(mev) { } -/** - * Retrieve the global Me instance - */ -function get_gme() { - return (/** @type {Object>} */(/** @type {unknown} */(document)))['gMe'] -} - - /** * For now hash the shared secret with the year. */ -function bearer_transform() { - let data = `${new Date().getUTCFullYear()}${get_gme().tools.proxyAuthInsecure}` - return crypto.subtle.digest('sha-256', new TextEncoder().encode(data)).then(ab=>{ - return Array.from(new Uint8Array(ab)).map(b=>b.toString(16).padStart(2,'0')).join('') - }) +async function bearer_transform() { + let data = `${new Date().getUTCFullYear()}${gMe.tools.proxyAuthInsecure}` + const ab = await crypto.subtle.digest('sha-256', new TextEncoder().encode(data)); + return Array.from(new Uint8Array(ab)).map(b => b.toString(16).padStart(2, '0')).join(''); } /** @@ -56,7 +53,7 @@ function bearer_transform() { async function proxyserver_get_anyargs(chatid, toolcallid, toolname, objSearchParams, path, objHeaders={}) { if (gToolsWorker.onmessage != null) { let params = new URLSearchParams(objSearchParams) - let newUrl = `${get_gme().tools.proxyUrl}/${path}?${params}` + let newUrl = `${gMe.tools.proxyUrl}/${path}?${params}` let headers = new Headers(objHeaders) let btoken = await bearer_transform() headers.append('Authorization', `Bearer ${btoken}`) @@ -84,7 +81,7 @@ async function proxyserver_get_anyargs(chatid, toolcallid, toolname, objSearchPa * @param {Object>} tcs */ async function proxyserver_tc_setup(tag, tcPath, tcName, tcsData, tcs) { - await fetch(`${get_gme().tools.proxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ + await fetch(`${gMe.tools.proxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ if (resp.statusText != 'bharatavarshe') { console.log(`WARN:ToolWeb:${tag}:Dont forget to run the bundled local.tools/simpleproxy.py to enable me`) return @@ -253,15 +250,13 @@ let searchwebtext_meta = { * @param {any} obj */ function searchwebtext_run(chatid, toolcallid, toolname, obj) { - if (gToolsWorker.onmessage != null) { - /** @type {string} */ - let searchUrl = get_gme().tools.searchUrl; - searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)); - delete(obj.words) - obj['url'] = searchUrl - let headers = { 'urltext-tag-drops': JSON.stringify(get_gme().tools.searchDrops) } - return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urltext', headers); - } + /** @type {string} */ + let searchUrl = gMe.tools.searchUrl; + searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)); + delete(obj.words) + obj['url'] = searchUrl + let headers = { 'urltext-tag-drops': JSON.stringify(gMe.tools.searchDrops) } + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urltext', headers); } @@ -349,13 +344,15 @@ async function fetchpdftext_setup(tcs) { * Used to get hold of the web worker to use for running tool/function call related code * Also to setup tool calls, which need to cross check things at runtime * @param {Worker} toolsWorker + * @param {mChatMagic.Me} me */ -export async function init(toolsWorker) { +export async function init(toolsWorker, me) { /** * @type {Object>} tcs */ let tc_switch = {} gToolsWorker = toolsWorker + gMe = me await fetchweburlraw_setup(tc_switch) await fetchweburltext_setup(tc_switch) await searchwebtext_setup(tc_switch) From 0e7fe8bcf2e5a8f215ca9c959d4083472f42df36 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 6 Nov 2025 01:12:30 +0530 Subject: [PATCH 236/446] SimpleChatTC:MeInTools: WebWorkers in Me Given that Me is now passed to the tools logic during setup, have the web worker handles in Me itself, instead of in tool related modules. Move setup of web worker related main thread callbacks, as well as posting messages directly to these main thread callbacks, into Me. --- tools/server/public_simplechat/simplechat.js | 41 ++++++++++++++++++-- tools/server/public_simplechat/tooldb.mjs | 14 ++++--- tools/server/public_simplechat/tooljs.mjs | 20 +++++----- tools/server/public_simplechat/tools.mjs | 35 +++++++---------- tools/server/public_simplechat/toolweb.mjs | 22 +++-------- 5 files changed, 73 insertions(+), 59 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index da4766ef43..21be956c5b 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1069,8 +1069,8 @@ class MultiChatUI { this.handle_tool_run(this.curChatId); }) - // Handle messages from Tools web worker - tools.setup((cid, tcid, name, data)=>{ + // Handle messages from tools web workers + this.me.workers_cb((cid, tcid, name, data)=>{ clearTimeout(this.timers.toolcallResponseTimeout) this.timers.toolcallResponseTimeout = undefined let chat = this.simpleChats[cid]; @@ -1386,6 +1386,10 @@ export class Me { //"frequency_penalty": 1.2, //"presence_penalty": 1.2, }; + this.workers = { + js: /** @type {Worker} */(/** @type {unknown} */(undefined)), + db: /** @type {Worker} */(/** @type {unknown} */(undefined)), + } } /** @@ -1469,6 +1473,35 @@ export class Me { }) } + /** + * Setup the callback that will be called when ever message + * is recieved from the Tools Web Workers. + * @param {(chatId: string, toolCallId: string, name: string, data: string) => void} cb + */ + workers_cb(cb) { + this.workers.js.onmessage = function (ev) { + cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) + } + this.workers.db.onmessage = function (ev) { + cb(ev.data.cid, ev.data.tcid, ev.data.name, JSON.stringify(ev.data.data, (k,v)=>{ + return (v === undefined) ? '__UNDEFINED__' : v; + })); + } + } + + /** + * Send a message to specified tools web worker's monitor in main thread directly + * @param {Worker} worker + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {string} data + */ + workers_postmessage_for_main(worker, chatid, toolcallid, toolname, data) { + let mev = new MessageEvent('message', {data: {cid: chatid, tcid: toolcallid, name: toolname, data: data}}); + if (worker.onmessage != null) { + worker.onmessage(mev) + } + } + } - - diff --git a/tools/server/public_simplechat/tooldb.mjs b/tools/server/public_simplechat/tooldb.mjs index f77a4e6985..fd02facffc 100644 --- a/tools/server/public_simplechat/tooldb.mjs +++ b/tools/server/public_simplechat/tooldb.mjs @@ -1,12 +1,14 @@ //@ts-check // ALERT - Simple Stupid flow - Using from a discardable VM is better // Helpers to handle tools/functions calling wrt data store -// using a web worker. +// using a db specific web worker. // by Humans for All // +import * as mChatMagic from './simplechat.js' -let gToolsDBWorker = /** @type{Worker} */(/** @type {unknown} */(null)); + +let gMe = /** @type{mChatMagic.Me} */(/** @type {unknown} */(null)); let dsget_meta = { @@ -93,7 +95,7 @@ let dslist_meta = { * @param {any} obj */ function dsops_run(chatid, toolcallid, toolname, obj) { - gToolsDBWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) + gMe.workers.db.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) } @@ -128,8 +130,8 @@ export let tc_switch = { /** * Used to get hold of the web worker to use for running tool/function call related code * Also to setup tool calls, which need to cross check things at runtime - * @param {Worker} toolsWorker + * @param {mChatMagic.Me} me */ -export async function init(toolsWorker) { - gToolsDBWorker = toolsWorker +export async function init(me) { + gMe = me } diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index cc0d6e4cb3..b3d18ce370 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -3,12 +3,14 @@ // Helpers to handle tools/functions calling wrt // * javascript interpreter // * simple arithmatic calculator -// using a web worker. +// using the js specific web worker. // by Humans for All // +import * as mChatMagic from './simplechat.js' -let gToolsWorker = /** @type{Worker} */(/** @type {unknown} */(null)); + +let gMe = /** @type{mChatMagic.Me} */(/** @type {unknown} */(null)); let sysdatetime_meta = { @@ -75,9 +77,7 @@ function sysdatetime_run(chatid, toolcallid, toolname, obj) { break; } } - if (gToolsWorker.onmessage != null) { - gToolsWorker.onmessage(new MessageEvent('message', {data: {cid: chatid, tcid: toolcallid, name: toolname, data: sDT}})) - } + gMe.workers_postmessage_for_main(gMe.workers.js, chatid, toolcallid, toolname, sDT); } @@ -109,7 +109,7 @@ let js_meta = { * @param {any} obj */ function js_run(chatid, toolcallid, toolname, obj) { - gToolsWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: obj["code"]}) + gMe.workers.js.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: obj["code"]}) } @@ -141,7 +141,7 @@ let calc_meta = { * @param {any} obj */ function calc_run(chatid, toolcallid, toolname, obj) { - gToolsWorker.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) + gMe.workers.js.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) } @@ -170,8 +170,8 @@ export let tc_switch = { /** * Used to get hold of the web worker to use for running tool/function call related code * Also to setup tool calls, which need to cross check things at runtime - * @param {Worker} toolsWorker + * @param {mChatMagic.Me} me */ -export async function init(toolsWorker) { - gToolsWorker = toolsWorker +export async function init(me) { + gMe = me } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 7fb9a8c2fa..5f1c46301f 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -11,8 +11,6 @@ import * as tdb from './tooldb.mjs' import * as mChatMagic from './simplechat.js' -let gToolsWorker = new Worker('./toolsworker.mjs', { type: 'module' }); -let gToolsDBWorker = new Worker('./toolsdbworker.mjs', { type: 'module' }); /** * Maintain currently available tool/function calls * @type {Object>} @@ -20,27 +18,37 @@ let gToolsDBWorker = new Worker('./toolsdbworker.mjs', { type: 'module' }); export let tc_switch = {} +/** + * @param {mChatMagic.Me} me + */ +function setup_workers(me) { + me.workers.js = new Worker('./toolsworker.mjs', { type: 'module' }); + me.workers.db = new Worker('./toolsdbworker.mjs', { type: 'module' }); +} + + /** * @param {mChatMagic.Me} me */ export async function init(me) { + setup_workers(me); /** * @type {string[]} */ let toolNames = [] - await tjs.init(gToolsWorker).then(()=>{ + await tjs.init(me).then(()=>{ for (const key in tjs.tc_switch) { tc_switch[key] = tjs.tc_switch[key] toolNames.push(key) } }) - await tdb.init(gToolsDBWorker).then(()=>{ + await tdb.init(me).then(()=>{ for (const key in tdb.tc_switch) { tc_switch[key] = tdb.tc_switch[key] toolNames.push(key) } }) - let tNs = await tweb.init(gToolsWorker, me) + let tNs = await tweb.init(me) for (const key in tNs) { tc_switch[key] = tNs[key] toolNames.push(key) @@ -58,23 +66,6 @@ export function meta() { } -/** - * Setup the callback that will be called when ever message - * is recieved from the Tools Web Worker. - * @param {(chatId: string, toolCallId: string, name: string, data: string) => void} cb - */ -export function setup(cb) { - gToolsWorker.onmessage = function (ev) { - cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) - } - gToolsDBWorker.onmessage = function (ev) { - cb(ev.data.cid, ev.data.tcid, ev.data.name, JSON.stringify(ev.data.data, (k,v)=>{ - return (v === undefined) ? '__UNDEFINED__' : v; - })); - } -} - - /** * Try call the specified tool/function call. * Returns undefined, if the call was placed successfully diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 1a0000a25f..a2181e4647 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -2,29 +2,19 @@ // ALERT - Simple Stupid flow - Using from a discardable VM is better // Helpers to handle tools/functions calling related to web access, pdf, etal // which work in sync with the bundled simpleproxy.py server logic. +// Uses the js specific web worker path. // by Humans for All // import * as mChatMagic from './simplechat.js' -let gToolsWorker = /** @type{Worker} */(/** @type {unknown} */(null)); /** * @type {mChatMagic.Me} */ let gMe = /** @type{mChatMagic.Me} */(/** @type {unknown} */(null)); -/** - * Send a message to Tools WebWorker's monitor in main thread directly - * @param {MessageEvent} mev - */ -function message_toolsworker(mev) { - // @ts-ignore - gToolsWorker.onmessage(mev) -} - - /** * For now hash the shared secret with the year. */ @@ -51,7 +41,7 @@ async function bearer_transform() { * @param {any} objHeaders */ async function proxyserver_get_anyargs(chatid, toolcallid, toolname, objSearchParams, path, objHeaders={}) { - if (gToolsWorker.onmessage != null) { + if (gMe.workers.js.onmessage != null) { let params = new URLSearchParams(objSearchParams) let newUrl = `${gMe.tools.proxyUrl}/${path}?${params}` let headers = new Headers(objHeaders) @@ -63,9 +53,9 @@ async function proxyserver_get_anyargs(chatid, toolcallid, toolname, objSearchPa } return resp.text() }).then(data => { - message_toolsworker(new MessageEvent('message', {data: {cid: chatid, tcid: toolcallid, name: toolname, data: data}})) + gMe.workers_postmessage_for_main(gMe.workers.js, chatid, toolcallid, toolname, data); }).catch((err)=>{ - message_toolsworker(new MessageEvent('message', {data: {cid: chatid, tcid: toolcallid, name: toolname, data: `Error:${err}`}})) + gMe.workers_postmessage_for_main(gMe.workers.js, chatid, toolcallid, toolname, `Error:${err}`); }) } } @@ -343,15 +333,13 @@ async function fetchpdftext_setup(tcs) { /** * Used to get hold of the web worker to use for running tool/function call related code * Also to setup tool calls, which need to cross check things at runtime - * @param {Worker} toolsWorker * @param {mChatMagic.Me} me */ -export async function init(toolsWorker, me) { +export async function init(me) { /** * @type {Object>} tcs */ let tc_switch = {} - gToolsWorker = toolsWorker gMe = me await fetchweburlraw_setup(tc_switch) await fetchweburltext_setup(tc_switch) From 2534af821562e7b1ee11fe647731a4fea82edba2 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 6 Nov 2025 02:23:37 +0530 Subject: [PATCH 237/446] SimpleChatTC:Rather bring in Tools Class So that all tools related management logic sits in tools module itself, but is accessible from Me by having a instance of Tools. The Workers moved into Tools class. The tc_switch moved into Tools class. The setup_workers, init, meta and tool_call moved into Tools class. --- tools/server/public_simplechat/tools.mjs | 132 ++++++++++++----------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 5f1c46301f..0ec037aa35 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -11,80 +11,88 @@ import * as tdb from './tooldb.mjs' import * as mChatMagic from './simplechat.js' -/** - * Maintain currently available tool/function calls - * @type {Object>} - */ -export let tc_switch = {} +class Tools { -/** - * @param {mChatMagic.Me} me - */ -function setup_workers(me) { - me.workers.js = new Worker('./toolsworker.mjs', { type: 'module' }); - me.workers.db = new Worker('./toolsdbworker.mjs', { type: 'module' }); -} + constructor() { + /** + * Maintain currently available tool/function calls + * @type {Object>} + */ + this.tc_switch = {} + this.workers = { + js: /** @type {Worker} */(/** @type {unknown} */(undefined)), + db: /** @type {Worker} */(/** @type {unknown} */(undefined)), + } + + } + + setup_workers() { + this.workers.js = new Worker('./toolsworker.mjs', { type: 'module' }); + this.workers.db = new Worker('./toolsdbworker.mjs', { type: 'module' }); + } -/** - * @param {mChatMagic.Me} me - */ -export async function init(me) { - setup_workers(me); /** - * @type {string[]} + * @param {mChatMagic.Me} me */ - let toolNames = [] - await tjs.init(me).then(()=>{ - for (const key in tjs.tc_switch) { - tc_switch[key] = tjs.tc_switch[key] + async init(me) { + this.setup_workers(); + /** + * @type {string[]} + */ + let toolNames = [] + await tjs.init(me).then(()=>{ + for (const key in tjs.tc_switch) { + this.tc_switch[key] = tjs.tc_switch[key] + toolNames.push(key) + } + }) + await tdb.init(me).then(()=>{ + for (const key in tdb.tc_switch) { + this.tc_switch[key] = tdb.tc_switch[key] + toolNames.push(key) + } + }) + let tNs = await tweb.init(me) + for (const key in tNs) { + this.tc_switch[key] = tNs[key] toolNames.push(key) } - }) - await tdb.init(me).then(()=>{ - for (const key in tdb.tc_switch) { - tc_switch[key] = tdb.tc_switch[key] - toolNames.push(key) + return toolNames + } + + meta() { + let tools = [] + for (const key in this.tc_switch) { + tools.push(this.tc_switch[key]["meta"]) } - }) - let tNs = await tweb.init(me) - for (const key in tNs) { - tc_switch[key] = tNs[key] - toolNames.push(key) + return tools } - return toolNames -} - -export function meta() { - let tools = [] - for (const key in tc_switch) { - tools.push(tc_switch[key]["meta"]) - } - return tools -} - - -/** - * Try call the specified tool/function call. - * Returns undefined, if the call was placed successfully - * Else some appropriate error message will be returned. - * @param {string} chatid - * @param {string} toolcallid - * @param {string} toolname - * @param {string} toolargs - */ -export async function tool_call(chatid, toolcallid, toolname, toolargs) { - for (const fn in tc_switch) { - if (fn == toolname) { - try { - tc_switch[fn]["handler"](chatid, toolcallid, fn, JSON.parse(toolargs)) - return undefined - } catch (/** @type {any} */error) { - return `Tool/Function call raised an exception:${error.name}:${error.message}` + /** + * Try call the specified tool/function call. + * Returns undefined, if the call was placed successfully + * Else some appropriate error message will be returned. + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {string} toolargs + */ + async tool_call(chatid, toolcallid, toolname, toolargs) { + for (const fn in this.tc_switch) { + if (fn == toolname) { + try { + this.tc_switch[fn]["handler"](chatid, toolcallid, fn, JSON.parse(toolargs)) + return undefined + } catch (/** @type {any} */error) { + return `Tool/Function call raised an exception:${error.name}:${error.message}` + } } } + return `Unknown Tool/Function Call:${toolname}` } - return `Unknown Tool/Function Call:${toolname}` + + + } From 4d71ded5df22d446905a29f624d40d55e1c2c3b4 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 6 Nov 2025 02:38:09 +0530 Subject: [PATCH 238/446] SimpleChatTC:ToolsManager: Instantiate in Me and Use Rename Tools to ToolsManager to convey its semantic better. Move setup of workers onmessage callback as well as directly passing result to these callbacks into ToolsManager. Now that Workers have been moved into ToolsManager, and ToolsManager has been instantiated as a member of Me, use the same in place of prev workers of Me. --- tools/server/public_simplechat/simplechat.js | 44 +++----------------- tools/server/public_simplechat/tooldb.mjs | 2 +- tools/server/public_simplechat/tooljs.mjs | 6 +-- tools/server/public_simplechat/tools.mjs | 36 +++++++++++++++- tools/server/public_simplechat/toolweb.mjs | 6 +-- 5 files changed, 47 insertions(+), 47 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 21be956c5b..d5e6969805 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -5,7 +5,7 @@ import * as du from "./datautils.mjs"; import * as ui from "./ui.mjs" -import * as tools from "./tools.mjs" +import * as mTools from "./tools.mjs" class Roles { @@ -522,7 +522,7 @@ class SimpleChat { obj["stream"] = true; } if (this.me.tools.enabled) { - obj["tools"] = tools.meta(); + obj["tools"] = this.me.toolsMgr.meta(); } return JSON.stringify(obj); } @@ -751,7 +751,7 @@ class SimpleChat { return "Tool/Function call name not specified" } try { - return await tools.tool_call(this.chatId, toolcallid, toolname, toolargs) + return await this.me.toolsMgr.tool_call(this.chatId, toolcallid, toolname, toolargs) } catch (/** @type {any} */error) { return `Tool/Function call raised an exception:${error.name}:${error.message}` } @@ -1070,7 +1070,7 @@ class MultiChatUI { }) // Handle messages from tools web workers - this.me.workers_cb((cid, tcid, name, data)=>{ + this.me.toolsMgr.workers_cb((cid, tcid, name, data)=>{ clearTimeout(this.timers.toolcallResponseTimeout) this.timers.toolcallResponseTimeout = undefined let chat = this.simpleChats[cid]; @@ -1386,10 +1386,7 @@ export class Me { //"frequency_penalty": 1.2, //"presence_penalty": 1.2, }; - this.workers = { - js: /** @type {Worker} */(/** @type {unknown} */(undefined)), - db: /** @type {Worker} */(/** @type {unknown} */(undefined)), - } + this.toolsMgr = new mTools.ToolsManager() } /** @@ -1473,35 +1470,4 @@ export class Me { }) } - /** - * Setup the callback that will be called when ever message - * is recieved from the Tools Web Workers. - * @param {(chatId: string, toolCallId: string, name: string, data: string) => void} cb - */ - workers_cb(cb) { - this.workers.js.onmessage = function (ev) { - cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) - } - this.workers.db.onmessage = function (ev) { - cb(ev.data.cid, ev.data.tcid, ev.data.name, JSON.stringify(ev.data.data, (k,v)=>{ - return (v === undefined) ? '__UNDEFINED__' : v; - })); - } - } - - /** - * Send a message to specified tools web worker's monitor in main thread directly - * @param {Worker} worker - * @param {string} chatid - * @param {string} toolcallid - * @param {string} toolname - * @param {string} data - */ - workers_postmessage_for_main(worker, chatid, toolcallid, toolname, data) { - let mev = new MessageEvent('message', {data: {cid: chatid, tcid: toolcallid, name: toolname, data: data}}); - if (worker.onmessage != null) { - worker.onmessage(mev) - } - } - } diff --git a/tools/server/public_simplechat/tooldb.mjs b/tools/server/public_simplechat/tooldb.mjs index fd02facffc..365006dced 100644 --- a/tools/server/public_simplechat/tooldb.mjs +++ b/tools/server/public_simplechat/tooldb.mjs @@ -95,7 +95,7 @@ let dslist_meta = { * @param {any} obj */ function dsops_run(chatid, toolcallid, toolname, obj) { - gMe.workers.db.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) + gMe.toolsMgr.workers.db.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, args: obj}) } diff --git a/tools/server/public_simplechat/tooljs.mjs b/tools/server/public_simplechat/tooljs.mjs index b3d18ce370..ad7cc4d0b2 100644 --- a/tools/server/public_simplechat/tooljs.mjs +++ b/tools/server/public_simplechat/tooljs.mjs @@ -77,7 +77,7 @@ function sysdatetime_run(chatid, toolcallid, toolname, obj) { break; } } - gMe.workers_postmessage_for_main(gMe.workers.js, chatid, toolcallid, toolname, sDT); + gMe.toolsMgr.workers_postmessage_for_main(gMe.toolsMgr.workers.js, chatid, toolcallid, toolname, sDT); } @@ -109,7 +109,7 @@ let js_meta = { * @param {any} obj */ function js_run(chatid, toolcallid, toolname, obj) { - gMe.workers.js.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: obj["code"]}) + gMe.toolsMgr.workers.js.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: obj["code"]}) } @@ -141,7 +141,7 @@ let calc_meta = { * @param {any} obj */ function calc_run(chatid, toolcallid, toolname, obj) { - gMe.workers.js.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) + gMe.toolsMgr.workers.js.postMessage({ cid: chatid, tcid: toolcallid, name: toolname, code: `console.log(${obj["arithexpr"]})`}) } diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 0ec037aa35..da22de2ed0 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -12,7 +12,7 @@ import * as mChatMagic from './simplechat.js' -class Tools { +export class ToolsManager { constructor() { /** @@ -34,6 +34,8 @@ class Tools { } /** + * Initialise the ToolsManager, + * including all the different tools groups. * @param {mChatMagic.Me} me */ async init(me) { @@ -62,6 +64,9 @@ class Tools { return toolNames } + /** + * Prepare the tools meta data that can be passed to the ai server. + */ meta() { let tools = [] for (const key in this.tc_switch) { @@ -93,6 +98,35 @@ class Tools { return `Unknown Tool/Function Call:${toolname}` } + /** + * Setup the callback that will be called when ever message + * is recieved from the Tools Web Workers. + * @param {(chatId: string, toolCallId: string, name: string, data: string) => void} cb + */ + workers_cb(cb) { + this.workers.js.onmessage = function (ev) { + cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) + } + this.workers.db.onmessage = function (ev) { + cb(ev.data.cid, ev.data.tcid, ev.data.name, JSON.stringify(ev.data.data, (k,v)=>{ + return (v === undefined) ? '__UNDEFINED__' : v; + })); + } + } + /** + * Send a message to specified tools web worker's monitor in main thread directly + * @param {Worker} worker + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {string} data + */ + workers_postmessage_for_main(worker, chatid, toolcallid, toolname, data) { + let mev = new MessageEvent('message', {data: {cid: chatid, tcid: toolcallid, name: toolname, data: data}}); + if (worker.onmessage != null) { + worker.onmessage(mev) + } + } } diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index a2181e4647..7ca4205ea3 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -41,7 +41,7 @@ async function bearer_transform() { * @param {any} objHeaders */ async function proxyserver_get_anyargs(chatid, toolcallid, toolname, objSearchParams, path, objHeaders={}) { - if (gMe.workers.js.onmessage != null) { + if (gMe.toolsMgr.workers.js.onmessage != null) { let params = new URLSearchParams(objSearchParams) let newUrl = `${gMe.tools.proxyUrl}/${path}?${params}` let headers = new Headers(objHeaders) @@ -53,9 +53,9 @@ async function proxyserver_get_anyargs(chatid, toolcallid, toolname, objSearchPa } return resp.text() }).then(data => { - gMe.workers_postmessage_for_main(gMe.workers.js, chatid, toolcallid, toolname, data); + gMe.toolsMgr.workers_postmessage_for_main(gMe.toolsMgr.workers.js, chatid, toolcallid, toolname, data); }).catch((err)=>{ - gMe.workers_postmessage_for_main(gMe.workers.js, chatid, toolcallid, toolname, `Error:${err}`); + gMe.toolsMgr.workers_postmessage_for_main(gMe.toolsMgr.workers.js, chatid, toolcallid, toolname, `Error:${err}`); }) } } From b2e7f5fd44a6194e1392d94324c3801644f24363 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 6 Nov 2025 03:28:33 +0530 Subject: [PATCH 239/446] SimpleChatTC:ToolsManager: Cleanup inc delayed direct posting Me.tools.toolNames is now directly updated by init of ToolsManager The two then in the old tools.init was also unneeded then also as both could have been merged into a single then, even then. However with the new flow, the 1st then is no longer required. Also now the direct calling of onmessage handler on the main thread side wrt immidiate result from tool call is delayed for a cycling through the events loop, by using a setTimeout. No longer expose the tools module throught documents, given that the tools module mainly contains ToolsManager, whose only instance is available through the global gMe. Move the devel related exposing throught document object into a function of its own. --- tools/server/public_simplechat/main.js | 26 ++++++++++++-------- tools/server/public_simplechat/readme.md | 14 ++++++++--- tools/server/public_simplechat/tools.mjs | 30 +++++++++++++++++------- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/tools/server/public_simplechat/main.js b/tools/server/public_simplechat/main.js index 0da0ea508a..c26dab47c8 100644 --- a/tools/server/public_simplechat/main.js +++ b/tools/server/public_simplechat/main.js @@ -1,28 +1,34 @@ // @ts-check -// A simple completions and chat/completions test related web front end logic +// A simple implementation of GenAi/LLM chat web client ui / front end logic. +// It handshake with ai server's completions and chat/completions endpoints +// and helps with basic usage and testing. // by Humans for All import * as mChatMagic from './simplechat.js' -import * as tools from "./tools.mjs" import * as du from "./datautils.mjs"; - /** @type {mChatMagic.Me} */ let gMe; -function startme() { - console.log("INFO:SimpleChat:StartMe:Starting..."); - gMe = new mChatMagic.Me(); - gMe.debug_disable(); + +function devel_expose() { // @ts-ignore document["gMe"] = gMe; // @ts-ignore document["du"] = du; - // @ts-ignore - document["tools"] = tools; - tools.init(gMe).then((toolNames)=>gMe.tools.toolNames=toolNames).then(()=>gMe.multiChat.chat_show(gMe.multiChat.curChatId)) +} + + +function startme() { + console.log("INFO:SimpleChat:StartMe:Starting..."); + gMe = new mChatMagic.Me(); + gMe.debug_disable(); + devel_expose() + gMe.toolsMgr.init(gMe).then(()=>{ + gMe.multiChat.chat_show(gMe.multiChat.curChatId); + }) for (let cid of gMe.defaultChatIds) { gMe.multiChat.new_chat_session(cid); } diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 65f77897ec..784c5c6ef6 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -632,9 +632,17 @@ sliding window based drop off or even before they kick in, this can help in many * sys_date_time tool call has been added. -* SimpleChat - Move the main chat related classes into its own js module file, independent of the -main runtime entry point. This allows these classes to be referenced from other modules like tools -related modules with full access to their details for developers and static check tools. +* Refactor code and flow a bit wrt the client web ui + * Move the main chat related classes into its own js module file, independent of the main + runtime entry point (rather move out the runtime entry point into its own file). This allows + these classes to be referenced from other modules like tools related modules with full access + to these classes's details for developers and static check tools. + * building on same make the Tools management code into a ToolsManager class which is inturn + instantiated and the handle stored in top level Me class. This class also maintains and + manages the web workers as well as routing of the tool calling among others. + * add a common helper for posting results directly to the main thread side web worker callback + handlers. Inturn run the calling through a setTimeout0, so that delayed/missing response + situation rescuer timeout logic etal flow doesnt get messed for now. #### ToDo diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index da22de2ed0..b3aa3d843f 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -43,25 +43,24 @@ export class ToolsManager { /** * @type {string[]} */ - let toolNames = [] + me.tools.toolNames = [] await tjs.init(me).then(()=>{ for (const key in tjs.tc_switch) { this.tc_switch[key] = tjs.tc_switch[key] - toolNames.push(key) + me.tools.toolNames.push(key) } }) await tdb.init(me).then(()=>{ for (const key in tdb.tc_switch) { this.tc_switch[key] = tdb.tc_switch[key] - toolNames.push(key) + me.tools.toolNames.push(key) } }) let tNs = await tweb.init(me) for (const key in tNs) { this.tc_switch[key] = tNs[key] - toolNames.push(key) + me.tools.toolNames.push(key) } - return toolNames } /** @@ -115,7 +114,18 @@ export class ToolsManager { } /** - * Send a message to specified tools web worker's monitor in main thread directly + * Send message to specified Tools-WebWorker's monitor/onmessage handler of main thread + * by calling it directly. + * + * The specified web worker's main thread monitor/callback logic is triggerd in a delayed + * manner by cycling the call through the events loop by using a setTimeout 0, so that the + * callback gets executed only after the caller's code following the call to this helper + * is done. + * + * NOTE: This is needed to ensure that any tool call handler that returns the tool call + * result immidiately without using any asynhronous mechanism, doesnt get-messed-by / + * mess-with the delayed response identifier and rescuer timeout logic. + * * @param {Worker} worker * @param {string} chatid * @param {string} toolcallid @@ -124,9 +134,11 @@ export class ToolsManager { */ workers_postmessage_for_main(worker, chatid, toolcallid, toolname, data) { let mev = new MessageEvent('message', {data: {cid: chatid, tcid: toolcallid, name: toolname, data: data}}); - if (worker.onmessage != null) { - worker.onmessage(mev) - } + setTimeout(function() { + if (worker.onmessage != null) { + worker.onmessage(mev) + } + }, 0); } } From 313b5f6db70afbce6ce16d7cc09dea7a184e2c70 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 6 Nov 2025 18:51:42 +0530 Subject: [PATCH 240/446] SimpleChatTC:TrackToolCalls:helps ignor delayed response, if reqd Add a pending object which maintains the pending toolcallid wrt each chat session, when ever a tool call is made. In turn when ever a tool call response is got cross check if its toolcallid matches that in the pending list. If so accept the tool call response and remove from pending list. If not just ignore the response. NOTE: The current implementation supports only 1 pending tool call at any time. NOTE: Had to change from a anonymous to arrow function so as to be able to get access to the ToolsManager instance (this) from within the function. ie make use of lexical binding semantic of arrow functions. --- tools/server/public_simplechat/tools.mjs | 54 +++++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index b3aa3d843f..9c955415a6 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -26,6 +26,12 @@ export class ToolsManager { db: /** @type {Worker} */(/** @type {unknown} */(undefined)), } + /** + * Maintain the latest pending tool call id for each unique chat session id + * @type {Object} + */ + this.pending = {} + } setup_workers() { @@ -74,6 +80,43 @@ export class ToolsManager { return tools } + /** + * Add specified toolcallid to pending list for specified chat session id. + * @param {string} chatid + * @param {string} toolcallid + */ + toolcallpending_add(chatid, toolcallid) { + console.debug(`DBUG:ToolsManager:ToolCallPendingAdd:${chatid}:${toolcallid}`) + this.pending[chatid] = toolcallid; + } + + /** + * Clear pending list for specified chat session id. + * @param {string} chatid + * @param {string} tag + */ + toolcallpending_clear(chatid, tag) { + let curtcid = this.pending[chatid]; + console.debug(`DBUG:ToolsManager:ToolCallPendingClear:${tag}:${chatid}:${curtcid}`) + delete(this.pending[chatid]); + } + + /** + * Check if there is a pending tool call awaiting tool call result for given chat session id. + * Clears from pending list, if found. + * @param {string} chatid + * @param {string} toolcallid + * @param {string} tag + */ + toolcallpending_found_cleared(chatid, toolcallid, tag) { + if (this.pending[chatid] !== toolcallid) { + console.log(`WARN:ToolsManager:ToolCallPendingFoundCleared:${tag}:${chatid}:${toolcallid} not found, skipping...`) + return false + } + this.toolcallpending_clear(chatid, tag) + return true + } + /** * Try call the specified tool/function call. * Returns undefined, if the call was placed successfully @@ -87,6 +130,7 @@ export class ToolsManager { for (const fn in this.tc_switch) { if (fn == toolname) { try { + this.toolcallpending_add(chatid, toolcallid); this.tc_switch[fn]["handler"](chatid, toolcallid, fn, JSON.parse(toolargs)) return undefined } catch (/** @type {any} */error) { @@ -103,10 +147,16 @@ export class ToolsManager { * @param {(chatId: string, toolCallId: string, name: string, data: string) => void} cb */ workers_cb(cb) { - this.workers.js.onmessage = function (ev) { + this.workers.js.onmessage = (ev) => { + if (!this.toolcallpending_found_cleared(ev.data.cid, ev.data.tcid, 'js')) { + return + } cb(ev.data.cid, ev.data.tcid, ev.data.name, ev.data.data) } - this.workers.db.onmessage = function (ev) { + this.workers.db.onmessage = (ev) => { + if (!this.toolcallpending_found_cleared(ev.data.cid, ev.data.tcid, 'db')) { + return + } cb(ev.data.cid, ev.data.tcid, ev.data.name, JSON.stringify(ev.data.data, (k,v)=>{ return (v === undefined) ? '__UNDEFINED__' : v; })); From f7cff8232d269de14b1c4b93f9dcca0a80b446de Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 6 Nov 2025 19:52:56 +0530 Subject: [PATCH 241/446] SimpleChatTC:TCPending: Clear pending in unhappy paths ie if exception raised during tool call execution and or time out occurs --- tools/server/public_simplechat/readme.md | 6 ++++++ tools/server/public_simplechat/simplechat.js | 2 ++ tools/server/public_simplechat/tools.mjs | 1 + 3 files changed, 9 insertions(+) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 784c5c6ef6..a1764b740a 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -644,6 +644,12 @@ sliding window based drop off or even before they kick in, this can help in many handlers. Inturn run the calling through a setTimeout0, so that delayed/missing response situation rescuer timeout logic etal flow doesnt get messed for now. +* track tool calling and inturn maintain pending tool calls so that only still valid tool call responses + will be accepted when the asynchronous tool call response is recieved. Also take care of clearing + pending tool call tracking in unhappy paths like when exception noticied as part of tool call execution, + or if there is no response within the configured timeout period. + NOTE: Currently the logic supports only 1 pending tool call per chat session. + #### ToDo diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index d5e6969805..6a583aab1c 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -753,6 +753,7 @@ class SimpleChat { try { return await this.me.toolsMgr.tool_call(this.chatId, toolcallid, toolname, toolargs) } catch (/** @type {any} */error) { + this.me.toolsMgr.toolcallpending_found_cleared(this.chatId, toolcallid, 'SC:HandleToolCall:Exc') return `Tool/Function call raised an exception:${error.name}:${error.message}` } } @@ -1213,6 +1214,7 @@ class MultiChatUI { this.ui_reset_userinput(false) } else { this.timers.toolcallResponseTimeout = setTimeout(() => { + this.me.toolsMgr.toolcallpending_found_cleared(chat.chatId, toolCallId, 'MCUI:HandleToolRun:TimeOut') chat.add(new ChatMessageEx(Roles.ToolTemp, ChatMessageEx.createToolCallResultAllInOne(toolCallId, toolname, `Tool/Function call ${toolname} taking too much time, aborting...`))) this.chat_show(chat.chatId) this.ui_reset_userinput(false) diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 9c955415a6..76f95e0947 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -134,6 +134,7 @@ export class ToolsManager { this.tc_switch[fn]["handler"](chatid, toolcallid, fn, JSON.parse(toolargs)) return undefined } catch (/** @type {any} */error) { + this.toolcallpending_found_cleared(chatid, toolcallid, 'ToolsManager:ToolCall:Exc') return `Tool/Function call raised an exception:${error.name}:${error.message}` } } From 899dff4eb4903f4152217d8830a4432d2cb4a70f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 6 Nov 2025 23:44:11 +0530 Subject: [PATCH 242/446] SimpleChatTC:IndexHTML:Fix a oversight with new module added Add forgotten to add , after simplechat entry. Currently I am not strictly using the importmap feature, so the error didnt create any problem, but the error was there which has been fixed. --- tools/server/public_simplechat/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/index.html b/tools/server/public_simplechat/index.html index d34a80dfb8..67a3834eb1 100644 --- a/tools/server/public_simplechat/index.html +++ b/tools/server/public_simplechat/index.html @@ -11,7 +11,7 @@ diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 6958948edf..a889645f6f 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -1,5 +1,5 @@ -# SimpleChat +# SimpleChat / AnveshikaSallap by Humans for All. diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index a348a67115..273f6d3e28 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1,6 +1,9 @@ // @ts-check -// Core classes which provide a simple implementation of handshake with ai server's completions and chat/completions endpoints -// as well as related web front end logic for basic usage and testing. +// Core classes which provide a simple implementation of handshake with ai server's completions and +// chat/completions endpoints as well as related web front end logic for basic usage and testing. +// Supports tool calling (including a bunch of builtin ones), reasoning and vision related handshakes, +// if supported by the ai model, that one is interacting with. +// // by Humans for All import * as du from "./datautils.mjs"; @@ -18,6 +21,7 @@ export const AI_TC_SESSIONNAME = `TCExternalAI` const ROLES_TEMP_ENDSWITH = TEMP_MARKER + export class Roles { static System = "system"; static User = "user"; @@ -451,21 +455,26 @@ function usage_note(sRecentUserMsgCnt) {
    Usage Note
      -
    • New button creates new chat session, with its own system prompt.
    • +
    • New btn creates new chat session, with its own system prompt & settings.
    • Prompt button toggles system prompt entry.
    • -
    • System prompt above, helps control ai response characteristics.
      • -
      • Completion mode - no system prompt normally.
      • +
      • System prompt, helps control ai response characteristics.
      • +
      • No system prompt normally if using Completion mode
    • Use shift+enter for inserting enter/newline.
    • -
    • Enter your query/response to ai assistant in text area provided below.
    • -
    • Use image button for vision models, submitting or switching session clears same
    • -
    • Settings button allows current chat session's configuration to be updated.
    • -
    • Remember that each chat session has its own setting.
    • -
    • settings-tools-enabled should be true to enable tool calling.
    • +
    • Enter your query/response to ai assistant in user input area provided below.
      • -
      • If ai assistant requests a tool call, verify same before triggering.
      • +
      • image btn for vision models, submitting / switching session clears same
      • +
      +
    • Settings button allows current chat session's configuration to be updated.
    • +
        +
      • Remember that each chat session has its own setting.
      • +
      +
    • settings-tools-enabled should be true for tool calling.
    • +
        +
      • if ai assistant requests a tool call, verify same before triggering.
      • submit tool response placed into user query/response text area
      • +
      • for web access inc search/pdf tool calls, run included simpleproxy.py
    • ContextWindow = [System, ${sRecentUserMsgCnt} User Query/Resp, Cur Query].
      • @@ -2157,7 +2166,7 @@ export class Config { export class Me { constructor() { - this.defaultChatIds = [ "Default", "Other", AI_TC_SESSIONNAME ]; + this.defaultChatIds = [ "Chat1", "Chat2", AI_TC_SESSIONNAME ]; this.defaultCfg = new Config() this.multiChat = new MultiChatUI(this); this.toolsMgr = new mTools.ToolsManager() From 4bca1f6f3e01dfe7504cf393f60cdf32cf40013f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 24 Nov 2025 14:43:50 +0530 Subject: [PATCH 357/446] SimpleChatTCRV:UIRefresh cleanup: Show only msgs in sliding window Ensure we are working with the Chat Messages in Chat session, which are in the currently active sliding window. Remove any chat message blocks in the chat session ui, which are no longer in the sliding window of context. This brings uirefresh semantic to be in sync with chat_show logic --- tools/server/public_simplechat/readme.md | 11 +++------ tools/server/public_simplechat/simplechat.js | 25 +++++++++++++++++--- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index a889645f6f..14969f5508 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -906,6 +906,9 @@ Chat Session specific settings tool call proxy server. * however any resultant changes to the available tool calls list wont get reflected, till one reloads the program. +* uirefresh helper ensures client side sliding window is always satisfied. + * now it remove messages no longer in the sliding window, so user only sees what is sent to the ai server, + in the chat session messages ui. #### ToDo @@ -933,14 +936,6 @@ session to the end user, but inturn not to be sent to the ai server. Ex current Updating system prompt, will reset user input area fully now, which seems a good enough behaviour, while keeping the code flow also simple and straight, do I need to change it, I dont think so as of now. -Should I have a toggle to control whether show only the messages sent to ai server based on sliding window -or show all the messages (ie even beyond the sliding window)? -* rather previously with chat_show only whats in current sliding window was being shown, but now with - the uirefresh based logic, all messages from last chat_show will be shown irrespective of whether still - in ai server handshake related sliding window or not. -* Update UIRefresh helper to optionally remove messages no longer in the sliding window, so user only sees - what is sent to the ai server in the chat session messages ui. - For now amn't bringing in mozilla/github/standard-entities pdf, md, mathslatex etal javascript libraries for their respective functionalities. diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 273f6d3e28..6f7706ff85 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -481,6 +481,9 @@ function usage_note(sRecentUserMsgCnt) {
      • ChatHistInCtxt, MaxTokens, ModelCtxt window to expand
    • ${AI_TC_SESSIONNAME} session keeps tool calls disabled, to avoid recursive...
    • +
        +
      • Used by external_ai tool call, which allows ai calling ai, as needed.
      • +
    `; return sUsageNote; @@ -1421,6 +1424,7 @@ class MultiChatUI { // Create main section let secMain = document.createElement('section') secMain.id = `cmuid${msg.uniqId}` + secMain.dataset.cmuid = String(msg.uniqId) secMain.classList.add(`role-${msg.ns.role}`) secMain.classList.add('chat-message') secMain.addEventListener('mouseenter', (ev)=>{ @@ -1510,7 +1514,7 @@ class MultiChatUI { } /** - * Refresh UI wrt given chatId, provided it matches the currently selected chatId + * Refresh UI (optionally bruteforce) wrt given chatId, provided it matches the currently selected chatId * * Show the chat contents in elDivChat. * Also update @@ -1522,6 +1526,8 @@ class MultiChatUI { * * option to load prev saved chat if any * * as well as settings/info. * + * Ensures only messages with in the currently set sliding window are shown + * * @param {string} chatId * @param {boolean} bClear * @param {boolean} bShowInfoAll @@ -1585,6 +1591,8 @@ class MultiChatUI { * If the houseKeeping.clear flag is set then fallback to * the brute force full on chat_show. * + * Also ensures only messages with in the currently set sliding window are shown + * * @param {string} chatId * @param {number} lastN */ @@ -1599,9 +1607,20 @@ class MultiChatUI { } this.ui_userinput_reset(false) this.ui_toolcallvalidated_as_needed(new ChatMessageEx()); + let chatToShow = chat.recent_chat(chat.cfg.chatProps.iRecentUserMsgCnt); + // Remove messages outside sliding window + /** @type {NodeListOf} */ + let elList = this.elDivChat.querySelectorAll('[id*="cmuid"]') + for (const el of elList) { + if (Number(el.dataset.cmuid) >= chatToShow[0].uniqId) { + break + } + el.remove() + } + // Refresh last few messages in the chat history, as requested by user for(let i=lastN; i > 0; i-=1) { - let msg = chat.xchat[chat.xchat.length-i] - let nextMsg = chat.xchat[chat.xchat.length-(i-1)] + let msg = chatToShow[chatToShow.length-i] + let nextMsg = chatToShow[chatToShow.length-(i-1)] if (msg) { this.chatmsg_ui_remove(msg.uniqId) this.show_message(this.elDivChat, chat.chatId, msg, (i-1), nextMsg) From b7b9109cb0c2de0c83f9b006628a457aed815f0d Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 24 Nov 2025 15:54:18 +0530 Subject: [PATCH 358/446] SimpleChatTCRV:Cleanup Make a note on how user can always view the full chat history if they want to. --- tools/server/public_simplechat/readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 14969f5508..3b6ac35ef4 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -909,6 +909,10 @@ Chat Session specific settings * uirefresh helper ensures client side sliding window is always satisfied. * now it remove messages no longer in the sliding window, so user only sees what is sent to the ai server, in the chat session messages ui. + * avoids adding additional control specifically for ui, and instead stick to the ai nw handshake related + chat sliding window size (which takes care of try avoid overloading the ai model context size) selected + by user already. User can always change the sliding window size to view past messages beyond the currently + active sliding window size and then switch back again, if they want to. #### ToDo From b1cfedaba7bbb46f76112f024aa7714ea6b07b18 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 24 Nov 2025 16:23:27 +0530 Subject: [PATCH 359/446] SimpleChatTCRV:Misc cleanup continues... --- tools/server/public_simplechat/readme.md | 12 ++++++++---- tools/server/public_simplechat/simplechat.js | 10 ++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 3b6ac35ef4..8c475c1599 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -455,7 +455,7 @@ always run this from a discardable vm, just in case if one wants to be extra cau The following tools/functions are currently provided by default -##### directly in browser +##### directly in and using browser capabilities * sys_date_time - provides the current date and time @@ -485,6 +485,8 @@ The following tools/functions are currently provided by default * or so and so and so ... * given the fuzzy nature of the generative ai, sometimes the model may even use this tool call to get answer to questions like what is your name ;> + * end user can use this mechanism to try and bring in an instance of ai running on a more powerful + machine, but then to be used only if needed or so Most of the above (except for external ai call) are run from inside web worker contexts. Currently the ai generated code / expression is run through a simple minded eval inside a web worker mechanism. Use @@ -935,7 +937,9 @@ if the models support same. It should also take care of some aspects of the tool potentially. MAYBE add a special ClientSideOnly role for use wrt Chat history to maintain things to be shown in a chat -session to the end user, but inturn not to be sent to the ai server. Ex current settings or so ... +session to the end user, but inturn not to be sent to the ai server. Ex current settings, any edits to toolcall, +any tool call or server handshake errors seen (which user might have worked around as needed and continued the +conversation) or so ... Updating system prompt, will reset user input area fully now, which seems a good enough behaviour, while keeping the code flow also simple and straight, do I need to change it, I dont think so as of now. @@ -963,9 +967,9 @@ in devel console of the browser * remove the assistant response from end of chat session, if any, using * document['gMe'].multiChat.simpleChats['SessionId'].xchat.pop() * [202511] One can even use the del button in the popover menu wrt each chat message to delete - * reset role of Tool response chat message to TOOL.TEMP from tool + * reset role of Tool response chat message to TOOL-TEMP from tool * toolMessageIndex = document['gMe'].multiChat.simpleChats['SessionId'].xchat.length - 1 - * document['gMe'].multiChat.simpleChats['SessionId'].xchat[toolMessageIndex].role = "TOOL.TEMP" + * document['gMe'].multiChat.simpleChats['SessionId'].xchat[toolMessageIndex].role = "TOOL-TEMP" * if you dont mind running the tool call again, just deleting the tool response message will also do * clicking on the SessionId at top in UI, should refresh the chat ui and inturn it should now give the option to control that tool call again diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 6f7706ff85..3abc6bd27d 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -33,9 +33,15 @@ export class Roles { /** Used to identify tool call response, which has not yet been accepted and submitted by users */ static ToolTemp = `TOOL${ROLES_TEMP_ENDSWITH}`; + + /** Could be used to maintain modified tool calls related info or errors or ... */ + static ExtraTemp = `EXTRA${ROLES_TEMP_ENDSWITH}`; + /** - * Used to maintain errors that wont be normally communicated back to ai server - * like error got during handshake with ai server or so. + * One could either use ExtraTemp Role to maintain the errors seen or maybe one could + * use this to maintain errors that wont be normally communicated back to ai server + * like error got during handshake with ai server or so, which in turn the user might + * work around by restarting the server with different / updated configuration or ... */ static ErrorTemp = `ERROR${ROLES_TEMP_ENDSWITH}`; } From 214ca2e00b469828b8087b66c38b3d3e87e26368 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 24 Nov 2025 19:23:44 +0530 Subject: [PATCH 360/446] SimpleChatTCRV: Documentation cleanup Move existing readme.md into docs/details.md Similarly move the screenshot image into docs/ --- .../{readme.md => docs/details.md} | 0 .../{ => docs}/simplechat_screens.webp | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename tools/server/public_simplechat/{readme.md => docs/details.md} (100%) rename tools/server/public_simplechat/{ => docs}/simplechat_screens.webp (100%) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/docs/details.md similarity index 100% rename from tools/server/public_simplechat/readme.md rename to tools/server/public_simplechat/docs/details.md diff --git a/tools/server/public_simplechat/simplechat_screens.webp b/tools/server/public_simplechat/docs/simplechat_screens.webp similarity index 100% rename from tools/server/public_simplechat/simplechat_screens.webp rename to tools/server/public_simplechat/docs/simplechat_screens.webp From ba6c7872cc3b3931d852b4694360b7b83119b194 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 24 Nov 2025 19:57:10 +0530 Subject: [PATCH 361/446] SimpleChatTCRV: split changelog from details into separate file --- .../public_simplechat/docs/changelog.md | 330 ++++++++++++++++++ .../server/public_simplechat/docs/details.md | 329 ----------------- 2 files changed, 330 insertions(+), 329 deletions(-) create mode 100644 tools/server/public_simplechat/docs/changelog.md diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md new file mode 100644 index 0000000000..16d0a9dbc2 --- /dev/null +++ b/tools/server/public_simplechat/docs/changelog.md @@ -0,0 +1,330 @@ +# Progress + +Look into source files and git logs for the details, this is a partial changelog of stuff already done +and some of the things that one may look at in the future. + +## Done + +Tool Calling support added, along with a bunch of useful tool calls as well as a bundled simple proxy +if one wants to access web as part of tool call usage. + +Reasoning / thinking response from Ai Models is shown to the user, as they are being generated/shared. + +Chat Messages/Session and UI handling have been moved into corresponding Classes to an extent, this +helps ensure that +* switching chat sessions or loading a previous auto saved chat session will restore state including + ui such that end user can continue the chat session from where they left it, even if in the middle + of a tool call handshake. +* new fields added to http handshake in oneshot or streaming mode can be handled in a structured way + to an extent. + +Chat message parts seperated out and tagged to allow theming chat message as needed in future. +The default Chat UI theme/look changed to help differentiate between different messages in chat +history as well as the parts of each message in a slightly better manner. Change the theme slightly +between normal and print views (beyond previous infinite height) for better printed chat history. + +A builtin data store related tool calls, inturn built on browser's indexedDB, without needing any +proxy / additional helper to handle the store. One could use the ai assistant to store ones (ie end +users) own data or data of ai model. + +Trap http response errors and inform user the specific error returned by ai server. + +Initial go at a pdftext tool call. It allows web / local pdf files to be read and their text content +extracted and passed to ai model for further processing, as decided by ai and end user. One could +either work with the full pdf or a subset of adjacent pages. + +SimpleProxy updates +* Convert from a single monolithic file into a collection of modules. +* UrlValidator to cross check scheme and domain of requested urls, + the whitelist inturn picked from config json +* Helpers to fetch file from local file system or the web, transparently +* Help check for needed modules before a particular service path is acknowledged as available + through /aum service path +* urltext and related - logic to drop contents of specified tag with a given id + * allow its use for the web search tool flow + * setup wrt default duckduckgo search result urltext plain text cleanup and found working. + * this works properly only if the html being processed has proper opening and ending tags + around the area of interest. + * remember to specify non overlapping tag blocks, if more than one specified for dropping. + * this path not tested, but should logically work + +Settings/Config default changes + +* Chances are for ai models which dont support tool calling, things will be such that the tool calls +meta data shared will be silently ignored without much issue. So enabling tool calling feature by +default, so that in case one is using a ai model with tool calling the feature is readily available +for use. + +* Revert SlidingWindow ChatHistory in Context from last 10 to last 5 (rather 2 more then origianl, +given more context support in todays models) by default, given that now tool handshakes go through +the tools related side channel in the http handshake and arent morphed into normal user-assistant +channel of the handshake. + +* Enable CachePrompt api option given that tool calling based interactions could involve chat sessions +having ai responses built over multiple steps of tool callings etal. So independent of our client side +sliding window based drop off or even before they kick in, this can help in many cases. + +UI - add ClearChat button and logic. Also add unicode icons for same as well as for Settings. + +Renamed pdf_to_text to fetch_pdf_as_text so that ai model can understand the semantic better. + +sys_date_time tool call has been added. + +Refactor code and flow a bit wrt the client web ui +* Move the main chat related classes into its own js module file, independent of the main + runtime entry point (rather move out the runtime entry point into its own file). This allows + these classes to be referenced from other modules like tools related modules with full access + to these classes's details for developers and static check tools. +* building on same make the Tools management code into a ToolsManager class which is inturn + instantiated and the handle stored in top level Me class. This class also maintains and + manages the web workers as well as routing of the tool calling among others. +* add a common helper for posting results directly to the main thread side web worker callback + handlers. Inturn run the calling through a setTimeout0, so that delayed/missing response + situation rescuer timeout logic etal flow doesnt get messed for now. + +Track tool calling and inturn maintain pending tool calls so that only still valid tool call responses +will be accepted when the asynchronous tool call response is recieved. Also take care of clearing +pending tool call tracking in unhappy paths like when exception noticied as part of tool call execution, +or if there is no response within the configured timeout period. +NOTE: Currently the logic supports only 1 pending tool call per chat session. + +Add support for fetch_xml_as_text tool call, fix importmaps in index.html + +Renamed and updated logic wrt xml fetching to be fetch_xml_filtered. allow one to use re to identify +the tags to be filtered in a fine grained manner including filtering based on tag heirarchy +* avoid showing empty skipped tag blocks + +Logic which shows the generated tool call has been updated to trap errors when parsing the function call +arguments generated by the ai. This ensures that the chat ui itself doesnt get stuck in it. Instead now +the tool call response can inform the ai model that its function call had issues. + +Renamed fetch_web_url_text to fetch_html_text, so that gen ai model wont try to use this to fetch xml or +rss files, because it will return empty content, because there wont be any html content to strip the tags +and unwanted blocks before returning. + +Capture the body of ai server not ok responses, to help debug as well as to show same to user. + +Extract and include the outline of titles (along with calculated numbering) in the text output of pdftext +* ensure that one doesnt recurse beyond a predefined limit. + +Convert NSChatMessage from typedef to Class and update ChatMessageEx, SimpleChat, MultiChatUI classes to +make use of the same. +* helpers consolidated + * helpers to check if given instance contains reasoning or content or toolcall or tool response related + fields/info in them. + * helpers to get the corresponding field values + * some of these helpers where in ChatMessageEx and beyond before +* now the network handshaked fields are declared as undefined by default (instead of empty values). + this ensures that json stringify will automatically discard fields whose values are still undefined. +* add fields wrt tool response and update full flow to directly work with these fields instead of the + xml based serialisation which was previously used for maintaining the different tool response fields + within the content field (and inturn extract from there when sending to server). + * now a dataset based attribute is used to identify when input element contains user input and when + it contains tool call result/response. +* this simplifies the flow wrt showing chat message (also make it appear more cleanly) as well as + identifying not yet accepted tool result and showing in user query input field and related things. +* ALERT: ON-DISK-STORAGE structure of chat sessions have changed wrt tool responses. So old saves will + no longer work wrt tool responses + +UI updates +* update logic to allow empty tool results to be sent to ai engine server +* css - when user input textarea is in tool result mode (ie wrt TOOL.TEMP role), change the background + color to match the tool role chat message block color, so that user can easily know that the input + area is being used for submitting tool response or user response, at any given moment in time. + +Vision +* Add image_url field. Allow user to load image, which is inturn stored as a dataURL in image_url. +* when user presses submit with a message, if there is some content (image for now) in dataURL, + then initialise image_url field with same. +* when generating chat messages for ai server network handshake, create the mixed content type of + content field which includes both the text (from content field) and image (from image_url field) + ie if a image_url is found wrt a image. + * follow the openai format/template wrt these mixed content messages. +* Usage: specify a mmproj file directly or through -hf, additionally had to set --batch-size to 8k + and ubatch-size to 2k wrt gemma3-4b-it +* when showing chat instantiate img elements to show image_urls. + * limit horizontally to max width and vertically to 20% of the height +* show any image loaded by the user, in the corresponding image button +* consolidate dataurl handling into a bunch of helper functions. +* trap quota errors wrt localStorage etal +* dont forget to reset the file type input's value, so that reselecting the same image still + triggers the input's change event. + +SimpleChat class now allows extra fields to be specified while adding, in a generic way using a +object/literal object or equivalent. + +UI Cleanup - msgs spaced out, toolcall edit hr not always, scroll ui only when required, +hide settings/info till user requests, heading gradient + +iDB module +* add open, transact, put and get. Use for chat session save and load +* getKeys used to show Restore/Load button wrt chat sessions. + +ChatMessage +* assign a globally unique (ie across sessions) id to each chat message instance. +* add support for deleting chat message based on its uniquie id in SimpleChat. + * try ensure that adjacent messages remain on screen, after a message is deleted from session. +* add a popover div block in html, which acts as a popup menu containing buttons to work with + individual chat messages. + * experiment and finalise on anchor based relative positioning of the popover menu. + * have a del button, which allows one to delete the currently in focus chat message. + * have a copy button, which allows one to copy the textual content into system clipboard. + +MultiChatUI +* chat_show takes care of showing or clearing tool call edit / trigger as well as tool response + edit / submit. Also show the currently active tool call and its response before it is submitted + was previously only shown in the edit / trigger and edit / submit ui elements, now instead it + also shows as part of the chat session message blocks, so that user can delete or copy these + if needed using the same mechanism as other messages in the chat session. +* use a delete msg helper, which takes care of deleting the msg from chat session as well as + efficiently update ui to any extent by removing the corresponding element directly from existing + chat session ui without recreating the full chat session ui. +* a helper to add a message into specified chat session, as well as show/update in the chat session + ui by appending the chat message, instead of recreating the full chat session ui. +... + +MultiChatUI+ +* both chat_show and chat_uirefresh (if lastN >= 2) both take care of updating tool call edit/trigger + as well as the tool call response edit/submit related ui elements suitably. + * chat_show recreates currently active sliding window of chat session (which could even be full) + * while chat_uirefresh recreates/updates ui only for the lastN messages (prefer in general, as optimal) +* normal user response / query submit as well as tool call response or error submit have been updated + to use the optimal uirefresh logic now. + +Cleanup in general +* Inform end user when loading from a saved session. +* Update starting entry point flow to avoid calling chat_show twice indirectly, inturn leading to + two restore previously saved session blocks. Rather when adding tool calls support, and inturn + had to account for delayed collating of available simpleproxy based tool calls, I forgot to clean + this flow up. +* Make the sys_date_time template description bit more verbose, just in case. +* ui_userinput_reset now also resets associated Role always, inturn + * full on version from chat_show, inturn when session switching. + So user switchs session will reset all user input area and related data, while + also ensuring user input area has the right needed associated role setup. + * partial version from uirefresh, inturn adding user or tool call response messages. +* ui cleanup + * more rounded buttons, chat messages and input area elements. + * make the body very very lightly gray in color, while the user input area is made whiter. + * gradients wrt heading, role-specific individual chat message blocks. + * avoid borders and instead give a box effect through light shadows. + * also avoid allround border around chat message role block and instead have to only one side. + * timeout close popover menu. + * usage notes + * update wrt vision and toggling of sessions and system prompt through main title area. + * fix issue with sliding window size not reflecting properly in context window entry. + * make restore block into details based block, and anchor its position independent of db check. + * avoid unneeded outer overall scrollbar by adjusting fullbody height in screen mode. + * user css variable to define the overall background color and inturn use same to merge gradients + to the background, as well as to help switch the same seemlessly between screen and print modes. + * make the scrollbars more subtle and in the background. + * allow user input textarea to grow vertically to some extent. + * make things rounded across board by default. add some padding to toolcall details block, ... + * use icons without text wrt chat sessions++, new chat, clear chat and settings top level buttons. + * use title property/attribute to give a hint to the user about the button functionality. + * add role specific background gradients wrt the tool call trigger and user input block as well as + fix wrt the tool temp message block. also wrt system input block at top. + * also rename the TEMP role tags to use -TEMP instead of .TEMP, so that CSS rule selectors will + treat such tags like role-TOOL-TEMP as say a proper css class name rather than messing up with + something like role-TOOL.TEMP which will get split to role-TOOL and TEMP and inturn corresponding + css rule doesnt/wont get applied. + * given that now there is a proper visual cue based seperation of the tool call trigger block from + surrounding content, using proper seperate tool call specific coloring, so remove the
    horiz + line seperation wrt tool call trigger block. + * however retain the horizontal line seperation between the tool trigger block and user input block, + given that some users and some ai dont seem to identify the difference very easily. + * work around firefox currently not yet supporting anchor based relative positioning of popover. + * ensure the new uirefresh flow handles the following situations in a clean way like + * a new chat session clearing out usagenote+restore+currentconfig, as user starts chatting + * the settings ui getting cleared out as user starts/continues chatting directly into user input + without using chat session button to switch back to the chat. +* Auto ObjPropsEdit UI + * allow it to be themed by assigning id to top level block. + * fix a oversight (forgotten $) with use of templated literals and having variables in them. + * ensure full props hierarchy is accounted for when setting the id of elements. +* Chat button to toggle sessions buttons and system prompt. +* Use unix date format markers wrt sys_date_time toolcall, also add w (day of week). +* Free up the useful vertical space by merging chat sessions buttons/tabs into heading +* Allow user to load multiple images and submit to ai as part of a single user message. +* Use popover ui to allow user to view larger versions of loaded images as well as remove before submitting + to ai, if and when needed. +* Add external_ai toolcall with no access to internet or tool calls (helps avoid recursive ai tool calling). + User can see response generated by the external ai tool call, as and when it is recieved. +* Maintain chat session specific DivStream elements, and show live ai responses (through corresponding + DivStream) wrt the current chat session as well as any from the external ai tool call session. + In future, if the logic is updated to allow switching chat session ui in the middle of a pending tool call + or pending ai server response, things wont mess up ui, as they will be updating their respective DivStream. + Also switching sessions takes care of showing the right DivStream ie of currently switched to chat, so that + end user can see the streamed response from that chat session as it is occuring. +* Cleanup the tool call descriptions and verbose messages returned a bit. + +Chat Session specific settings +* Needed so that one could + * setup a different ai model / engine as the external ai backend. + * interact with different independent ai models / engines / parallel instances in general +* Move needed configs from Me into a seperate Config class. + * also move ShowSettings, ShowInfo etal into Config class +* SimpleChat maintains an instance of Config class instead of Me. +* ToolsManager and the different tool call modules have been updated to + * have seperate init and setup calls. + * init is called at the begining + * setup will be called when ever a chat session is being created + and or in future when ever any config of interest changes. + * pick needed config etal from the specified chatId's config and not any global config. +* Starting flow updated to chain the different logical blocks of code + * first allow tools manager to be initd + * next create the needed default set of sessions, while parallely calling tool manager setup as needed. + * ensures that the available list of tool calls match the config of the chat session involved. + Needed as user could change tools related proxy server url. + * next setup the main ui as needed. +* Hide user-input area and tool call validate/trigger area when switching into settings and ensure they + get unhidden when returning back, as needed. +* Save and restore ChatSession config entries, as needed, in localStorage. + * load previously saved config if any, when creating ChatSession + * when ever switching, including into a, ChatSession, Configs of all chat sessions are saved. +* ALERT: If a chat session's tools proxyUrl is changed + * the same will be picked up immidiately wrt all subsequent tool calls which depend on the + tool call proxy server. + * however any resultant changes to the available tool calls list wont get reflected, + till one reloads the program. +* uirefresh helper ensures client side sliding window is always satisfied. + * now it remove messages no longer in the sliding window, so user only sees what is sent to the ai server, + in the chat session messages ui. + * avoids adding additional control specifically for ui, and instead stick to the ai nw handshake related + chat sliding window size (which takes care of try avoid overloading the ai model context size) selected + by user already. User can always change the sliding window size to view past messages beyond the currently + active sliding window size and then switch back again, if they want to. + + +## ToDo + +Is the tool call promise land trap deep enough, need to think through and explore around this once later. + +Add fetch_rss and may be different document formats processing related tool calling, in turn through +the simpleproxy.py if and where needed. + +* Using xmlfiltered and tagDropREs of + * ["^rss:channel:item:(?!title).+$"] one can fetch and extract out all the titles. + * ["^rss:channel:item:(?!title|link|description).+$"] one can fetch and extract out all the + titles along with corresponding links and descriptions + * rather with some minimal proding and guidance gpt-oss generated this to use xmlfiltered to read rss + +Add a option/button to reset the chat session config, to defaults. + +Have a seperate helper to show the user input area, based on set state. And have support for multiple images +if the models support same. It should also take care of some aspects of the tool call response edit / submit, +potentially. + +MAYBE add a special ClientSideOnly role for use wrt Chat history to maintain things to be shown in a chat +session to the end user, but inturn not to be sent to the ai server. Ex current settings, any edits to toolcall, +any tool call or server handshake errors seen (which user might have worked around as needed and continued the +conversation) or so ... + +Updating system prompt, will reset user input area fully now, which seems a good enough behaviour, while +keeping the code flow also simple and straight, do I need to change it, I dont think so as of now. + +For now amn't bringing in mozilla/github/standard-entities pdf, md, mathslatex etal javascript libraries for +their respective functionalities. + +Add support for base64 encoded pdf passing to ai models, when the models and llama engine gain that capability +in turn using openai file - file-data type sub block within content array or so ... diff --git a/tools/server/public_simplechat/docs/details.md b/tools/server/public_simplechat/docs/details.md index 8c475c1599..421264b75a 100644 --- a/tools/server/public_simplechat/docs/details.md +++ b/tools/server/public_simplechat/docs/details.md @@ -622,335 +622,6 @@ gets executed, before tool calling returns and thus data / error generated by th get incorporated in result sent to ai engine on the server side. -### Progress - -#### Done - -Tool Calling support added, along with a bunch of useful tool calls as well as a bundled simple proxy -if one wants to access web as part of tool call usage. - -Reasoning / thinking response from Ai Models is shown to the user, as they are being generated/shared. - -Chat Messages/Session and UI handling have been moved into corresponding Classes to an extent, this -helps ensure that -* switching chat sessions or loading a previous auto saved chat session will restore state including - ui such that end user can continue the chat session from where they left it, even if in the middle - of a tool call handshake. -* new fields added to http handshake in oneshot or streaming mode can be handled in a structured way - to an extent. - -Chat message parts seperated out and tagged to allow theming chat message as needed in future. -The default Chat UI theme/look changed to help differentiate between different messages in chat -history as well as the parts of each message in a slightly better manner. Change the theme slightly -between normal and print views (beyond previous infinite height) for better printed chat history. - -A builtin data store related tool calls, inturn built on browser's indexedDB, without needing any -proxy / additional helper to handle the store. One could use the ai assistant to store ones (ie end -users) own data or data of ai model. - -Trap http response errors and inform user the specific error returned by ai server. - -Initial go at a pdftext tool call. It allows web / local pdf files to be read and their text content -extracted and passed to ai model for further processing, as decided by ai and end user. One could -either work with the full pdf or a subset of adjacent pages. - -SimpleProxy updates -* Convert from a single monolithic file into a collection of modules. -* UrlValidator to cross check scheme and domain of requested urls, - the whitelist inturn picked from config json -* Helpers to fetch file from local file system or the web, transparently -* Help check for needed modules before a particular service path is acknowledged as available - through /aum service path -* urltext and related - logic to drop contents of specified tag with a given id - * allow its use for the web search tool flow - * setup wrt default duckduckgo search result urltext plain text cleanup and found working. - * this works properly only if the html being processed has proper opening and ending tags - around the area of interest. - * remember to specify non overlapping tag blocks, if more than one specified for dropping. - * this path not tested, but should logically work - -Settings/Config default changes - -* Chances are for ai models which dont support tool calling, things will be such that the tool calls -meta data shared will be silently ignored without much issue. So enabling tool calling feature by -default, so that in case one is using a ai model with tool calling the feature is readily available -for use. - -* Revert SlidingWindow ChatHistory in Context from last 10 to last 5 (rather 2 more then origianl, -given more context support in todays models) by default, given that now tool handshakes go through -the tools related side channel in the http handshake and arent morphed into normal user-assistant -channel of the handshake. - -* Enable CachePrompt api option given that tool calling based interactions could involve chat sessions -having ai responses built over multiple steps of tool callings etal. So independent of our client side -sliding window based drop off or even before they kick in, this can help in many cases. - -UI - add ClearChat button and logic. Also add unicode icons for same as well as for Settings. - -Renamed pdf_to_text to fetch_pdf_as_text so that ai model can understand the semantic better. - -sys_date_time tool call has been added. - -Refactor code and flow a bit wrt the client web ui -* Move the main chat related classes into its own js module file, independent of the main - runtime entry point (rather move out the runtime entry point into its own file). This allows - these classes to be referenced from other modules like tools related modules with full access - to these classes's details for developers and static check tools. -* building on same make the Tools management code into a ToolsManager class which is inturn - instantiated and the handle stored in top level Me class. This class also maintains and - manages the web workers as well as routing of the tool calling among others. -* add a common helper for posting results directly to the main thread side web worker callback - handlers. Inturn run the calling through a setTimeout0, so that delayed/missing response - situation rescuer timeout logic etal flow doesnt get messed for now. - -Track tool calling and inturn maintain pending tool calls so that only still valid tool call responses -will be accepted when the asynchronous tool call response is recieved. Also take care of clearing -pending tool call tracking in unhappy paths like when exception noticied as part of tool call execution, -or if there is no response within the configured timeout period. -NOTE: Currently the logic supports only 1 pending tool call per chat session. - -Add support for fetch_xml_as_text tool call, fix importmaps in index.html - -Renamed and updated logic wrt xml fetching to be fetch_xml_filtered. allow one to use re to identify -the tags to be filtered in a fine grained manner including filtering based on tag heirarchy -* avoid showing empty skipped tag blocks - -Logic which shows the generated tool call has been updated to trap errors when parsing the function call -arguments generated by the ai. This ensures that the chat ui itself doesnt get stuck in it. Instead now -the tool call response can inform the ai model that its function call had issues. - -Renamed fetch_web_url_text to fetch_html_text, so that gen ai model wont try to use this to fetch xml or -rss files, because it will return empty content, because there wont be any html content to strip the tags -and unwanted blocks before returning. - -Capture the body of ai server not ok responses, to help debug as well as to show same to user. - -Extract and include the outline of titles (along with calculated numbering) in the text output of pdftext -* ensure that one doesnt recurse beyond a predefined limit. - -Convert NSChatMessage from typedef to Class and update ChatMessageEx, SimpleChat, MultiChatUI classes to -make use of the same. -* helpers consolidated - * helpers to check if given instance contains reasoning or content or toolcall or tool response related - fields/info in them. - * helpers to get the corresponding field values - * some of these helpers where in ChatMessageEx and beyond before -* now the network handshaked fields are declared as undefined by default (instead of empty values). - this ensures that json stringify will automatically discard fields whose values are still undefined. -* add fields wrt tool response and update full flow to directly work with these fields instead of the - xml based serialisation which was previously used for maintaining the different tool response fields - within the content field (and inturn extract from there when sending to server). - * now a dataset based attribute is used to identify when input element contains user input and when - it contains tool call result/response. -* this simplifies the flow wrt showing chat message (also make it appear more cleanly) as well as - identifying not yet accepted tool result and showing in user query input field and related things. -* ALERT: ON-DISK-STORAGE structure of chat sessions have changed wrt tool responses. So old saves will - no longer work wrt tool responses - -UI updates -* update logic to allow empty tool results to be sent to ai engine server -* css - when user input textarea is in tool result mode (ie wrt TOOL.TEMP role), change the background - color to match the tool role chat message block color, so that user can easily know that the input - area is being used for submitting tool response or user response, at any given moment in time. - -Vision -* Add image_url field. Allow user to load image, which is inturn stored as a dataURL in image_url. -* when user presses submit with a message, if there is some content (image for now) in dataURL, - then initialise image_url field with same. -* when generating chat messages for ai server network handshake, create the mixed content type of - content field which includes both the text (from content field) and image (from image_url field) - ie if a image_url is found wrt a image. - * follow the openai format/template wrt these mixed content messages. -* Usage: specify a mmproj file directly or through -hf, additionally had to set --batch-size to 8k - and ubatch-size to 2k wrt gemma3-4b-it -* when showing chat instantiate img elements to show image_urls. - * limit horizontally to max width and vertically to 20% of the height -* show any image loaded by the user, in the corresponding image button -* consolidate dataurl handling into a bunch of helper functions. -* trap quota errors wrt localStorage etal -* dont forget to reset the file type input's value, so that reselecting the same image still - triggers the input's change event. - -SimpleChat class now allows extra fields to be specified while adding, in a generic way using a -object/literal object or equivalent. - -UI Cleanup - msgs spaced out, toolcall edit hr not always, scroll ui only when required, -hide settings/info till user requests, heading gradient - -iDB module -* add open, transact, put and get. Use for chat session save and load -* getKeys used to show Restore/Load button wrt chat sessions. - -ChatMessage -* assign a globally unique (ie across sessions) id to each chat message instance. -* add support for deleting chat message based on its uniquie id in SimpleChat. - * try ensure that adjacent messages remain on screen, after a message is deleted from session. -* add a popover div block in html, which acts as a popup menu containing buttons to work with - individual chat messages. - * experiment and finalise on anchor based relative positioning of the popover menu. - * have a del button, which allows one to delete the currently in focus chat message. - * have a copy button, which allows one to copy the textual content into system clipboard. - -MultiChatUI -* chat_show takes care of showing or clearing tool call edit / trigger as well as tool response - edit / submit. Also show the currently active tool call and its response before it is submitted - was previously only shown in the edit / trigger and edit / submit ui elements, now instead it - also shows as part of the chat session message blocks, so that user can delete or copy these - if needed using the same mechanism as other messages in the chat session. -* use a delete msg helper, which takes care of deleting the msg from chat session as well as - efficiently update ui to any extent by removing the corresponding element directly from existing - chat session ui without recreating the full chat session ui. -* a helper to add a message into specified chat session, as well as show/update in the chat session - ui by appending the chat message, instead of recreating the full chat session ui. -... - -MultiChatUI+ -* both chat_show and chat_uirefresh (if lastN >= 2) both take care of updating tool call edit/trigger - as well as the tool call response edit/submit related ui elements suitably. - * chat_show recreates currently active sliding window of chat session (which could even be full) - * while chat_uirefresh recreates/updates ui only for the lastN messages (prefer in general, as optimal) -* normal user response / query submit as well as tool call response or error submit have been updated - to use the optimal uirefresh logic now. - -Cleanup in general -* Inform end user when loading from a saved session. -* Update starting entry point flow to avoid calling chat_show twice indirectly, inturn leading to - two restore previously saved session blocks. Rather when adding tool calls support, and inturn - had to account for delayed collating of available simpleproxy based tool calls, I forgot to clean - this flow up. -* Make the sys_date_time template description bit more verbose, just in case. -* ui_userinput_reset now also resets associated Role always, inturn - * full on version from chat_show, inturn when session switching. - So user switchs session will reset all user input area and related data, while - also ensuring user input area has the right needed associated role setup. - * partial version from uirefresh, inturn adding user or tool call response messages. -* ui cleanup - * more rounded buttons, chat messages and input area elements. - * make the body very very lightly gray in color, while the user input area is made whiter. - * gradients wrt heading, role-specific individual chat message blocks. - * avoid borders and instead give a box effect through light shadows. - * also avoid allround border around chat message role block and instead have to only one side. - * timeout close popover menu. - * usage notes - * update wrt vision and toggling of sessions and system prompt through main title area. - * fix issue with sliding window size not reflecting properly in context window entry. - * make restore block into details based block, and anchor its position independent of db check. - * avoid unneeded outer overall scrollbar by adjusting fullbody height in screen mode. - * user css variable to define the overall background color and inturn use same to merge gradients - to the background, as well as to help switch the same seemlessly between screen and print modes. - * make the scrollbars more subtle and in the background. - * allow user input textarea to grow vertically to some extent. - * make things rounded across board by default. add some padding to toolcall details block, ... - * use icons without text wrt chat sessions++, new chat, clear chat and settings top level buttons. - * use title property/attribute to give a hint to the user about the button functionality. - * add role specific background gradients wrt the tool call trigger and user input block as well as - fix wrt the tool temp message block. also wrt system input block at top. - * also rename the TEMP role tags to use -TEMP instead of .TEMP, so that CSS rule selectors will - treat such tags like role-TOOL-TEMP as say a proper css class name rather than messing up with - something like role-TOOL.TEMP which will get split to role-TOOL and TEMP and inturn corresponding - css rule doesnt/wont get applied. - * given that now there is a proper visual cue based seperation of the tool call trigger block from - surrounding content, using proper seperate tool call specific coloring, so remove the
    horiz - line seperation wrt tool call trigger block. - * however retain the horizontal line seperation between the tool trigger block and user input block, - given that some users and some ai dont seem to identify the difference very easily. - * work around firefox currently not yet supporting anchor based relative positioning of popover. - * ensure the new uirefresh flow handles the following situations in a clean way like - * a new chat session clearing out usagenote+restore+currentconfig, as user starts chatting - * the settings ui getting cleared out as user starts/continues chatting directly into user input - without using chat session button to switch back to the chat. -* Auto ObjPropsEdit UI - * allow it to be themed by assigning id to top level block. - * fix a oversight (forgotten $) with use of templated literals and having variables in them. - * ensure full props hierarchy is accounted for when setting the id of elements. -* Chat button to toggle sessions buttons and system prompt. -* Use unix date format markers wrt sys_date_time toolcall, also add w (day of week). -* Free up the useful vertical space by merging chat sessions buttons/tabs into heading -* Allow user to load multiple images and submit to ai as part of a single user message. -* Use popover ui to allow user to view larger versions of loaded images as well as remove before submitting - to ai, if and when needed. -* Add external_ai toolcall with no access to internet or tool calls (helps avoid recursive ai tool calling). - User can see response generated by the external ai tool call, as and when it is recieved. -* Maintain chat session specific DivStream elements, and show live ai responses (through corresponding - DivStream) wrt the current chat session as well as any from the external ai tool call session. - In future, if the logic is updated to allow switching chat session ui in the middle of a pending tool call - or pending ai server response, things wont mess up ui, as they will be updating their respective DivStream. - Also switching sessions takes care of showing the right DivStream ie of currently switched to chat, so that - end user can see the streamed response from that chat session as it is occuring. -* Cleanup the tool call descriptions and verbose messages returned a bit. - -Chat Session specific settings -* Needed so that one could - * setup a different ai model / engine as the external ai backend. - * interact with different independent ai models / engines / parallel instances in general -* Move needed configs from Me into a seperate Config class. - * also move ShowSettings, ShowInfo etal into Config class -* SimpleChat maintains an instance of Config class instead of Me. -* ToolsManager and the different tool call modules have been updated to - * have seperate init and setup calls. - * init is called at the begining - * setup will be called when ever a chat session is being created - and or in future when ever any config of interest changes. - * pick needed config etal from the specified chatId's config and not any global config. -* Starting flow updated to chain the different logical blocks of code - * first allow tools manager to be initd - * next create the needed default set of sessions, while parallely calling tool manager setup as needed. - * ensures that the available list of tool calls match the config of the chat session involved. - Needed as user could change tools related proxy server url. - * next setup the main ui as needed. -* Hide user-input area and tool call validate/trigger area when switching into settings and ensure they - get unhidden when returning back, as needed. -* Save and restore ChatSession config entries, as needed, in localStorage. - * load previously saved config if any, when creating ChatSession - * when ever switching, including into a, ChatSession, Configs of all chat sessions are saved. -* ALERT: If a chat session's tools proxyUrl is changed - * the same will be picked up immidiately wrt all subsequent tool calls which depend on the - tool call proxy server. - * however any resultant changes to the available tool calls list wont get reflected, - till one reloads the program. -* uirefresh helper ensures client side sliding window is always satisfied. - * now it remove messages no longer in the sliding window, so user only sees what is sent to the ai server, - in the chat session messages ui. - * avoids adding additional control specifically for ui, and instead stick to the ai nw handshake related - chat sliding window size (which takes care of try avoid overloading the ai model context size) selected - by user already. User can always change the sliding window size to view past messages beyond the currently - active sliding window size and then switch back again, if they want to. - - -#### ToDo - -Is the tool call promise land trap deep enough, need to think through and explore around this once later. - -Add fetch_rss and may be different document formats processing related tool calling, in turn through -the simpleproxy.py if and where needed. - -* Using xmlfiltered and tagDropREs of - * ["^rss:channel:item:(?!title).+$"] one can fetch and extract out all the titles. - * ["^rss:channel:item:(?!title|link|description).+$"] one can fetch and extract out all the - titles along with corresponding links and descriptions - * rather with some minimal proding and guidance gpt-oss generated this to use xmlfiltered to read rss - -Add a option/button to reset the chat session config, to defaults. - -Have a seperate helper to show the user input area, based on set state. And have support for multiple images -if the models support same. It should also take care of some aspects of the tool call response edit / submit, -potentially. - -MAYBE add a special ClientSideOnly role for use wrt Chat history to maintain things to be shown in a chat -session to the end user, but inturn not to be sent to the ai server. Ex current settings, any edits to toolcall, -any tool call or server handshake errors seen (which user might have worked around as needed and continued the -conversation) or so ... - -Updating system prompt, will reset user input area fully now, which seems a good enough behaviour, while -keeping the code flow also simple and straight, do I need to change it, I dont think so as of now. - -For now amn't bringing in mozilla/github/standard-entities pdf, md, mathslatex etal javascript libraries for -their respective functionalities. - -Add support for base64 encoded pdf passing to ai models, when the models and llama engine gain that capability -in turn using openai file - file-data type sub block within content array or so ... - - ### Debuging the handshake and beyond When working with llama.cpp server based GenAi/LLM running locally, to look at the handshake directly From ec41001ba33f20066f9b381dfb42a9baa892a58e Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 24 Nov 2025 19:48:53 +0530 Subject: [PATCH 362/446] SimpleChatTCRV: Add simple readme in place of detailed one Gives quick overview of the features, given that the original readme (now docs/details.md++) got created over a period of disjoined time as features got added. --- .../public_simplechat/docs/changelog.md | 2 + tools/server/public_simplechat/readme.md | 180 ++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 tools/server/public_simplechat/readme.md diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 16d0a9dbc2..4456698c97 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -1,5 +1,7 @@ # Progress +by Humans for All. + Look into source files and git logs for the details, this is a partial changelog of stuff already done and some of the things that one may look at in the future. diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md new file mode 100644 index 0000000000..9e17fbb43f --- /dev/null +++ b/tools/server/public_simplechat/readme.md @@ -0,0 +1,180 @@ +# SimpleChat / AnveshikaSallap + +by Humans for All. + +## Quickstart + +### Server + +From the root directory of llama.cpp source code repo containing build / tools / ... sub directories + +Start ai engine / server using + +```bash +build/bin/llama-server -m \ + --path tools/server/public_simplechat --jinja +``` + +- `--jinja` enables tool‑calling support +- `--mmproj ` enables vision support +- `--port ` use if a custom port is needed + +If one needs web related access / tool calls dont forget to run + +```bash +cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config simpleproxy.json +``` + +- `--debug True` enables debug mode which captures internet handshake data. + +### Client + +1. Open `http://127.0.0.1:PORT/index.html` in a browser + +2. Select / Create a chat session + - set a suitable system prompt, if needed + - modify **settings**, if needed + - **Restore** loads last autosaved session with same name + +3. Enter the query, press **Enter** + - use **Shift‑Enter** for newline + - include images if required (ai vision models) + +4. View any streamed ai response (if enabled and supported) + +5. If a tool call is requested + - verify / edit the tool call details before triggering the same + - one can even ask ai to rethink on the tool call requested, + by sending a appropriate user response instead of a tool call response. + - tool call is executed using browser's web worker or simpleproxy + - tool call response is placed in user input area (with color coding) + - verify / edit the tool call response, before submit same back to ai + - tool response initially assigned `TOOL-TEMP` role, promoted to `TOOL` upon submit + - based on got response, if needed one can rerun tool call with modified arguments + - *at any time there can be one pending tool call wrt a chat session* + +6. **Delete / Copy** available via popover menu for each message + +7. **Clear** / **+ New** chat with provided buttons, as needed + + +## Overview + +A lightweight ai chat client with a web front-end that supports multiple chat sessions, vision, reasoning and tool calling. + +- Supports multiple independent chat sessions with + - One‑shot or streamed (default) responses + - Custom settings and system prompts per session + - Automatic local autosave (restorable on next load) + - can handshake with `/completions` or `/chat/completions` (default) endpoints + +- Supports peeking at model's reasoning live + - if model streams the same and + - streaming mode is enabled in settings (default) + +- Supports vision / image / multimodal ai models + - attach image files as part of user chat messages + - handshaked as `image_url`s in chat message content array along with text + - supports multi-image uploads per message + - images displayed inline in the chat history + - specify `mmproj` file via `-mmproj` or using `-hf` + - specify `-batch-size` and `-ubatch-size` if needed + +- Built-in support for GenAI models that expose tool calling + + - includes a bunch of useful builtin tool calls, without needing any additional setup + + - direct browser based tool calls include + - `sys_date_time`, `simple_calculator`, `run_javascript_function_code`, `data_store_*`, `external_ai` + - except for external_ai, these are run from within a web worker context to isolate main context from them + - data_store brings in browser IndexedDB based persistant key/value storage across sessions + + - along with included python based simpleproxy.py + - `search_web_text`, `fetch_web_url_raw`, `fetch_html_text`, `fetch_pdf_as_text`, `fetch_xml_filtered` + - these built‑in tool calls (via SimpleProxy) help fetch PDFs, HTML, XML or perform web search + - PDF tool also returns an outline with numbering + - result is truncated to `iResultMaxDataLength` (default 128 kB) + - helps isolate these functionality into a separate vm running locally or otherwise, if needed + - supports whitelisting of `allowed.schemes` and `allowed.domains` through `simpleproxy.json` + - supports a bearer token shared between server and client for auth + - needs https support, for better security wrt this flow, avoided now given mostly local use + + - follows a safety first design and lets the user + - verify and optionally edit the tool call requests, before executing the same + - verify and optionally edit the tool call response, before submitting the same + - user can update the settings for auto executing these actions, if needed + + - external_ai allows invoking a separate fresh ai instance + - ai could run self modified targeted versions of itself/... with custom system prompts and user messages as needed + - user can bring in an ai instance with additional compute access, which should be used only if needed + - tool calling is currently kept disabled in such a instance + +- Client side Sliding window Context control, using `iRecentUserMsgCnt`, helps limit context sent to ai model + +- Optional auto trimming of trailing garbage from model outputs + +- Follows responsive design to try adapt to any screen size + +- built using plain html + css + javascript and python + - no additional dependencies that one needs to keep track of + - except for pypdf, if pdf support needed. automaticaly drops pdf tool call if pypdf missing + - fits within ~260KB even in uncompressed source form (including simpleproxy.py) + - easily extend with additional tool calls using either javascript or python, for additional functionality + +Start exploring / experimenting with your favorite ai models and thier capabilities. + + +## Configuration / Settings + +One can modify the session configuration using Settings UI. All the settings and more are also exposed in the browser console via `document['gMe']`. + +### Settings Groups + +| Group | Purpose | +|---------|---------| +| `chatProps` | ApiEndpoint, streaming, sliding window, ... | +| `tools` | `enabled`, `proxyUrl`, `proxyAuthInsecure`, search URL/template & drop rules, max data length, timeouts | +| `apiRequestOptions` | `temperature`, `max_tokens`, `frequency_penalty`, `presence_penalty`, `cache_prompt`, ... | +| `headers` | `Content-Type`, `Authorization`, ... | + +### Some specific settings + +- **Ai Server** (`baseURL`) + - ai server (llama-server) address + - default is `http://127.0.0.1:PORT` +- **Stream** (`stream`) + - `true` for live streaming, `false` for oneshot +- **Client side Sliding Window** (`iRecentUserMsgCnt`) + - `-1` : send full history + - `0` : only system prompt + - `>0` : last N user messages after the most recent system prompt +- **Cache Prompt** (`cache_prompt`) + - enables server‑side caching of system prompt and history +- **Tool Call Timeout** (`toolCallResponseTimeoutMS`) + - 200s by default +- **Tool call Auto** (`autoSecs`) + - seconds to wait before auto-triggering tool calls and auto-submitting tool responses + - default is 0 ie manual +- **Trim Garbage** (`bTrimGarbage`) + - Removes repeated trailing text + + +## Debugging Tips + +- **Local TCPdump** + - `sudo tcpdump -i lo -s 0 -vvv -A host 127.0.0.1 and port 8080` +- **Browser DevTools** + - inspect `document['gMe']` for session state +- **Reset Tool Call** + - delete any assistant response after the tool call handshake + - next wrt the last tool message + - set role back to `TOOL-TEMP` + - edit the response as needed + - delete the same + - user will be given option to edit and retrigger the tool call + - submit the new response + + +## At the end + +A thank you to all open source and open model developers, who strive for the common good. From 143237a8d0644b2ca5b04aefd481a9861391cc24 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 24 Nov 2025 21:09:02 +0530 Subject: [PATCH 363/446] SimpleChatTCRV: Update/Cleanup the new readme --- tools/server/public_simplechat/readme.md | 59 ++++++++++++++---------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 9e17fbb43f..b789ab396f 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -2,6 +2,9 @@ by Humans for All. +A lightweight simple minded ai chat client with a web front-end that supports multiple chat sessions, vision, reasoning and tool calling. + + ## Quickstart ### Server @@ -18,6 +21,7 @@ build/bin/llama-server -m \ - `--jinja` enables tool‑calling support - `--mmproj ` enables vision support - `--port ` use if a custom port is needed + - default is 8080 wrt llama-server If one needs web related access / tool calls dont forget to run @@ -25,18 +29,20 @@ If one needs web related access / tool calls dont forget to run cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config simpleproxy.json ``` -- `--debug True` enables debug mode which captures internet handshake data. +- `--debug True` enables debug mode which captures internet handshake data +- port defaults to 3128, can be changed from simpleproxy.json, if needed ### Client -1. Open `http://127.0.0.1:PORT/index.html` in a browser +1. Open `http://127.0.0.1:8080/index.html` in a browser + - assuming one is running the llama-server locally with its default port 2. Select / Create a chat session - set a suitable system prompt, if needed - modify **settings**, if needed - **Restore** loads last autosaved session with same name -3. Enter the query, press **Enter** +3. Enter query/response into user input area at the bottom, press **Enter** - use **Shift‑Enter** for newline - include images if required (ai vision models) @@ -45,25 +51,26 @@ cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config 5. If a tool call is requested - verify / edit the tool call details before triggering the same - one can even ask ai to rethink on the tool call requested, - by sending a appropriate user response instead of a tool call response. - - tool call is executed using browser's web worker or simpleproxy - - tool call response is placed in user input area (with color coding) + by sending a appropriate user response instead of a tool call response + - tool call is executed using Browser's web worker or included SimpleProxy.py + - tool call response is placed in user input area + - the user input area is color coded to distinguish between user and tool responses - verify / edit the tool call response, before submit same back to ai - tool response initially assigned `TOOL-TEMP` role, promoted to `TOOL` upon submit - based on got response, if needed one can rerun tool call with modified arguments - - *at any time there can be one pending tool call wrt a chat session* + - at any time there can be one pending tool call wrt a chat session -6. **Delete / Copy** available via popover menu for each message +6. **Delete & Copy** available via popover menu for each message -7. **Clear** / **+ New** chat with provided buttons, as needed +7. **Clear / + New** chat with provided buttons, as needed ## Overview -A lightweight ai chat client with a web front-end that supports multiple chat sessions, vision, reasoning and tool calling. +A lightweight simple minded ai chat client with a web front-end that supports multiple chat sessions, vision, reasoning and tool calling. - Supports multiple independent chat sessions with - - One‑shot or streamed (default) responses + - One‑shot or Streamed (default) responses - Custom settings and system prompts per session - Automatic local autosave (restorable on next load) - can handshake with `/completions` or `/chat/completions` (default) endpoints @@ -75,29 +82,30 @@ A lightweight ai chat client with a web front-end that supports multiple chat se - Supports vision / image / multimodal ai models - attach image files as part of user chat messages - handshaked as `image_url`s in chat message content array along with text - - supports multi-image uploads per message + - supports multiple image uploads per message - images displayed inline in the chat history - specify `mmproj` file via `-mmproj` or using `-hf` - specify `-batch-size` and `-ubatch-size` if needed -- Built-in support for GenAI models that expose tool calling +- Built-in support for GenAI/LLM models that support tool calling - includes a bunch of useful builtin tool calls, without needing any additional setup - - direct browser based tool calls include + - building on modern browsers' flexibility, following tool calls are directly supported by default - `sys_date_time`, `simple_calculator`, `run_javascript_function_code`, `data_store_*`, `external_ai` - except for external_ai, these are run from within a web worker context to isolate main context from them - data_store brings in browser IndexedDB based persistant key/value storage across sessions - - along with included python based simpleproxy.py + - in collaboration with included python based simpleproxy.py, these additional tool calls are supported - `search_web_text`, `fetch_web_url_raw`, `fetch_html_text`, `fetch_pdf_as_text`, `fetch_xml_filtered` - these built‑in tool calls (via SimpleProxy) help fetch PDFs, HTML, XML or perform web search - - PDF tool also returns an outline with numbering + - PDF tool also returns an outline with numbering, if available - result is truncated to `iResultMaxDataLength` (default 128 kB) - - helps isolate these functionality into a separate vm running locally or otherwise, if needed + - helps isolate core of these functionality into a separate vm running locally or otherwise, if needed - supports whitelisting of `allowed.schemes` and `allowed.domains` through `simpleproxy.json` - supports a bearer token shared between server and client for auth - needs https support, for better security wrt this flow, avoided now given mostly local use + and need for user to setup corresponding pki key pairs. - follows a safety first design and lets the user - verify and optionally edit the tool call requests, before executing the same @@ -105,8 +113,8 @@ A lightweight ai chat client with a web front-end that supports multiple chat se - user can update the settings for auto executing these actions, if needed - external_ai allows invoking a separate fresh ai instance - - ai could run self modified targeted versions of itself/... with custom system prompts and user messages as needed - - user can bring in an ai instance with additional compute access, which should be used only if needed + - ai could run self modified targeted versions of itself/... using custom system prompts and user messages as needed + - user can setup an ai instance with additional compute access, which should be used only if needed - tool calling is currently kept disabled in such a instance - Client side Sliding window Context control, using `iRecentUserMsgCnt`, helps limit context sent to ai model @@ -116,8 +124,8 @@ A lightweight ai chat client with a web front-end that supports multiple chat se - Follows responsive design to try adapt to any screen size - built using plain html + css + javascript and python - - no additional dependencies that one needs to keep track of - - except for pypdf, if pdf support needed. automaticaly drops pdf tool call if pypdf missing + - no additional dependencies that one needs to worry about and inturn keep track of + - except for pypdf, if pdf support needed. automaticaly drops pdf tool call support, if pypdf missing - fits within ~260KB even in uncompressed source form (including simpleproxy.py) - easily extend with additional tool calls using either javascript or python, for additional functionality @@ -141,7 +149,10 @@ One can modify the session configuration using Settings UI. All the settings and - **Ai Server** (`baseURL`) - ai server (llama-server) address - - default is `http://127.0.0.1:PORT` + - default is `http://127.0.0.1:8080` +- **SimpleProxy Server** (`proxyUrl`) + - the simpleproxy.py server address + - default is `http://127.0.0.1:3128` - **Stream** (`stream`) - `true` for live streaming, `false` for oneshot - **Client side Sliding Window** (`iRecentUserMsgCnt`) @@ -149,14 +160,14 @@ One can modify the session configuration using Settings UI. All the settings and - `0` : only system prompt - `>0` : last N user messages after the most recent system prompt - **Cache Prompt** (`cache_prompt`) - - enables server‑side caching of system prompt and history + - enables server‑side caching of system prompt and history to an extent - **Tool Call Timeout** (`toolCallResponseTimeoutMS`) - 200s by default - **Tool call Auto** (`autoSecs`) - seconds to wait before auto-triggering tool calls and auto-submitting tool responses - default is 0 ie manual - **Trim Garbage** (`bTrimGarbage`) - - Removes repeated trailing text + - tries to remove repeating trailing text ## Debugging Tips From 1751ed18279c3d979819ad727c1cf0e5fdd65457 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 24 Nov 2025 21:45:42 +0530 Subject: [PATCH 364/446] SimpleChatTCRV:Cleanup importmap Remove the unneeded , wrt last entry Rather the import map isnt used currently, but certain entries kept there for future, more as a reminder. --- tools/server/public_simplechat/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/index.html b/tools/server/public_simplechat/index.html index 702b902fb2..217b025039 100644 --- a/tools/server/public_simplechat/index.html +++ b/tools/server/public_simplechat/index.html @@ -14,7 +14,7 @@ "simplechat": "./simplechat.js", "datautils": "./datautils.mjs", "ui": "./ui.mjs", - "toolsmanager": "./tools.mjs", + "toolsmanager": "./tools.mjs" } } From 073c570cad5ffcb54b9b94128a4793cc4b483c97 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 24 Nov 2025 22:41:19 +0530 Subject: [PATCH 365/446] SimpleChatTCRV:AiCallingAi ToolCall: flow cleanup and flexibility By default ensure external_ai tool call related special chat session starts with tool calls disabled and client side sliding window of 1. Add a helper in SimpleChat class to set these along with clearing of any chat history. Inturn now give the user flexibility to change this from within the program, if they need to for what ever reason, till the program restarts. --- .../public_simplechat/docs/changelog.md | 3 ++ .../server/public_simplechat/docs/details.md | 28 +++++++++------ tools/server/public_simplechat/main.js | 2 +- tools/server/public_simplechat/readme.md | 11 ++++-- tools/server/public_simplechat/simplechat.js | 35 +++++++++++++++++-- tools/server/public_simplechat/toolai.mjs | 5 +-- 6 files changed, 66 insertions(+), 18 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 4456698c97..963d65b908 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -296,6 +296,9 @@ Chat Session specific settings chat sliding window size (which takes care of try avoid overloading the ai model context size) selected by user already. User can always change the sliding window size to view past messages beyond the currently active sliding window size and then switch back again, if they want to. +* More flexibility to user wrt ExternalAi tool call ie ai calling ai + * the user can change the default behaviour of tools being disabled and sliding window of 1 + * program restart will reset these back to the default ## ToDo diff --git a/tools/server/public_simplechat/docs/details.md b/tools/server/public_simplechat/docs/details.md index 421264b75a..5e29e5a0c5 100644 --- a/tools/server/public_simplechat/docs/details.md +++ b/tools/server/public_simplechat/docs/details.md @@ -254,16 +254,18 @@ It is attached to the document object. Some of these can also be updated using t * if a very long text is being generated, which leads to no user interaction for sometime and inturn the machine goes into power saving mode or so, the platform may stop network connection, leading to exception. - * iRecentUserMsgCnt - a simple minded SlidingWindow to limit context window load at Ai Model end. This is set to 5 by default. So in addition to latest system message, last/latest iRecentUserMsgCnt user messages after the latest system prompt and its responses from the ai model will be sent to the ai-model, when querying for a new response. Note that if enabled, only user messages after the latest system message/prompt will be considered. + * iRecentUserMsgCnt - a simple minded ClientSide SlidingWindow logic to limit context window load at Ai Model end. This is set to 5 by default. So in addition to latest system message, last/latest iRecentUserMsgCnt user messages (after the latest system prompt) and its responses from the ai model along with any associated tool calls will be sent to the ai-model, when querying for a new response. Note that if enabled, only user messages after the latest system message/prompt will be considered. This specified sliding window user message count also includes the latest user query. * less than 0 : Send entire chat history to server - * 0 : Send only the system message if any to the server + * 0 : Send only the system message if any to the server. Even the latest user message wont be sent. * greater than 0 : Send the latest chat history from the latest system prompt, limited to specified cnt. + * NOTE: the latest user message (query/response/...) for which we need a ai response, will also be counted as belonging to the iRecentUserMsgCnt. + * bCompletionFreshChatAlways - whether Completion mode collates complete/sliding-window history when communicating with the server or only sends the latest user query/message. * bCompletionInsertStandardRolePrefix - whether Completion mode inserts role related prefix wrt the messages that get inserted into prompt field wrt /Completion endpoint. @@ -467,16 +469,22 @@ The following tools/functions are currently provided by default * data_store_get/set/delete/list - allows for a basic data store to be used, to maintain data and or context across sessions and so... -* external_ai - allows ai to use an independent session of itself / different instance of ai, +* external_ai - allows ai to use an independent fresh by default session of itself / different ai, with a custom system prompt of ai's choosing and similarly user message of ai's choosing, in order to get any job it deems necessary to be done in a uncluttered indepedent session. - * helps ai to process stuff that it needs, without having to worry about any previous chat history - etal messing with the current data's context and processing. + * in its default configuration, helps ai to process stuff that it needs, without having to worry + about any previous chat history etal messing with the current data's context and processing. * helps ai to process stuff with targeted system prompts of its choosing, for the job at hand. - * tool calling is disabled wrt the external_ai's independent session, for now. - * it was noticed that else even the external_ai may call into more external_ai calls trying to - find answer to the same question. maybe one can enable tool calling, while explicitly disabling - external_ai tool call from within external_ai tool call or so later... + * by default + * tool calling is disabled wrt the external_ai's independent session. + * it was noticed that else even external_ai may call into more external_ai calls trying to + find answers to the same question/situation. + * maybe one can enable tool calling, while explicitly disabling of external_ai tool call + from within external_ai tool call related session or so later... + * client side sliding window size is set to 1 so that only system prompt and ai set user message + gets handshaked with the external_ai instance + * End user can change this behaviour by changing the corresponding settings of the TCExternalAi + special chat session, which is internally used for this tool call. * Could be used by ai for example to * summarise a large text content, where it could use the context of the text to generate a suitable system prompt for summarising things suitably @@ -486,7 +494,7 @@ The following tools/functions are currently provided by default * given the fuzzy nature of the generative ai, sometimes the model may even use this tool call to get answer to questions like what is your name ;> * end user can use this mechanism to try and bring in an instance of ai running on a more powerful - machine, but then to be used only if needed or so + machine with more compute and memory capabiliteis, but then to be used only if needed or so Most of the above (except for external ai call) are run from inside web worker contexts. Currently the ai generated code / expression is run through a simple minded eval inside a web worker mechanism. Use diff --git a/tools/server/public_simplechat/main.js b/tools/server/public_simplechat/main.js index 6e0efa03ff..d60fc91034 100644 --- a/tools/server/public_simplechat/main.js +++ b/tools/server/public_simplechat/main.js @@ -32,7 +32,7 @@ function startme() { sL.push(gMe.multiChat.new_chat_session(cid)); } await Promise.allSettled(sL) - gMe.multiChat.simpleChats[mChatMagic.AI_TC_SESSIONNAME].cfg.tools.enabled = false + gMe.multiChat.simpleChats[mChatMagic.AI_TC_SESSIONNAME].default_isolating() gMe.multiChat.setup_ui(gMe.defaultChatIds[0]); gMe.multiChat.show_sessions(); gMe.multiChat.handle_session_switch(gMe.multiChat.curChatId) diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index b789ab396f..9763b64f30 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -112,10 +112,17 @@ A lightweight simple minded ai chat client with a web front-end that supports mu - verify and optionally edit the tool call response, before submitting the same - user can update the settings for auto executing these actions, if needed - - external_ai allows invoking a separate fresh ai instance + - external_ai allows invoking a separate optionally fresh by default ai instance - ai could run self modified targeted versions of itself/... using custom system prompts and user messages as needed - user can setup an ai instance with additional compute access, which should be used only if needed - - tool calling is currently kept disabled in such a instance + - by default in such a instance + - tool calling is kept disabled along with + - client side sliding window of 1, + ie only system prompt and latest user message is sent to ai server. + - TCExternalAI is the special chat session used internally for this, + and the default behaviour will get impacted if you modify the settings of this special chat session. + - Restarting this chat client logic will force reset things to the default behaviour, + how ever any other settings wrt TCExternalAi, that where changed, will persist across restarts. - Client side Sliding window Context control, using `iRecentUserMsgCnt`, helps limit context sent to ai model diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 3abc6bd27d..0f3df0e742 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -486,9 +486,10 @@ function usage_note(sRecentUserMsgCnt) {
    • ChatHistInCtxt, MaxTokens, ModelCtxt window to expand
    -
  • ${AI_TC_SESSIONNAME} session keeps tool calls disabled, to avoid recursive...
  • +
  • ${AI_TC_SESSIONNAME} session used for external_ai tool call, ie ai calling ai
    • -
    • Used by external_ai tool call, which allows ai calling ai, as needed.
    • +
    • by default keeps tool calls disabled, client side sliding window of 1
    • +
    • if you change for some reason, you may want to change back to these
    `; @@ -572,6 +573,18 @@ export class SimpleChat { this.latestResponse = new ChatMessageEx(); } + /** + * A relatively isolating default setup + * * clear any chat history + * * disable tool calls + * * set client side sliding window to 1 so that only system prompt is sent along with latest user message + */ + default_isolating() { + this.clear() + this.cfg.tools.enabled = false + this.cfg.chatProps.iRecentUserMsgCnt = 1 + } + setup() { return this.toolsMgr.setup(this.chatId) } @@ -669,6 +682,7 @@ export class SimpleChat { * * Else Return chat messages from latest going back till the last/latest system prompt. * While keeping track that the number of user queries/messages doesnt exceed iRecentUserMsgCnt. + * * @param {number} iRecentUserMsgCnt */ recent_chat(iRecentUserMsgCnt) { @@ -2074,6 +2088,13 @@ export class Config { this.chatProps = { apiEP: ApiEP.Type.Chat, stream: true, + /** + * How many recent user msgs to consider and include along with their corresponding + * assistant responses and tool calls if any, wrt client side sliding window logic. + * * user specified System prompt is outside this count. + * * the latest user query/response to send to ai server is part of this. + * * only user messages following the latest system prompt is considered. + */ iRecentUserMsgCnt: 5, bCompletionFreshChatAlways: true, bCompletionInsertStandardRolePrefix: false, @@ -2201,7 +2222,15 @@ export class Me { this.dataURLs = [] this.houseKeeping = { clear: true, - } + }; + /** + * Control if externalai toolcall related special chat session starts in a forced isolating state + * * always external_ai tool call is made + * * Or does it start in such a state only at the time of program loading and inturn + * if user has the flexibility to change this characteristic till this program is restarted, + * for what ever reason they may deem fit. + */ + this.tcexternalaiForceIsolatingDefaultsAlways = false; } /** diff --git a/tools/server/public_simplechat/toolai.mjs b/tools/server/public_simplechat/toolai.mjs index 412624757b..02f1bc0e5a 100644 --- a/tools/server/public_simplechat/toolai.mjs +++ b/tools/server/public_simplechat/toolai.mjs @@ -92,10 +92,11 @@ let externalai_meta = { */ function externalai_run(chatid, toolcallid, toolname, obj) { let sc = gMe.multiChat.simpleChats[mChatMagic.AI_TC_SESSIONNAME]; - sc.clear() + if (gMe.tcexternalaiForceIsolatingDefaultsAlways) { + sc.default_isolating() + } sc.add_system_anytime(obj['system_prompt'], 'TC:ExternalAI') sc.add(new mChatMagic.ChatMessageEx(new mChatMagic.NSChatMessage(mChatMagic.Roles.User, obj['user_message']))) - sc.cfg.tools.enabled = false sc.handle_chat_hs(sc.cfg.baseURL, mChatMagic.ApiEP.Type.Chat, gMe.multiChat.elDivStreams).then((resp)=>{ gMe.toolsMgr.workers_postmessage_for_main(gMe.toolsMgr.workers.js, chatid, toolcallid, toolname, resp.content_equiv()); }).catch((err)=>{ From 3cd2e3fd902cc3bb775c688173c876ad11d6e57f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 25 Nov 2025 14:44:42 +0530 Subject: [PATCH 366/446] SimpleChatTCRV:UI:Cleanup: Have common div+label+el logic Avoid seperate duplicated logic for creating the div+label+el based element --- tools/server/public_simplechat/simplechat.js | 8 +-- tools/server/public_simplechat/ui.mjs | 72 ++++---------------- 2 files changed, 17 insertions(+), 63 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 0f3df0e742..29924ad639 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -2178,16 +2178,16 @@ export class Config { } }, [":chatProps:apiEP", ":chatProps:iRecentUserMsgCnt"], (propWithPath, prop, elParent)=>{ if (propWithPath == ":chatProps:apiEP") { - let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.chatProps.apiEP, (val)=>{ + let sel = ui.el_create_divlabelel("ApiEndPoint", ui.el_create_select("SetApiEP", ApiEP.Type, this.chatProps.apiEP, (val)=>{ // @ts-ignore this.chatProps.apiEP = ApiEP.Type[val]; - }); + })); elParent.appendChild(sel.div); } if (propWithPath == ":chatProps:iRecentUserMsgCnt") { - let sel = ui.el_creatediv_select("SetChatHistoryInCtxt", "ChatHistoryInCtxt", this.sRecentUserMsgCntDict, this.chatProps.iRecentUserMsgCnt, (val)=>{ + let sel = ui.el_create_divlabelel("ChatHistoryInCtxt", ui.el_create_select("SetChatHistoryInCtxt", this.sRecentUserMsgCntDict, this.chatProps.iRecentUserMsgCnt, (val)=>{ this.chatProps.iRecentUserMsgCnt = this.sRecentUserMsgCntDict[val]; - }); + })); elParent.appendChild(sel.div); } }) diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index fac63e489e..2f542cb03f 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -113,28 +113,6 @@ export function el_create_boolbutton(id, texts, defaultValue, cb) { return el; } -/** - * Create a div wrapped button which represents bool value using specified text wrt true and false. - * @param {string} id - * @param {string} label - * @param {{ true: string; false: string; }} texts - * @param {boolean} defaultValue - * @param {(arg0: boolean) => void} cb - * @param {string} className - */ -export function el_creatediv_boolbutton(id, label, texts, defaultValue, cb, className="gridx2") { - let div = document.createElement("div"); - div.className = className; - let lbl = document.createElement("label"); - lbl.setAttribute("for", id); - lbl.innerText = label; - div.appendChild(lbl); - let btn = el_create_boolbutton(id, texts, defaultValue, cb); - div.appendChild(btn); - return { div: div, el: btn }; -} - - /** * Create a select ui element, with a set of options to select from. * * options: an object which contains name-value pairs @@ -171,29 +149,6 @@ export function el_create_select(id, options, defaultOption, cb) { return el; } -/** - * Create a div wrapped select ui element, with a set of options to select from. - * - * @param {string} id - * @param {any} label - * @param {{ [x: string]: any; }} options - * @param {any} defaultOption - * @param {(arg0: string) => void} cb - * @param {string} className - */ -export function el_creatediv_select(id, label, options, defaultOption, cb, className="gridx2") { - let div = document.createElement("div"); - div.className = className; - let lbl = document.createElement("label"); - lbl.setAttribute("for", id); - lbl.innerText = label; - div.appendChild(lbl); - let sel = el_create_select(id, options,defaultOption, cb); - div.appendChild(sel); - return { div: div, el: sel }; -} - - /** * Create a input ui element. * @@ -215,28 +170,27 @@ export function el_create_input(id, type, defaultValue, cb) { return el; } + /** - * Create a div wrapped input. + * Create a div wrapped labeled instance of the passed el. * - * @param {string} id + * @template {HTMLElement | HTMLInputElement} T * @param {string} label - * @param {string} type - * @param {any} defaultValue - * @param {function(any):void} cb + * @param {T} el * @param {string} className */ -export function el_creatediv_input(id, label, type, defaultValue, cb, className="gridx2") { +export function el_create_divlabelel(label, el, className="gridx2") { let div = document.createElement("div"); div.className = className; let lbl = document.createElement("label"); - lbl.setAttribute("for", id); + lbl.setAttribute("for", el.id); lbl.innerText = label; div.appendChild(lbl); - let el = el_create_input(id, type, defaultValue, cb); div.appendChild(el); return { div: div, el: el }; } + /** * Create a div wrapped input of type file, * which hides input and shows a button which chains to underlying file type input. @@ -249,8 +203,8 @@ export function el_creatediv_input(id, label, type, defaultValue, cb, className= * @param {string} className */ export function el_creatediv_inputfilebtn(id, label, labelBtnHtml, defaultValue, acceptable, cb, className) { - let elX = el_creatediv_input(id, label, "file", defaultValue, cb, className) - elX.el.hidden = true + let elX = el_create_divlabelel(label, el_create_input(id, "file", defaultValue, cb), className) + elX.el.hidden = true; elX.el.accept = acceptable let idB = `${id}-button` let elB = el_create_button(idB, (mev) => { @@ -334,20 +288,20 @@ export function ui_show_obj_props_edit(elParent, propsTreeRoot, oObj, lProps, sL let type = typeof(val); let id = `Set${propsTreeRootNew.replaceAll(':','-')}` if (((type == "string") || (type == "number"))) { - let inp = el_creatediv_input(`${id}`, k, typeDict[type], oObj[k], (val)=>{ + let inp = el_create_divlabelel(k, el_create_input(`${id}`, typeDict[type], oObj[k], (val)=>{ if (type == "number") { val = Number(val); } oObj[k] = val; - }); + })); if (fRefiner) { fRefiner(k, inp.el) } elFS.appendChild(inp.div); } else if (type == "boolean") { - let bbtn = el_creatediv_boolbutton(`${id}`, k, {true: "true", false: "false"}, val, (userVal)=>{ + let bbtn = el_create_divlabelel(k, el_create_boolbutton(`${id}`, {true: "true", false: "false"}, val, (userVal)=>{ oObj[k] = userVal; - }); + })); if (fRefiner) { fRefiner(k, bbtn.el) } From c5eb783ec13b367ec27ba15f86cda4ac44c46d5e Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 25 Nov 2025 18:28:05 +0530 Subject: [PATCH 367/446] SimpleChatTCRV:Ui:Cleanup: Extended Type annotations So there is slightly better typecheck and less extra code. --- tools/server/public_simplechat/ui.mjs | 55 +++++++++++---------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 2f542cb03f..92984e4beb 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -4,27 +4,6 @@ // -/** - * Insert key-value pairs into passed element object. - * @param {HTMLElement} el - * @param {string} key - * @param {any} value - */ -function el_set(el, key, value) { - // @ts-ignore - el[key] = value -} - -/** - * Retrieve the value corresponding to given key from passed element object. - * @param {HTMLElement} el - * @param {string} key - */ -function el_get(el, key) { - // @ts-ignore - return el[key] -} - /** * Set the class of the children, based on whether it is the idSelected or not. * @param {HTMLDivElement} elBase @@ -88,31 +67,41 @@ export function el_create_append_p(text, elParent=undefined, id=undefined) { return para; } + +/** @typedef {{true: string, false: string}} BoolToAnyString */ + +/** @typedef {HTMLButtonElement & {xbool: boolean, xtexts: BoolToAnyString}} HTMLBoolButtonElement */ + /** * Create a button which represents bool value using specified text wrt true and false. * When ever user clicks the button, it will toggle the value and update the shown text. * * @param {string} id - * @param {{true: string, false: string}} texts + * @param {BoolToAnyString} texts * @param {boolean} defaultValue * @param {function(boolean):void} cb */ export function el_create_boolbutton(id, texts, defaultValue, cb) { - let el = document.createElement("button"); - el_set(el, "xbool", defaultValue) - el_set(el, "xtexts", structuredClone(texts)) - el.innerText = el_get(el, "xtexts")[String(defaultValue)]; + let el = /** @type {HTMLBoolButtonElement} */(document.createElement("button")); + el.xbool = defaultValue + el.xtexts = structuredClone(texts) + el.innerText = el.xtexts[`${defaultValue}`]; if (id) { el.id = id; } el.addEventListener('click', (ev)=>{ - el_set(el, "xbool", !el_get(el, "xbool")); - el.innerText = el_get(el, "xtexts")[String(el_get(el, "xbool"))]; - cb(el_get(el, "xbool")); + el.xbool = !el.xbool + el.innerText = el.xtexts[`${el.xbool}`]; + cb(el.xbool); }) return el; } + +/** @typedef {Object} XSelectOptions */ + +/** @typedef {HTMLSelectElement & {xselected: *, xoptions: XSelectOptions}} HTMLXSelectElement */ + /** * Create a select ui element, with a set of options to select from. * * options: an object which contains name-value pairs @@ -120,14 +109,14 @@ export function el_create_boolbutton(id, texts, defaultValue, cb) { * * cb : the call back returns the name string of the option selected. * * @param {string} id - * @param {Object} options + * @param {XSelectOptions} options * @param {*} defaultOption * @param {function(string):void} cb */ export function el_create_select(id, options, defaultOption, cb) { - let el = document.createElement("select"); - el_set(el, "xselected", defaultOption); - el_set(el, "xoptions", structuredClone(options)); + let el = /** @type{HTMLXSelectElement} */(document.createElement("select")); + el.xselected = defaultOption + el.xoptions = structuredClone(options) for(let cur of Object.keys(options)) { let op = document.createElement("option"); op.value = cur; From d3f1a398d805fdeb7974de44d856c1a0a5cc95de Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 25 Nov 2025 20:05:40 +0530 Subject: [PATCH 368/446] SimpleChatTCRV:Markdown:Initial skeleton Try identify headings, and blocks in markdown and convert them into equivalent stuff in html Show the same in the chat message blocks. --- tools/server/public_simplechat/simplechat.js | 11 +++- tools/server/public_simplechat/typemd.mjs | 58 ++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tools/server/public_simplechat/typemd.mjs diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 29924ad639..72008b936a 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -10,6 +10,7 @@ import * as du from "./datautils.mjs"; import * as ui from "./ui.mjs" import * as mTools from "./tools.mjs" import * as mIdb from "./idb.mjs" +import * as mMD from "./typemd.mjs" const TEMP_MARKER = "-TEMP" @@ -1496,7 +1497,15 @@ class MultiChatUI { } for (const [name, content] of showList) { if (content.length > 0) { - entry = ui.el_create_append_p(`${content}`, secContents); + if (name == "content") { + entry = document.createElement('div') + let md = new mMD.MarkDown() + md.process(content) + entry.innerHTML = md.html + secContents.appendChild(entry) + } else { + entry = ui.el_create_append_p(`${content}`, secContents); + } entry.classList.add(`chat-message-${name}`) } } diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs new file mode 100644 index 0000000000..a1d3eab57e --- /dev/null +++ b/tools/server/public_simplechat/typemd.mjs @@ -0,0 +1,58 @@ +//@ts-check +// Helpers to handle markdown content +// by Humans for All +// + + +export class MarkDown { + + constructor() { + this.in = { + pre: false, + table: false, + } + this.md = "" + this.html = "" + } + + /** + * Process a line from markdown content + * @param {string} line + */ + process_line(line) { + let lineA = line.split(' ') + let lineRStripped = line.trimStart() + if (this.in.pre) { + if (lineA[0] == '```') { + this.in.pre = false + this.html += "\n" + } else { + this.html += line + } + return + } + if (line.startsWith ("#")) { + let hLevel = lineA[0].length + this.html += `${lineRStripped}\n` + return + } + if (lineA[0] == '```') { + this.in.pre = true + this.html += "
    \n"
    +            return
    +        }
    +        this.html += `

    ${line}

    ` + } + + /** + * Process a bunch of lines in markdown format. + * @param {string} lines + */ + process(lines) { + let linesA = lines.split('\n') + for(const line of linesA) { + this.process_line(line) + } + } + +} From c9ddb90aae441cf071d19e47a66019d194d3cdf0 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 25 Nov 2025 20:55:46 +0530 Subject: [PATCH 369/446] SimpleChatTCRV:MarkDown:Headings, Pre initial cleanup Remove markdown heading markers Fix pre equivalent blocks of markdown given that they can have the block type following ``` marker Remember to add line break at end of line wrt pre block. --- tools/server/public_simplechat/typemd.mjs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index a1d3eab57e..fe67b9e7f8 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -27,18 +27,19 @@ export class MarkDown { this.in.pre = false this.html += "
    \n" } else { - this.html += line + this.html += `${line}\n` } return } if (line.startsWith ("#")) { let hLevel = lineA[0].length - this.html += `${lineRStripped}\n` + this.html += `${line.slice(hLevel)}\n` return } - if (lineA[0] == '```') { + let matchPre = line.match(/^```([a-zA-Z0-9]*)(.*)/); + if ( matchPre != null) { this.in.pre = true - this.html += "
    \n"
    +            this.html += `
    \n`
                 return
             }
             this.html += `

    ${line}

    ` From be528bc34f845c15f2777dfb71a686c4c192561c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 25 Nov 2025 21:42:00 +0530 Subject: [PATCH 370/446] SimpleChatTCRV:Markdown:Unordered list initial go --- tools/server/public_simplechat/typemd.mjs | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index fe67b9e7f8..a3740bcaea 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -10,6 +10,8 @@ export class MarkDown { this.in = { pre: false, table: false, + /** @type {Array} */ + listUnordered: [] } this.md = "" this.html = "" @@ -42,6 +44,39 @@ export class MarkDown { this.html += `
    \n`
                 return
             }
    +        let matchUnOrdered = line.match(/^([ ]*)[-*][ ](.*)$/);
    +        if ( matchUnOrdered != null) {
    +            let sList = 'none'
    +            let listLvl = 0
    +            if (this.in.listUnordered.length == 0) {
    +                sList = 'next'
    +                this.in.listUnordered.push(matchUnOrdered[1].length)
    +                listLvl = this.in.listUnordered.length // ie 1
    +            } else {
    +                if (this.in.listUnordered[this.in.listUnordered.length-1] < matchUnOrdered[1].length){
    +                    sList = 'next'
    +                    this.in.listUnordered.push(matchUnOrdered[1].length)
    +                    listLvl = this.in.listUnordered.length
    +                } else if (this.in.listUnordered[this.in.listUnordered.length-1] == matchUnOrdered[1].length){
    +                    sList = 'same'
    +                } else {
    +                    sList = 'same'
    +                    while (this.in.listUnordered[this.in.listUnordered.length-1] > matchUnOrdered[1].length) {
    +                        this.in.listUnordered.pop()
    +                        this.html += `\n`
    +                        if (this.in.listUnordered.length == 0) {
    +                            break
    +                        }
    +                    }
    +                }
    +            }
    +            if (sList == 'same') {
    +                this.html += `
  • ${line}
  • \n` + } else if (sList == 'next') { + this.html += `
      \n
    • ${line}
    • \n` + } + return + } this.html += `

      ${line}

      ` } From c420f165049b5a9cea9bf2fe92dc8c929f9adcb7 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 25 Nov 2025 21:55:39 +0530 Subject: [PATCH 371/446] SimpleChatTCRV:MarkDown:Cleanup Unordered list initial go Ensure '---' is treated as a horizontal line and doesnt mess with unordered list handling. Take care of unwinding the unordered list everywhere it is needed. --- tools/server/public_simplechat/typemd.mjs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index a3740bcaea..ca448b8fb8 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -1,5 +1,5 @@ //@ts-check -// Helpers to handle markdown content +// simple minded helpers to handle markdown content // by Humans for All // @@ -17,13 +17,23 @@ export class MarkDown { this.html = "" } + unwind_list_unordered() { + for(const i in this.in.listUnordered) { + this.html += "
    \n" + } + this.in.listUnordered.length = 0 + } + + unwind_list() { + this.unwind_list_unordered() + } + /** * Process a line from markdown content * @param {string} line */ process_line(line) { let lineA = line.split(' ') - let lineRStripped = line.trimStart() if (this.in.pre) { if (lineA[0] == '```') { this.in.pre = false @@ -33,13 +43,20 @@ export class MarkDown { } return } + if (line == '---') { + this.unwind_list() + this.html += "
    \n" + return + } if (line.startsWith ("#")) { + this.unwind_list() let hLevel = lineA[0].length this.html += `${line.slice(hLevel)}\n` return } let matchPre = line.match(/^```([a-zA-Z0-9]*)(.*)/); if ( matchPre != null) { + this.unwind_list() this.in.pre = true this.html += `
    \n`
                 return
    @@ -77,6 +94,7 @@ export class MarkDown {
                 }
                 return
             }
    +        this.unwind_list()
             this.html += `

    ${line}

    ` } From 781f86fee805de58535f94ae29ea95a59afcbcee Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 25 Nov 2025 22:05:47 +0530 Subject: [PATCH 372/446] SimpleChatTCRV:Markdown: Remove unordered list marker also make flow simple by using same logic for putting the list content. --- tools/server/public_simplechat/typemd.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index ca448b8fb8..a52281811b 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -66,14 +66,16 @@ export class MarkDown { let sList = 'none' let listLvl = 0 if (this.in.listUnordered.length == 0) { - sList = 'next' + sList = 'same' this.in.listUnordered.push(matchUnOrdered[1].length) listLvl = this.in.listUnordered.length // ie 1 + this.html += "
      \n" } else { if (this.in.listUnordered[this.in.listUnordered.length-1] < matchUnOrdered[1].length){ - sList = 'next' + sList = 'same' this.in.listUnordered.push(matchUnOrdered[1].length) listLvl = this.in.listUnordered.length + this.html += "
        \n" } else if (this.in.listUnordered[this.in.listUnordered.length-1] == matchUnOrdered[1].length){ sList = 'same' } else { @@ -88,9 +90,7 @@ export class MarkDown { } } if (sList == 'same') { - this.html += `
      • ${line}
      • \n` - } else if (sList == 'next') { - this.html += `
          \n
        • ${line}
        • \n` + this.html += `
        • ${matchUnOrdered[2]}
        • \n` } return } From 924bb6cb47981ad70c654b98b58fb716e32b71f1 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 25 Nov 2025 23:02:35 +0530 Subject: [PATCH 373/446] SimpleChatTCRV:MarkDown:HorizLine and Unordered list Allow for other valid char based markers wrt horizontal lines and unordered lists ?Also allow for spaces after horizontal line marker, in same line? --- tools/server/public_simplechat/typemd.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index a52281811b..458dbc34ae 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -43,7 +43,7 @@ export class MarkDown { } return } - if (line == '---') { + if (line.match(/^[-]{3,}|[*]{3,}|[_]{3,}\s*$/) != null) { this.unwind_list() this.html += "
          \n" return @@ -61,7 +61,7 @@ export class MarkDown { this.html += `
          \n`
                       return
                   }
          -        let matchUnOrdered = line.match(/^([ ]*)[-*][ ](.*)$/);
          +        let matchUnOrdered = line.match(/^([ ]*)[-+*][ ](.*)$/);
                   if ( matchUnOrdered != null) {
                       let sList = 'none'
                       let listLvl = 0
          
          From 95c8cd6eba74881b8bada89c6bcbe9d6080ab4a3 Mon Sep 17 00:00:00 2001
          From: hanishkvc 
          Date: Tue, 25 Nov 2025 23:24:37 +0530
          Subject: [PATCH 374/446] SimpleChatTCRV:MarkDown: Better Fenced Pre
          
          Allow fenced code block / pre to be demarkated using either ```
          or ~~~
          
          Ensure the termination line wrt fenced block doesnt contain anything
          else.
          
          Same starting marker needs to be present wrt ending also
          ---
           tools/server/public_simplechat/typemd.mjs | 16 ++++++++--------
           1 file changed, 8 insertions(+), 8 deletions(-)
          
          diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs
          index 458dbc34ae..71aed12775 100644
          --- a/tools/server/public_simplechat/typemd.mjs
          +++ b/tools/server/public_simplechat/typemd.mjs
          @@ -8,7 +8,7 @@ export class MarkDown {
           
               constructor() {
                   this.in = {
          -            pre: false,
          +            preFenced: "",
                       table: false,
                       /** @type {Array} */
                       listUnordered: []
          @@ -34,9 +34,9 @@ export class MarkDown {
                */
               process_line(line) {
                   let lineA = line.split(' ')
          -        if (this.in.pre) {
          -            if (lineA[0] == '```') {
          -                this.in.pre = false
          +        if (this.in.preFenced.length > 0) {
          +            if (line == this.in.preFenced) {
          +                this.in.preFenced = ""
                           this.html += "
          \n" } else { this.html += `${line}\n` @@ -54,11 +54,11 @@ export class MarkDown { this.html += `${line.slice(hLevel)}\n` return } - let matchPre = line.match(/^```([a-zA-Z0-9]*)(.*)/); - if ( matchPre != null) { + let matchPreFenced = line.match(/^(```|~~~)([a-zA-Z0-9]*)(.*)/); + if ( matchPreFenced != null) { this.unwind_list() - this.in.pre = true - this.html += `
          \n`
          +            this.in.preFenced = matchPreFenced[1]
          +            this.html += `
          \n`
                       return
                   }
                   let matchUnOrdered = line.match(/^([ ]*)[-+*][ ](.*)$/);
          
          From 03220d4a2b333a9d3db1b5ccef1ddd8f5a299dac Mon Sep 17 00:00:00 2001
          From: hanishkvc 
          Date: Tue, 25 Nov 2025 23:43:21 +0530
          Subject: [PATCH 375/446] SimpleChatTCRV:Markdown: Allow fixed spaces b4 fenced
           pre marker
          
          ---
           tools/server/public_simplechat/docs/changelog.md | 1 +
           tools/server/public_simplechat/typemd.mjs        | 7 ++++++-
           2 files changed, 7 insertions(+), 1 deletion(-)
          
          diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md
          index 963d65b908..4680eefd5c 100644
          --- a/tools/server/public_simplechat/docs/changelog.md
          +++ b/tools/server/public_simplechat/docs/changelog.md
          @@ -299,6 +299,7 @@ Chat Session specific settings
           * More flexibility to user wrt ExternalAi tool call ie ai calling ai
             * the user can change the default behaviour of tools being disabled and sliding window of 1
             * program restart will reset these back to the default
          +* A simple minded basic Markdown to Html logic
           
           
           ## ToDo
          diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs
          index 71aed12775..0f40603865 100644
          --- a/tools/server/public_simplechat/typemd.mjs
          +++ b/tools/server/public_simplechat/typemd.mjs
          @@ -43,6 +43,8 @@ export class MarkDown {
                       }
                       return
                   }
          +        // 3 or more of --- or ___ or *** followed by space
          +        // some online notes seemed to indicate spaces at end, so accepting same
                   if (line.match(/^[-]{3,}|[*]{3,}|[_]{3,}\s*$/) != null) {
                       this.unwind_list()
                       this.html += "
          \n" @@ -54,13 +56,16 @@ export class MarkDown { this.html += `${line.slice(hLevel)}\n` return } - let matchPreFenced = line.match(/^(```|~~~)([a-zA-Z0-9]*)(.*)/); + // same number of space followed by ``` or ~~~ + // some samples with spaces at beginning seen, so accepting spaces at begin + let matchPreFenced = line.match(/^(\s*```|\s*~~~)([a-zA-Z0-9]*)(.*)/); if ( matchPreFenced != null) { this.unwind_list() this.in.preFenced = matchPreFenced[1] this.html += `
          \n`
                       return
                   }
          +        // spaces followed by - or + or * followed by a space and actual list item
                   let matchUnOrdered = line.match(/^([ ]*)[-+*][ ](.*)$/);
                   if ( matchUnOrdered != null) {
                       let sList = 'none'
          
          From 3cc5bd01ae286a7a90d7eb4918683a7842b6cac1 Mon Sep 17 00:00:00 2001
          From: hanishkvc 
          Date: Wed, 26 Nov 2025 00:08:49 +0530
          Subject: [PATCH 376/446] SimpleChatTCRV:MarkDown:Tables initial skeleton
          
          Try create a table head
          ---
           tools/server/public_simplechat/typemd.mjs | 24 ++++++++++++++++++++++-
           1 file changed, 23 insertions(+), 1 deletion(-)
          
          diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs
          index 0f40603865..a06032d16f 100644
          --- a/tools/server/public_simplechat/typemd.mjs
          +++ b/tools/server/public_simplechat/typemd.mjs
          @@ -9,7 +9,7 @@ export class MarkDown {
               constructor() {
                   this.in = {
                       preFenced: "",
          -            table: false,
          +            table: 0,
                       /** @type {Array} */
                       listUnordered: []
                   }
          @@ -28,6 +28,28 @@ export class MarkDown {
                   this.unwind_list_unordered()
               }
           
          +    /**
          +     * @param {string} line
          +     */
          +    process_table_line(line) {
          +        let lineParts = line.match(/^(|.*)*|$/)
          +        if (lineParts != null) {
          +            if (this.in.table == 0) {
          +                // table heading
          +                this.html += "\n\n"
          +                for(let i=1; i${lineParts[i]}\n`
          +                }
          +                this.html += "\n"
          +                this.in.table = lineParts.length-1;
          +                return
          +            }
          +            if (this.in.table > 0) {
          +
          +            }
          +        }
          +    }
          +
               /**
                * Process a line from markdown content
                * @param {string} line
          
          From 2f85d444281a95a458c08e5eefdf16ce26e478e2 Mon Sep 17 00:00:00 2001
          From: hanishkvc 
          Date: Wed, 26 Nov 2025 01:01:04 +0530
          Subject: [PATCH 377/446] SimpleChatTCRV:MarkDown:Tables initial go
          
          Rather this wont work, need to refresh on regex, been too long.
          
          Rather using split should be simpler
          
          However the extraction of head and body parts with seperation
          inbetween for transition should work
          
          Rather the seperation is blindly assumed and corresponding line
          discarded for now
          ---
           tools/server/public_simplechat/typemd.mjs | 44 +++++++++++++++++++----
           1 file changed, 37 insertions(+), 7 deletions(-)
          
          diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs
          index a06032d16f..f1cc075294 100644
          --- a/tools/server/public_simplechat/typemd.mjs
          +++ b/tools/server/public_simplechat/typemd.mjs
          @@ -9,7 +9,10 @@ export class MarkDown {
               constructor() {
                   this.in = {
                       preFenced: "",
          -            table: 0,
          +            table: {
          +                columns: 0,
          +                rawRow: 0,
          +            },
                       /** @type {Array} */
                       listUnordered: []
                   }
          @@ -32,21 +35,45 @@ export class MarkDown {
                * @param {string} line
                */
               process_table_line(line) {
          -        let lineParts = line.match(/^(|.*)*|$/)
          +        //let lineParts = line.match(/^([|].*?)+?[|]$/)
          +        let lineParts = line.match(/^[|](\s*[^|]*\s*[|])+$/)
                   if (lineParts != null) {
          -            if (this.in.table == 0) {
          +            if (this.in.table.columns == 0) {
                           // table heading
                           this.html += "
          \n\n" for(let i=1; i${lineParts[i]}\n` } this.html += "\n" - this.in.table = lineParts.length-1; - return + this.in.table.columns = lineParts.length-1; + this.in.table.rawRow = 0 + return true } - if (this.in.table > 0) { - + if (this.in.table.columns > 0) { + if (this.in.table.columns != lineParts.length-1) { + console.log("DBUG:TypeMD:Table:NonHead columns mismatch") + } + this.in.table.rawRow += 1 + if (this.in.table.rawRow == 1) { + // skip the table head vs body seperator + // rather skipping blindly without even checking if seperator or not. + this.html += "\n" + return true + } + this.html += "\n" + for(let i=1; i${lineParts[i]}\n` + } + this.html += "\n" + return true } + console.warn("DBUG:TypeMD:Table:Thrisanku???") + } else { + if (this.in.table.columns > 0) { + this.html += "\n" + this.html += "
          \n" + } + return false } } @@ -65,6 +92,9 @@ export class MarkDown { } return } + if (this.process_table_line(line)) { + return + } // 3 or more of --- or ___ or *** followed by space // some online notes seemed to indicate spaces at end, so accepting same if (line.match(/^[-]{3,}|[*]{3,}|[_]{3,}\s*$/) != null) { From edf4c6588dad92fc10b78d59d94fe34c2950123c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 01:19:18 +0530 Subject: [PATCH 378/446] SimpleChatTCRV:MarkDown:Table cleanup initial go Switch to the simpler split based flow. Include tr wrt the table head block also. Add a css entry to try and have header cell contents text aling to left for now, given that there is no border or color shaded or so distinguishing characteristics wrt the table cells for now. --- tools/server/public_simplechat/simplechat.css | 3 ++ tools/server/public_simplechat/typemd.mjs | 33 ++++++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.css b/tools/server/public_simplechat/simplechat.css index 4789666a3e..9b46a30c76 100644 --- a/tools/server/public_simplechat/simplechat.css +++ b/tools/server/public_simplechat/simplechat.css @@ -125,6 +125,9 @@ body { word-break: break-word; hyphens: auto; } +.chat-message-content thead { + text-align: left; +} .chat-message-content-live { overflow-wrap: break-word; word-break: break-word; diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index f1cc075294..4020d91e21 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -32,25 +32,35 @@ export class MarkDown { } /** + * Try extract a table from markdown content, + * one line at a time. + * This is a imperfect logic, but should give a rough semblance of a table many a times. * @param {string} line */ process_table_line(line) { - //let lineParts = line.match(/^([|].*?)+?[|]$/) - let lineParts = line.match(/^[|](\s*[^|]*\s*[|])+$/) - if (lineParts != null) { + if (!line.startsWith("|")) { + if (this.in.table.columns > 0) { + this.html += "\n" + this.html += "\n" + this.in.table.columns = 0 + } + return false + } + let lineA = line.split('|') + if (lineA.length > 2) { if (this.in.table.columns == 0) { // table heading - this.html += "\n\n" - for(let i=1; i${lineParts[i]}\n` + this.html += "
          \n\n\n" + for(let i=1; i${lineA[i]}\n` } - this.html += "\n" - this.in.table.columns = lineParts.length-1; + this.html += "\n\n" + this.in.table.columns = lineA.length-2; this.in.table.rawRow = 0 return true } if (this.in.table.columns > 0) { - if (this.in.table.columns != lineParts.length-1) { + if (this.in.table.columns != lineA.length-2) { console.log("DBUG:TypeMD:Table:NonHead columns mismatch") } this.in.table.rawRow += 1 @@ -61,8 +71,8 @@ export class MarkDown { return true } this.html += "\n" - for(let i=1; i${lineParts[i]}\n` + for(let i=1; i${lineA[i]}\n` } this.html += "\n" return true @@ -72,6 +82,7 @@ export class MarkDown { if (this.in.table.columns > 0) { this.html += "\n" this.html += "
          \n" + this.in.table.columns = 0 } return false } From 654e234a4ec9eea4de4074ec5e189bd2a2b44a4e Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 01:36:54 +0530 Subject: [PATCH 379/446] SimpleChatTCRV:Markdown:User configurable per session User can enable or disable the simple minded bruteforce markdown parsing from the per session settings. Add grey shading and align text to left wrt table headings of markdown to html converted tables. --- tools/server/public_simplechat/docs/changelog.md | 1 + tools/server/public_simplechat/readme.md | 4 +++- tools/server/public_simplechat/simplechat.css | 5 ++++- tools/server/public_simplechat/simplechat.js | 3 ++- tools/server/public_simplechat/typemd.mjs | 13 +++++++++++-- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 4680eefd5c..017e975abb 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -299,6 +299,7 @@ Chat Session specific settings * More flexibility to user wrt ExternalAi tool call ie ai calling ai * the user can change the default behaviour of tools being disabled and sliding window of 1 * program restart will reset these back to the default +* Ui module cleanup to avoid duplicated/unneeded boiler plates, including using updated jsdoc annotations * A simple minded basic Markdown to Html logic diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 9763b64f30..8f624d7191 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -126,7 +126,9 @@ A lightweight simple minded ai chat client with a web front-end that supports mu - Client side Sliding window Context control, using `iRecentUserMsgCnt`, helps limit context sent to ai model -- Optional auto trimming of trailing garbage from model outputs +- Optional + - simple minded markdown parsing of chat message text contents (default) + - auto trimming of trailing garbage from model outputs - Follows responsive design to try adapt to any screen size diff --git a/tools/server/public_simplechat/simplechat.css b/tools/server/public_simplechat/simplechat.css index 9b46a30c76..ef1237fee1 100644 --- a/tools/server/public_simplechat/simplechat.css +++ b/tools/server/public_simplechat/simplechat.css @@ -125,8 +125,11 @@ body { word-break: break-word; hyphens: auto; } -.chat-message-content thead { +.chat-message-content thead th { text-align: left; + background-color: lightgray; + /* padding-inline: 1vmin; */ + border-radius: 0.4vmin; } .chat-message-content-live { overflow-wrap: break-word; diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 72008b936a..5a16d62d1f 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1497,7 +1497,7 @@ class MultiChatUI { } for (const [name, content] of showList) { if (content.length > 0) { - if (name == "content") { + if ((name == "content") && (this.simpleChats[chatId].cfg.chatProps.bMarkdown)) { entry = document.createElement('div') let md = new mMD.MarkDown() md.process(content) @@ -2105,6 +2105,7 @@ export class Config { * * only user messages following the latest system prompt is considered. */ iRecentUserMsgCnt: 5, + bMarkdown: true, bCompletionFreshChatAlways: true, bCompletionInsertStandardRolePrefix: false, bTrimGarbage: true, diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 4020d91e21..100ff04519 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -4,6 +4,15 @@ // +/** + * A simple minded Markdown to Html convertor, which tries to support + * basic forms of the below in a simple, stupid and some cases in a semi rigid way. + * * headings + * * fenced code blocks / pres + * * unordered list + * * tables + * * horizontal line + */ export class MarkDown { constructor() { @@ -32,9 +41,9 @@ export class MarkDown { } /** - * Try extract a table from markdown content, - * one line at a time. + * Try extract a table from markdown content, one line at a time. * This is a imperfect logic, but should give a rough semblance of a table many a times. + * Purposefully allows for any text beyond table row end | marker to be shown. * @param {string} line */ process_table_line(line) { From a1dc72ba6629b3ca4304ea52f71e1ef4e6d61cd8 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 03:45:34 +0530 Subject: [PATCH 380/446] SimpleChatTCRV:MarkDown:Cleanup overall initial go Save copy of data being processed. Try and sanitize the data passed for markdown to html conversion, so that if there are any special characters wrt html in the passed markdown content, it gets translated into a harmless text. This also ensures that those text dont disappear, bcas of browser trying to interpret them as html tagged content. Trap any errors during sanitizing and or processing of the lines in general and push them into a errors array. Callers of this markdown class can decide whether to use the converted html or not based on errors being empty or not or ... Move the processing of unordered list into a function of its own. Rather the ordered list can also use the same flow in general except for some tiny changes including wrt the regex, potentially. --- tools/server/public_simplechat/typemd.mjs | 91 ++++++++++++++--------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 100ff04519..3d2f90f0ca 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -25,7 +25,11 @@ export class MarkDown { /** @type {Array} */ listUnordered: [] } - this.md = "" + /** + * @type {Array<*>} + */ + this.errors = [] + this.raw = "" this.html = "" } @@ -40,6 +44,48 @@ export class MarkDown { this.unwind_list_unordered() } + /** + * Process a unordered list one line at a time + * @param {string} line + */ + process_list_unordered(line) { + // spaces followed by - or + or * followed by a space and actual list item + let matchUnOrdered = line.match(/^([ ]*)[-+*][ ](.*)$/); + if (matchUnOrdered != null) { + let sList = 'none' + let listLvl = 0 + if (this.in.listUnordered.length == 0) { + sList = 'same' + this.in.listUnordered.push(matchUnOrdered[1].length) + listLvl = this.in.listUnordered.length // ie 1 + this.html += "
            \n" + } else { + if (this.in.listUnordered[this.in.listUnordered.length-1] < matchUnOrdered[1].length){ + sList = 'same' + this.in.listUnordered.push(matchUnOrdered[1].length) + listLvl = this.in.listUnordered.length + this.html += "
              \n" + } else if (this.in.listUnordered[this.in.listUnordered.length-1] == matchUnOrdered[1].length){ + sList = 'same' + } else { + sList = 'same' + while (this.in.listUnordered[this.in.listUnordered.length-1] > matchUnOrdered[1].length) { + this.in.listUnordered.pop() + this.html += `
            \n` + if (this.in.listUnordered.length == 0) { + break + } + } + } + } + if (sList == 'same') { + this.html += `
          • ${matchUnOrdered[2]}
          • \n` + } + return true + } + return false + } + /** * Try extract a table from markdown content, one line at a time. * This is a imperfect logic, but should give a rough semblance of a table many a times. @@ -102,6 +148,9 @@ export class MarkDown { * @param {string} line */ process_line(line) { + let elSanitize = document.createElement('div') + elSanitize.textContent = line + line = elSanitize.innerHTML let lineA = line.split(' ') if (this.in.preFenced.length > 0) { if (line == this.in.preFenced) { @@ -137,38 +186,7 @@ export class MarkDown { this.html += `
            \n`
                         return
                     }
            -        // spaces followed by - or + or * followed by a space and actual list item
            -        let matchUnOrdered = line.match(/^([ ]*)[-+*][ ](.*)$/);
            -        if ( matchUnOrdered != null) {
            -            let sList = 'none'
            -            let listLvl = 0
            -            if (this.in.listUnordered.length == 0) {
            -                sList = 'same'
            -                this.in.listUnordered.push(matchUnOrdered[1].length)
            -                listLvl = this.in.listUnordered.length // ie 1
            -                this.html += "
              \n" - } else { - if (this.in.listUnordered[this.in.listUnordered.length-1] < matchUnOrdered[1].length){ - sList = 'same' - this.in.listUnordered.push(matchUnOrdered[1].length) - listLvl = this.in.listUnordered.length - this.html += "
                \n" - } else if (this.in.listUnordered[this.in.listUnordered.length-1] == matchUnOrdered[1].length){ - sList = 'same' - } else { - sList = 'same' - while (this.in.listUnordered[this.in.listUnordered.length-1] > matchUnOrdered[1].length) { - this.in.listUnordered.pop() - this.html += `
              \n` - if (this.in.listUnordered.length == 0) { - break - } - } - } - } - if (sList == 'same') { - this.html += `
            • ${matchUnOrdered[2]}
            • \n` - } + if (this.process_list_unordered(line)) { return } this.unwind_list() @@ -180,9 +198,14 @@ export class MarkDown { * @param {string} lines */ process(lines) { + this.raw = lines let linesA = lines.split('\n') for(const line of linesA) { - this.process_line(line) + try { + this.process_line(line) + } catch (err) { + this.errors.push(err) + } } } From 9453a81b955e87b3ce3e06b672402ddffe4f6a62 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 11:50:17 +0530 Subject: [PATCH 381/446] SimpleChatTCRV:Markdown:Ordered and Unordered Update regex to match both ordered and unordered list --- tools/server/public_simplechat/typemd.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 3d2f90f0ca..40a2a2b359 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -50,7 +50,7 @@ export class MarkDown { */ process_list_unordered(line) { // spaces followed by - or + or * followed by a space and actual list item - let matchUnOrdered = line.match(/^([ ]*)[-+*][ ](.*)$/); + let matchUnOrdered = line.match(/^([ ]*)([-+*]|[a-zA-Z0-9]\.)[ ](.*)$/); if (matchUnOrdered != null) { let sList = 'none' let listLvl = 0 @@ -79,7 +79,7 @@ export class MarkDown { } } if (sList == 'same') { - this.html += `
            • ${matchUnOrdered[2]}
            • \n` + this.html += `
            • ${matchUnOrdered[3]}
            • \n` } return true } From 908ca170fa6c1ce6c9e84d830f17783ba16abc63 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 11:57:10 +0530 Subject: [PATCH 382/446] SimpleChatTCRV:Markdown:OrderedUnOrdered: list.offsets --- tools/server/public_simplechat/typemd.mjs | 30 ++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 40a2a2b359..ed57b64d18 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -22,8 +22,10 @@ export class MarkDown { columns: 0, rawRow: 0, }, - /** @type {Array} */ - listUnordered: [] + list: { + /** @type {Array} */ + offsets: [], + } } /** * @type {Array<*>} @@ -34,10 +36,10 @@ export class MarkDown { } unwind_list_unordered() { - for(const i in this.in.listUnordered) { + for(const i in this.in.list.offsets) { this.html += "
            \n" } - this.in.listUnordered.length = 0 + this.in.list.offsets.length = 0 } unwind_list() { @@ -54,25 +56,25 @@ export class MarkDown { if (matchUnOrdered != null) { let sList = 'none' let listLvl = 0 - if (this.in.listUnordered.length == 0) { + if (this.in.list.offsets.length == 0) { sList = 'same' - this.in.listUnordered.push(matchUnOrdered[1].length) - listLvl = this.in.listUnordered.length // ie 1 + this.in.list.offsets.push(matchUnOrdered[1].length) + listLvl = this.in.list.offsets.length // ie 1 this.html += "
              \n" } else { - if (this.in.listUnordered[this.in.listUnordered.length-1] < matchUnOrdered[1].length){ + if (this.in.list.offsets[this.in.list.offsets.length-1] < matchUnOrdered[1].length){ sList = 'same' - this.in.listUnordered.push(matchUnOrdered[1].length) - listLvl = this.in.listUnordered.length + this.in.list.offsets.push(matchUnOrdered[1].length) + listLvl = this.in.list.offsets.length this.html += "
                \n" - } else if (this.in.listUnordered[this.in.listUnordered.length-1] == matchUnOrdered[1].length){ + } else if (this.in.list.offsets[this.in.list.offsets.length-1] == matchUnOrdered[1].length){ sList = 'same' } else { sList = 'same' - while (this.in.listUnordered[this.in.listUnordered.length-1] > matchUnOrdered[1].length) { - this.in.listUnordered.pop() + while (this.in.list.offsets[this.in.list.offsets.length-1] > matchUnOrdered[1].length) { + this.in.list.offsets.pop() this.html += `
              \n` - if (this.in.listUnordered.length == 0) { + if (this.in.list.offsets.length == 0) { break } } From e7b8fc139245f74cd3a92352ccee83f435701e4a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 12:03:09 +0530 Subject: [PATCH 383/446] SimpleChatTCRV:Markdown:OrderedUnOrdered: rename to semantic --- tools/server/public_simplechat/typemd.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index ed57b64d18..a7f6017568 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -56,22 +56,24 @@ export class MarkDown { if (matchUnOrdered != null) { let sList = 'none' let listLvl = 0 + let curOffset = matchUnOrdered[1].length if (this.in.list.offsets.length == 0) { sList = 'same' - this.in.list.offsets.push(matchUnOrdered[1].length) + this.in.list.offsets.push(curOffset) listLvl = this.in.list.offsets.length // ie 1 this.html += "
                \n" } else { - if (this.in.list.offsets[this.in.list.offsets.length-1] < matchUnOrdered[1].length){ + let lastOffset = this.in.list.offsets[this.in.list.offsets.length-1]; + if (lastOffset < curOffset){ sList = 'same' - this.in.list.offsets.push(matchUnOrdered[1].length) + this.in.list.offsets.push(curOffset) listLvl = this.in.list.offsets.length this.html += "
                  \n" - } else if (this.in.list.offsets[this.in.list.offsets.length-1] == matchUnOrdered[1].length){ + } else if (lastOffset == curOffset){ sList = 'same' } else { sList = 'same' - while (this.in.list.offsets[this.in.list.offsets.length-1] > matchUnOrdered[1].length) { + while (lastOffset > curOffset) { this.in.list.offsets.pop() this.html += `
                \n` if (this.in.list.offsets.length == 0) { From 9b75e494be545d18428d90821938afe19878e3a5 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 12:10:54 +0530 Subject: [PATCH 384/446] SimpleChatTCRV:Markdown:OrderedUnOrderd: Simplify flow Avoid seperate new list level logic for a fresh list and list with in list paths. Rather adjust lastOffset specifically for fresh list. All paths lead to need to insert list item and the difference to be handled wrt starting or ending a list level is handled by respective condition check blocks directly without delaying it for later so no need for that sList state, so remove. Avoid check for same level list item path, as nothing special needs to be do in that path currently. Live identify the last offset, when unwinding. NOTE: Logic currently will handle ordered lists on their own or unordered lists on thier own or intermixed list containing both type of lists within them, however remember that all will be shown as unordered lists. ALERT: if there is a really long line, the logic currently doesnt support it being broken into smaller line with same or greater offset than the line identifying the current list item. --- tools/server/public_simplechat/typemd.mjs | 39 +++++++++-------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index a7f6017568..05ba522ff9 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -54,37 +54,28 @@ export class MarkDown { // spaces followed by - or + or * followed by a space and actual list item let matchUnOrdered = line.match(/^([ ]*)([-+*]|[a-zA-Z0-9]\.)[ ](.*)$/); if (matchUnOrdered != null) { - let sList = 'none' let listLvl = 0 let curOffset = matchUnOrdered[1].length - if (this.in.list.offsets.length == 0) { - sList = 'same' + let lastOffset = this.in.list.offsets[this.in.list.offsets.length-1]; + if (lastOffset == undefined) { + lastOffset = -1 + } + + if (lastOffset < curOffset){ this.in.list.offsets.push(curOffset) - listLvl = this.in.list.offsets.length // ie 1 + listLvl = this.in.list.offsets.length this.html += "
                  \n" - } else { - let lastOffset = this.in.list.offsets[this.in.list.offsets.length-1]; - if (lastOffset < curOffset){ - sList = 'same' - this.in.list.offsets.push(curOffset) - listLvl = this.in.list.offsets.length - this.html += "
                    \n" - } else if (lastOffset == curOffset){ - sList = 'same' - } else { - sList = 'same' - while (lastOffset > curOffset) { - this.in.list.offsets.pop() - this.html += `
                  \n` - if (this.in.list.offsets.length == 0) { - break - } + } else if (lastOffset > curOffset){ + while (this.in.list.offsets[this.in.list.offsets.length-1] > curOffset) { + this.in.list.offsets.pop() + this.html += `
                \n` + if (this.in.list.offsets.length == 0) { + break } } } - if (sList == 'same') { - this.html += `
              • ${matchUnOrdered[3]}
              • \n` - } + + this.html += `
              • ${matchUnOrdered[3]}
              • \n` return true } return false From 27887528809530e28e373587ff1d9999a6f0ee50 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 12:36:13 +0530 Subject: [PATCH 385/446] SimpleChatTCRV:Markdown:OrdUnOrded: EndType tracker initial go --- tools/server/public_simplechat/typemd.mjs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 05ba522ff9..7964223c2c 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -25,6 +25,8 @@ export class MarkDown { list: { /** @type {Array} */ offsets: [], + /** @type {Array} */ + endType: [], } } /** @@ -36,8 +38,12 @@ export class MarkDown { } unwind_list_unordered() { - for(const i in this.in.list.offsets) { - this.html += "
              \n" + while (true) { + let popped = this.in.list.endType.pop() + if (popped == undefined) { + break + } + this.html += popped } this.in.list.offsets.length = 0 } @@ -65,10 +71,12 @@ export class MarkDown { this.in.list.offsets.push(curOffset) listLvl = this.in.list.offsets.length this.html += "
                \n" + this.in.list.endType.push("
              \n") } else if (lastOffset > curOffset){ while (this.in.list.offsets[this.in.list.offsets.length-1] > curOffset) { this.in.list.offsets.pop() - this.html += `
            \n` + let popped = this.in.list.endType.pop() + this.html += popped; if (this.in.list.offsets.length == 0) { break } From b4be1cb4b8b9958288b15fcea0089c4d4aed2b18 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 13:09:43 +0530 Subject: [PATCH 386/446] SimpleChatTCRV:Markdown: Ordered Ya Unordered Start ordered or unordered list as the case may be and push the same into endType for matching unwinding. Ignore empty lines and dont force a list unwind. --- tools/server/public_simplechat/typemd.mjs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 7964223c2c..8a30d90742 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -70,8 +70,13 @@ export class MarkDown { if (lastOffset < curOffset){ this.in.list.offsets.push(curOffset) listLvl = this.in.list.offsets.length - this.html += "
              \n" - this.in.list.endType.push("
            \n") + if (matchUnOrdered[2][matchUnOrdered[2].length-1] == '.') { + this.html += "
              \n" + this.in.list.endType.push("
            \n") + } else { + this.html += "
              \n" + this.in.list.endType.push("
            \n") + } } else if (lastOffset > curOffset){ while (this.in.list.offsets[this.in.list.offsets.length-1] > curOffset) { this.in.list.offsets.pop() @@ -192,7 +197,9 @@ export class MarkDown { if (this.process_list_unordered(line)) { return } - this.unwind_list() + if (line.trim().length > 0) { + this.unwind_list() + } this.html += `

            ${line}

            ` } From 226dc793b1bafc35fd7c1340c9ecb6deae5a8935 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 14:45:22 +0530 Subject: [PATCH 387/446] SimpleChatTCRV:Markdown:Lists - handle split lines to an extent If a split line is found which remains within the constraints of the preceding list item, then dont unwind the list, rather for now add the split line as a new item at the same level. --- tools/server/public_simplechat/typemd.mjs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 8a30d90742..48694c6c80 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -87,9 +87,24 @@ export class MarkDown { } } } - this.html += `
          • ${matchUnOrdered[3]}
          • \n` return true + } else { + if (this.in.list.offsets.length > 0) { + if (line.trim().length == 0) { + return true + } + let matchOffset = line.match(/^([ ]*)(.*)$/); + if (matchOffset == null) { + return false + } + let lastOffset = this.in.list.offsets[this.in.list.offsets.length-1]; + if (matchOffset[1].length < lastOffset) { + return false + } + this.html += `
          • ${matchOffset[2]}
          • \n` + return true + } } return false } @@ -197,9 +212,7 @@ export class MarkDown { if (this.process_list_unordered(line)) { return } - if (line.trim().length > 0) { - this.unwind_list() - } + this.unwind_list() this.html += `

            ${line}

            ` } From 1129ab5a6dc5095779f7af3af05028a89234ccf0 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 14:54:03 +0530 Subject: [PATCH 388/446] SimpleChatTCRV:Markdown:Basic List (ordered/unordered) handling Rename from unordered to just list, given that the logic handles both types of lists at a basic level now. --- tools/server/public_simplechat/typemd.mjs | 26 +++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 48694c6c80..1380835c35 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -27,6 +27,9 @@ export class MarkDown { offsets: [], /** @type {Array} */ endType: [], + }, + /** @type {Object} */ + empty: { } } /** @@ -37,7 +40,7 @@ export class MarkDown { this.html = "" } - unwind_list_unordered() { + unwind_list() { while (true) { let popped = this.in.list.endType.pop() if (popped == undefined) { @@ -48,29 +51,24 @@ export class MarkDown { this.in.list.offsets.length = 0 } - unwind_list() { - this.unwind_list_unordered() - } - /** - * Process a unordered list one line at a time + * Process list one line at a time * @param {string} line */ - process_list_unordered(line) { + process_list(line) { // spaces followed by - or + or * followed by a space and actual list item - let matchUnOrdered = line.match(/^([ ]*)([-+*]|[a-zA-Z0-9]\.)[ ](.*)$/); - if (matchUnOrdered != null) { + let matchList = line.match(/^([ ]*)([-+*]|[a-zA-Z0-9]\.)[ ](.*)$/); + if (matchList != null) { let listLvl = 0 - let curOffset = matchUnOrdered[1].length + let curOffset = matchList[1].length let lastOffset = this.in.list.offsets[this.in.list.offsets.length-1]; if (lastOffset == undefined) { lastOffset = -1 } - if (lastOffset < curOffset){ this.in.list.offsets.push(curOffset) listLvl = this.in.list.offsets.length - if (matchUnOrdered[2][matchUnOrdered[2].length-1] == '.') { + if (matchList[2][matchList[2].length-1] == '.') { this.html += "
              \n" this.in.list.endType.push("
            \n") } else { @@ -87,7 +85,7 @@ export class MarkDown { } } } - this.html += `
          • ${matchUnOrdered[3]}
          • \n` + this.html += `
          • ${matchList[3]}
          • \n` return true } else { if (this.in.list.offsets.length > 0) { @@ -209,7 +207,7 @@ export class MarkDown { this.html += `
            \n`
                         return
                     }
            -        if (this.process_list_unordered(line)) {
            +        if (this.process_list(line)) {
                         return
                     }
                     this.unwind_list()
            
            From 2242ad44024fa74225b43582b52563b9fb366c6e Mon Sep 17 00:00:00 2001
            From: hanishkvc 
            Date: Wed, 26 Nov 2025 16:03:07 +0530
            Subject: [PATCH 389/446] SimpleChatTCRV:Markdown:List: Allow split line items
            
            If the split lines dont have any empty lines inbetween and also
            remain within the block area of the list item which they belong
            to, then the split line will be appended to the corresponding
            list item, ELSE a new list item will be created.
            
            To help with same a generic keyed empty lines tracker logic has
            been added.
            
            TODO: Account similar semantic wrt paragraph related split lines
            ---
             .../public_simplechat/docs/changelog.md       |  6 ++-
             tools/server/public_simplechat/typemd.mjs     | 39 +++++++++++++++++--
             2 files changed, 40 insertions(+), 5 deletions(-)
            
            diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md
            index 017e975abb..66bdffa20f 100644
            --- a/tools/server/public_simplechat/docs/changelog.md
            +++ b/tools/server/public_simplechat/docs/changelog.md
            @@ -300,7 +300,11 @@ Chat Session specific settings
               * the user can change the default behaviour of tools being disabled and sliding window of 1
               * program restart will reset these back to the default
             * Ui module cleanup to avoid duplicated/unneeded boiler plates, including using updated jsdoc annotations
            -* A simple minded basic Markdown to Html logic
            +* A simple minded basic Markdown to Html logic with support for
            +  * headings
            +  * lists (ordered, unordered, intermixed at diff leves)
            +  * tables
            +  * fenced code blocks
             
             
             ## ToDo
            diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs
            index 1380835c35..125f68343b 100644
            --- a/tools/server/public_simplechat/typemd.mjs
            +++ b/tools/server/public_simplechat/typemd.mjs
            @@ -28,7 +28,7 @@ export class MarkDown {
                             /** @type {Array} */
                             endType: [],
                         },
            -            /** @type {Object} */
            +            /** @type {Object} */
                         empty: {
                         }
                     }
            @@ -40,6 +40,23 @@ export class MarkDown {
                     this.html = ""
                 }
             
            +    /**
            +     * @param {string} key
            +     * @param {string} line
            +     */
            +    empty_tracker(key, line) {
            +        if (this.in.empty[key] == undefined) {
            +            this.in.empty[key] = 0
            +        }
            +        let prev = this.in.empty[key]
            +        if (line.trim().length == 0) {
            +            this.in.empty[key] += 1
            +        } else {
            +            this.in.empty[key] = 0
            +        }
            +        return {prev: prev, cur: this.in.empty[key]}
            +    }
            +
                 unwind_list() {
                     while (true) {
                         let popped = this.in.list.endType.pop()
            @@ -52,10 +69,19 @@ export class MarkDown {
                 }
             
                 /**
            -     * Process list one line at a time
            +     * Process list one line at a time.
            +     * * Account for ordered lists as well as unordered lists, including intermixing of the lists.
            +     *   at different list hierarchy levels.
            +     * * Allow a list item line to be split into multiple lines provided the split lines retain
            +     *   the same or more line offset compared to the starting line of the item to which they belong.
            +     *   * if there is a empty line in between, then the new line will be treated as a new item.
            +     * * allows for empty lines inbetween items.
            +     *   * currently there is no limit on the number of empty lines.
            +     *     but may bring in a limit later.
                  * @param {string} line
                  */
                 process_list(line) {
            +        let emptyTracker = this.empty_tracker("list", line)
                     // spaces followed by - or + or * followed by a space and actual list item
                     let matchList = line.match(/^([ ]*)([-+*]|[a-zA-Z0-9]\.)[ ](.*)$/);
                     if (matchList != null) {
            @@ -89,7 +115,7 @@ export class MarkDown {
                         return true
                     } else {
                         if (this.in.list.offsets.length > 0) {
            -                if (line.trim().length == 0) {
            +                if (emptyTracker.cur > 0) {
                                 return true
                             }
                             let matchOffset = line.match(/^([ ]*)(.*)$/);
            @@ -100,7 +126,12 @@ export class MarkDown {
                             if (matchOffset[1].length < lastOffset) {
                                 return false
                             }
            -                this.html += `
          • ${matchOffset[2]}
          • \n` + if ((emptyTracker.prev != 0) || (!this.html.endsWith("\n"))) { + this.html += `
          • ${matchOffset[2]}
          • \n` + } else { + let html = this.html + this.html = `${html.slice(0,html.length-"\n".length)} ${matchOffset[2]}\n` + } return true } } From b3645a1164006f1ef7e0011469fa4438196c1a29 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 17:56:17 +0530 Subject: [PATCH 390/446] SimpleChatTCRV:ToolCall:ExtAi: Task decomposition/planning Had forgotten to include this in the examples before. --- .../server/public_simplechat/docs/details.md | 1 + tools/server/public_simplechat/toolai.mjs | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/tools/server/public_simplechat/docs/details.md b/tools/server/public_simplechat/docs/details.md index 5e29e5a0c5..860ce53a48 100644 --- a/tools/server/public_simplechat/docs/details.md +++ b/tools/server/public_simplechat/docs/details.md @@ -486,6 +486,7 @@ The following tools/functions are currently provided by default * End user can change this behaviour by changing the corresponding settings of the TCExternalAi special chat session, which is internally used for this tool call. * Could be used by ai for example to + * break down the task at hand into sub tasks that need to be carried out * summarise a large text content, where it could use the context of the text to generate a suitable system prompt for summarising things suitably * create a structured data from a raw textual data diff --git a/tools/server/public_simplechat/toolai.mjs b/tools/server/public_simplechat/toolai.mjs index 02f1bc0e5a..9f5032bd3e 100644 --- a/tools/server/public_simplechat/toolai.mjs +++ b/tools/server/public_simplechat/toolai.mjs @@ -68,6 +68,61 @@ let externalai_meta = { } } }, + { + "description": "Task decomposition and planning", + "tool_call": { + "name": "external_ai", + "arguments": { + "system_prompt": ` + You are an expert task decomposition / planning assistant. + Your primary role is to understand the user's complex request and break it down into a series of manageable, sequential sub tasks. + Prioritize clarity and efficiency in your breakdown. + Present your plan as a numbered list, detailing each task along with its required tools and corresponding inputs/outputs. + End with a concise Next Step recommendation. + Focus on creating a robust execution plan that another ai can follow.`, + "user_message": "Find the last happening in the field of medicine focussed around ai and robotics", + } + }, + "tool_response": { + "content": ` + 1. Clarify scope + 1.1 Confirm desired depth (overview or deep dive). + 1.2 Suggest possible focus areas (diagnostics AI, surgical robotics, rehab robotics). + + 2. Define suitable categories based on user clarification, for example + 2.1 Medical AI - diagnostics, predictive analytics. drug discovery. + 2.3 Medical robotics - surgical robots, rehabilitation and assistive robotics. + + 3. Identify authoritative sources + 3.1 Peer reviewed journals (Nature medicine, The lancet, ...). + 3.2 Major conferences. + 3.3 Industry press releases from leading companies in these domains. + + 4. Research workflow / Sourcing strategy + 4.1 Use search tool to gather recent news articles on “AI in medicine”, “robotic surgery”, ... + 4.2 Fetch the top papers and clinical trials wrt each category. + 4.3 Collate conference proceedings from last year on emerging research. + + 5. Extract & synthesize + 5.1 List key papers/patents with main findings. + 5.2 Summarize key clinical trials and their outcomes. + 5.3 Highlight notable patents and product launches. + 5.4 Note limitations, ethical concerns, regulatory status, ... + + 6. Structure output + 6.1 Create Sections - Diagnostics AI, Treatment AI, Surgical Robotics, Rehab Robotics, ... + 6.2 Present sub topics under each section with bullet points and concise explanations. + + 7. Review for accuracy and balance + 7.1 Cross check facts with at least two independent sources. + 7.2 Ensure representation of both benefits and current limitations/challenges. + + 8. Format the final output + 8.1 Use Markdown for headings and bullet lists. + 8.2 Include citations or links where appropriate. + 8.3 Add an executive summary at the beginning.` + } + }, { "description": "Literary critic", "tool_call": { From 1d26453b52fddbebee12b05c74170c121c0f6a79 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 19:02:23 +0530 Subject: [PATCH 391/446] SimpleChatTCRV:Markdown: CommonLogic listitem & para extend/append Similar to listitem before, now also allow a para to have its long lines split into adjacent lines. Inturn the logic will take care of merging them into single para. The common logic wrt both flows moved into its own helper function. --- tools/server/public_simplechat/typemd.mjs | 55 ++++++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 125f68343b..d4c41382b6 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -40,9 +40,14 @@ export class MarkDown { this.html = "" } + /** @typedef {{prev: number, cur: number}} EmptyTrackerResult */ + /** + * Track how many adjacent empty lines have been seen till now, in the immidate past. + * as well as whether the current line is empty or otherwise. * @param {string} key * @param {string} line + * @returns {EmptyTrackerResult} */ empty_tracker(key, line) { if (this.in.empty[key] == undefined) { @@ -57,6 +62,46 @@ export class MarkDown { return {prev: prev, cur: this.in.empty[key]} } + /** + * Append a new block to the end of html. + * @param {string} line + * @param {string} startMarker + * @param {string} endMarker + */ + appendnew(line, startMarker, endMarker) { + this.html += `${startMarker}${line}${endMarker}` + } + + /** + * Extend to the existing last block + * @param {string} line + * @param {string} endMarker + */ + extend(line, endMarker) { + let html = this.html + this.html = `${html.slice(0,html.length-endMarker.length)} ${line}${endMarker}` + } + + /** + * Extend the existing block, if + * * there was no immidiate empty lines AND + * * the existing block corresponds to what is specified. + * Else + * * append a new block + * + * @param {string} line + * @param {string} endMarker + * @param {string} startMarker + * @param {EmptyTrackerResult} emptyTracker + */ + extend_else_appendnew(line, endMarker, startMarker, emptyTracker) { + if ((emptyTracker.prev != 0) || (!this.html.endsWith(endMarker))) { + this.appendnew(line, startMarker, endMarker) + } else { + this.extend(line, endMarker) + } + } + unwind_list() { while (true) { let popped = this.in.list.endType.pop() @@ -126,12 +171,7 @@ export class MarkDown { if (matchOffset[1].length < lastOffset) { return false } - if ((emptyTracker.prev != 0) || (!this.html.endsWith("\n"))) { - this.html += `
          • ${matchOffset[2]}
          • \n` - } else { - let html = this.html - this.html = `${html.slice(0,html.length-"\n".length)} ${matchOffset[2]}\n` - } + this.extend_else_appendnew(matchOffset[2], "\n", '
          • ', emptyTracker) return true } } @@ -242,7 +282,8 @@ export class MarkDown { return } this.unwind_list() - this.html += `

            ${line}

            ` + let emptyTrackerPara = this.empty_tracker("para", line) + this.extend_else_appendnew(line, "

            \n", "

            ", emptyTrackerPara) } /** From edba012d80e090bef7e2ee0c6039a22ccde46cbf Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 20:03:35 +0530 Subject: [PATCH 392/446] SimpleChatTCRV:Markdown:BlockQuote support --- tools/server/public_simplechat/simplechat.js | 3 +- tools/server/public_simplechat/typemd.mjs | 56 ++++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 5a16d62d1f..31f78b201e 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1499,7 +1499,7 @@ class MultiChatUI { if (content.length > 0) { if ((name == "content") && (this.simpleChats[chatId].cfg.chatProps.bMarkdown)) { entry = document.createElement('div') - let md = new mMD.MarkDown() + let md = new mMD.MarkDown(this.simpleChats[chatId].cfg.chatProps.bMarkdownHtmlSanitize) md.process(content) entry.innerHTML = md.html secContents.appendChild(entry) @@ -2106,6 +2106,7 @@ export class Config { */ iRecentUserMsgCnt: 5, bMarkdown: true, + bMarkdownHtmlSanitize: false, bCompletionFreshChatAlways: true, bCompletionInsertStandardRolePrefix: false, bTrimGarbage: true, diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index d4c41382b6..4ba6f029d9 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -15,7 +15,12 @@ */ export class MarkDown { - constructor() { + /** + * Markdown parse and convert to html. + * @param {boolean} bHtmlSanitize + */ + constructor(bHtmlSanitize) { + this.bHtmlSanitize = bHtmlSanitize this.in = { preFenced: "", table: { @@ -30,7 +35,9 @@ export class MarkDown { }, /** @type {Object} */ empty: { - } + }, + /** @type {string} */ + blockQuote: "", } /** * @type {Array<*>} @@ -235,14 +242,50 @@ export class MarkDown { } } + unwind_blockquote() { + for(let i=0; i\n` + } + this.in.blockQuote = "" + } + + /** + * Handle blockquote block one line at a time. + * This expects all lines in the block quote to have the marker at the begining. + * + * @param {string} line + * @param {string} startTok + */ + process_blockquote(line, startTok) { + if (!line.startsWith(">")) { + this.unwind_blockquote() + return false + } + if (startTok.match(/^>+$/) == null) { + this.unwind_blockquote() + return false + } + this.unwind_list() + if (startTok.length > this.in.blockQuote.length) { + this.html += `

            \n` + } else if (startTok.length < this.in.blockQuote.length) { + this.html += `
            \n` + } + this.in.blockQuote = startTok + this.html += `

            ${line}

            ` + return true + } + /** * Process a line from markdown content * @param {string} line */ process_line(line) { - let elSanitize = document.createElement('div') - elSanitize.textContent = line - line = elSanitize.innerHTML + if (this.bHtmlSanitize) { + let elSanitize = document.createElement('div') + elSanitize.textContent = line + line = elSanitize.innerHTML + } let lineA = line.split(' ') if (this.in.preFenced.length > 0) { if (line == this.in.preFenced) { @@ -278,6 +321,9 @@ export class MarkDown { this.html += `
            \n`
                         return
                     }
            +        if (this.process_blockquote(line, lineA[0])) {
            +            return
            +        }
                     if (this.process_list(line)) {
                         return
                     }
            
            From 0f5be059cfea9597143ad6fede415bfc64654e8b Mon Sep 17 00:00:00 2001
            From: hanishkvc 
            Date: Wed, 26 Nov 2025 20:48:34 +0530
            Subject: [PATCH 393/446] SimpleChatTCRV:Markdown: Move Pre Fenced handling
             into func
            
            ---
             tools/server/public_simplechat/typemd.mjs | 43 ++++++++++++++---------
             1 file changed, 27 insertions(+), 16 deletions(-)
            
            diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs
            index 4ba6f029d9..26c0182a3c 100644
            --- a/tools/server/public_simplechat/typemd.mjs
            +++ b/tools/server/public_simplechat/typemd.mjs
            @@ -242,6 +242,32 @@ export class MarkDown {
                     }
                 }
             
            +    /**
            +     * Process Pre Fenced block one line at a time.
            +     * @param {string} line
            +     */
            +    process_pre_fenced(line) {
            +        if (this.in.preFenced.length > 0) {
            +            if (line == this.in.preFenced) {
            +                this.in.preFenced = ""
            +                this.html += "
            \n" + } else { + this.html += `${line}\n` + } + return true + } + // same number of space followed by ``` or ~~~ + // some samples with spaces at beginning seen, so accepting spaces at begin + let matchPreFenced = line.match(/^(\s*```|\s*~~~)([a-zA-Z0-9]*)(.*)/); + if ( matchPreFenced != null) { + this.unwind_list() + this.in.preFenced = matchPreFenced[1] + this.html += `
            \n`
            +            return true
            +        }
            +        return false
            +    }
            +
                 unwind_blockquote() {
                     for(let i=0; i\n`
            @@ -287,13 +313,7 @@ export class MarkDown {
                         line = elSanitize.innerHTML
                     }
                     let lineA = line.split(' ')
            -        if (this.in.preFenced.length > 0) {
            -            if (line == this.in.preFenced) {
            -                this.in.preFenced = ""
            -                this.html += "
            \n" - } else { - this.html += `${line}\n` - } + if (this.process_pre_fenced(line)) { return } if (this.process_table_line(line)) { @@ -312,15 +332,6 @@ export class MarkDown { this.html += `${line.slice(hLevel)}\n` return } - // same number of space followed by ``` or ~~~ - // some samples with spaces at beginning seen, so accepting spaces at begin - let matchPreFenced = line.match(/^(\s*```|\s*~~~)([a-zA-Z0-9]*)(.*)/); - if ( matchPreFenced != null) { - this.unwind_list() - this.in.preFenced = matchPreFenced[1] - this.html += `
            \n`
            -            return
            -        }
                     if (this.process_blockquote(line, lineA[0])) {
                         return
                     }
            
            From 707b719f6746e85df30461bc340a4ee89d7847e6 Mon Sep 17 00:00:00 2001
            From: hanishkvc 
            Date: Wed, 26 Nov 2025 21:26:55 +0530
            Subject: [PATCH 394/446] SimpleChatTCRV:Markdown:ReEnable Sanitize, lineRaw
            
            Maintain raw and sanitized versions of line.
            
            Make blockquote work with raw line and not the sanitized line.
            So irrespective of whether sanitize is enabled or not, the logic
            will still work. Inturn re-enable HtmlSanitize.
            ---
             tools/server/public_simplechat/simplechat.js |  2 +-
             tools/server/public_simplechat/typemd.mjs    | 22 ++++++++++++--------
             2 files changed, 14 insertions(+), 10 deletions(-)
            
            diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js
            index 31f78b201e..676c3385df 100644
            --- a/tools/server/public_simplechat/simplechat.js
            +++ b/tools/server/public_simplechat/simplechat.js
            @@ -2106,7 +2106,7 @@ export class Config {
                          */
                         iRecentUserMsgCnt: 5,
                         bMarkdown: true,
            -            bMarkdownHtmlSanitize: false,
            +            bMarkdownHtmlSanitize: true,
                         bCompletionFreshChatAlways: true,
                         bCompletionInsertStandardRolePrefix: false,
                         bTrimGarbage: true,
            diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs
            index 26c0182a3c..753c1834e5 100644
            --- a/tools/server/public_simplechat/typemd.mjs
            +++ b/tools/server/public_simplechat/typemd.mjs
            @@ -279,14 +279,15 @@ export class MarkDown {
                  * Handle blockquote block one line at a time.
                  * This expects all lines in the block quote to have the marker at the begining.
                  *
            -     * @param {string} line
            -     * @param {string} startTok
            +     * @param {string} lineRaw
            +     * @param {string} lineSani
                  */
            -    process_blockquote(line, startTok) {
            -        if (!line.startsWith(">")) {
            +    process_blockquote(lineRaw, lineSani) {
            +        if (!lineRaw.startsWith(">")) {
                         this.unwind_blockquote()
                         return false
                     }
            +        let startTok = lineRaw.split(' ', 1)[0]
                     if (startTok.match(/^>+$/) == null) {
                         this.unwind_blockquote()
                         return false
            @@ -298,19 +299,22 @@ export class MarkDown {
                         this.html += `\n`
                     }
                     this.in.blockQuote = startTok
            -        this.html += `

            ${line}

            ` + this.html += `

            ${lineSani}

            ` return true } /** * Process a line from markdown content - * @param {string} line + * @param {string} lineRaw */ - process_line(line) { + process_line(lineRaw) { + let line = "" if (this.bHtmlSanitize) { let elSanitize = document.createElement('div') - elSanitize.textContent = line + elSanitize.textContent = lineRaw line = elSanitize.innerHTML + } else { + line = lineRaw } let lineA = line.split(' ') if (this.process_pre_fenced(line)) { @@ -332,7 +336,7 @@ export class MarkDown { this.html += `${line.slice(hLevel)}\n` return } - if (this.process_blockquote(line, lineA[0])) { + if (this.process_blockquote(lineRaw, line)) { return } if (this.process_list(line)) { From 67f971527d25e5fabafed04fe1d54927e0cfda3f Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Wed, 26 Nov 2025 21:44:23 +0530 Subject: [PATCH 395/446] SimpleChatTCRV:Markdown:Process headline and horizline Also update readme a bit, better satisfying md file format. --- .../server/public_simplechat/docs/details.md | 18 +++++--- tools/server/public_simplechat/readme.md | 17 ++++--- tools/server/public_simplechat/typemd.mjs | 44 ++++++++++++++----- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/tools/server/public_simplechat/docs/details.md b/tools/server/public_simplechat/docs/details.md index 860ce53a48..42023e3ade 100644 --- a/tools/server/public_simplechat/docs/details.md +++ b/tools/server/public_simplechat/docs/details.md @@ -266,6 +266,10 @@ It is attached to the document object. Some of these can also be updated using t * NOTE: the latest user message (query/response/...) for which we need a ai response, will also be counted as belonging to the iRecentUserMsgCnt. + * bMarkdown - text contents in the messages are interpreted as Markdown based text and inturn converted to html form for viewing by the end user. + + * bMarkdownHtmlSanitize - the text content is sanitized using the browser's dom parser, so that any html tags get converted to normal visually equivalent text representation, before processing by the markdown to html conversion logic. + * bCompletionFreshChatAlways - whether Completion mode collates complete/sliding-window history when communicating with the server or only sends the latest user query/message. * bCompletionInsertStandardRolePrefix - whether Completion mode inserts role related prefix wrt the messages that get inserted into prompt field wrt /Completion endpoint. @@ -280,13 +284,13 @@ It is attached to the document object. Some of these can also be updated using t * enabled - control whether tool calling is enabled or not - remember to enable this only for GenAi/LLM models which support tool/function calling. + * remember to enable this only for GenAi/LLM models which support tool/function calling. * proxyUrl - specify the address for the running instance of bundled local.tools/simpleproxy.py * proxyAuthInsecure - shared token between simpleproxy.py server and client ui, for accessing service provided by it. - Shared token is currently hashed with the current year and inturn handshaked over the network. In future if required one could also include a dynamic token provided by simpleproxy server during /aum handshake and running counter or so into hashed token. ALERT: However do remember that currently the handshake occurs over http and not https, so others can snoop the network and get token. Per client ui running counter and random dynamic token can help mitigate things to some extent, if required in future. + * Shared token is currently hashed with the current year and inturn handshaked over the network. In future if required one could also include a dynamic token provided by simpleproxy server during /aum handshake and running counter or so into hashed token. ALERT: However do remember that currently the handshake occurs over http and not https, so others can snoop the network and get token. Per client ui running counter and random dynamic token can help mitigate things to some extent, if required in future. * searchUrl - specify the search engine's search url template along with the tag SEARCHWORDS in place where the search words should be substituted at runtime. @@ -304,9 +308,9 @@ It is attached to the document object. Some of these can also be updated using t * autoSecs - the amount of time in seconds to wait before the tool call request is auto triggered and generated response is auto submitted back. - setting this value to 0 (default), disables auto logic, so that end user can review the tool calls requested by ai and if needed even modify them, before triggering/executing them as well as review and modify results generated by the tool call, before submitting them back to the ai. + * setting this value to 0 (default), disables auto logic, so that end user can review the tool calls requested by ai and if needed even modify them, before triggering/executing them as well as review and modify results generated by the tool call, before submitting them back to the ai. - this is specified in seconds, so that users by default will normally not overload any website through the proxy server. + * this is specified in seconds, so that users by default will normally not overload any website through the proxy server. the builtin tools' meta data is sent to the ai model in the requests sent to it. @@ -316,11 +320,11 @@ It is attached to the document object. Some of these can also be updated using t * apiRequestOptions - maintains the list of options/fields to send along with api request, irrespective of whether /chat/completions or /completions endpoint. - If you want to add additional options/fields to send to the server/ai-model, and or remove them, for now you can do these actions manually using browser's development-tools/console. + * If you want to add additional options/fields to send to the server/ai-model, and or remove them, for now you can do these actions manually using browser's development-tools/console. - For string, numeric, boolean, object fields in apiRequestOptions, including even those added by a user at runtime by directly modifying gMe.apiRequestOptions, setting ui entries will be auto created. + * For string, numeric, boolean, object fields in apiRequestOptions, including even those added by a user at runtime by directly modifying gMe.apiRequestOptions, setting ui entries will be auto created. - cache_prompt option supported by example/server is allowed to be controlled by user, so that any caching supported wrt system-prompt and chat history, if usable can get used. When chat history sliding window is enabled, cache_prompt logic may or may not kick in at the backend wrt same, based on aspects related to model, positional encoding, attention mechanism etal. However system prompt should ideally get the benefit of caching. + * cache_prompt option supported by tools/server is allowed to be controlled by user, so that any caching supported wrt system-prompt and chat history, if usable can get used. When chat history sliding window is enabled, cache_prompt logic may or may not kick in at the backend wrt same, based on aspects related to model, positional encoding, attention mechanism etal. However system prompt should ideally get the benefit of caching. * headers - maintains the list of http headers sent when request is made to the server. By default diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 8f624d7191..ec578f1c6c 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -113,8 +113,6 @@ A lightweight simple minded ai chat client with a web front-end that supports mu - user can update the settings for auto executing these actions, if needed - external_ai allows invoking a separate optionally fresh by default ai instance - - ai could run self modified targeted versions of itself/... using custom system prompts and user messages as needed - - user can setup an ai instance with additional compute access, which should be used only if needed - by default in such a instance - tool calling is kept disabled along with - client side sliding window of 1, @@ -123,6 +121,14 @@ A lightweight simple minded ai chat client with a web front-end that supports mu and the default behaviour will get impacted if you modify the settings of this special chat session. - Restarting this chat client logic will force reset things to the default behaviour, how ever any other settings wrt TCExternalAi, that where changed, will persist across restarts. + - this instance maps to the current ai server itself by default, but can be changed by user if needed. + - could help with handling specific tasks using targetted personas or models + - ai could run self modified targeted versions of itself/... using custom system prompts and user messages as needed + - user can setup ai instance with additional compute, which should be used only if needed, to keep costs in control + - can enable a modular pipeline with task type and or job instance specific decoupling, if needed + - tasks offloaded could include + - summarising, data extraction, formatted output, translation, ... + - creative writing, task breakdown, ... - Client side Sliding window Context control, using `iRecentUserMsgCnt`, helps limit context sent to ai model @@ -135,8 +141,9 @@ A lightweight simple minded ai chat client with a web front-end that supports mu - built using plain html + css + javascript and python - no additional dependencies that one needs to worry about and inturn keep track of - except for pypdf, if pdf support needed. automaticaly drops pdf tool call support, if pypdf missing - - fits within ~260KB even in uncompressed source form (including simpleproxy.py) + - fits within ~50KB compressed source or ~284KB in uncompressed source form (both including simpleproxy.py) - easily extend with additional tool calls using either javascript or python, for additional functionality + as you see fit Start exploring / experimenting with your favorite ai models and thier capabilities. @@ -149,7 +156,7 @@ One can modify the session configuration using Settings UI. All the settings and | Group | Purpose | |---------|---------| -| `chatProps` | ApiEndpoint, streaming, sliding window, ... | +| `chatProps` | ApiEndpoint, streaming, sliding window, markdown, ... | | `tools` | `enabled`, `proxyUrl`, `proxyAuthInsecure`, search URL/template & drop rules, max data length, timeouts | | `apiRequestOptions` | `temperature`, `max_tokens`, `frequency_penalty`, `presence_penalty`, `cache_prompt`, ... | | `headers` | `Content-Type`, `Authorization`, ... | @@ -190,7 +197,7 @@ One can modify the session configuration using Settings UI. All the settings and - next wrt the last tool message - set role back to `TOOL-TEMP` - edit the response as needed - - delete the same + - or delete the same - user will be given option to edit and retrigger the tool call - submit the new response diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 753c1834e5..2b07f417bf 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -299,10 +299,40 @@ export class MarkDown { this.html += `\n` } this.in.blockQuote = startTok - this.html += `

            ${lineSani}

            ` + this.html += `

            ${lineSani}

            \n` return true } + /** + * Process headline. + * @param {string} line + */ + process_headline(line) { + if (line.startsWith ("#")) { + this.unwind_list() + let startTok = line.split(' ', 1)[0] + let hLevel = startTok.length + this.html += `${line.slice(hLevel)}\n` + return true + } + return false + } + + /** + * Process horizontal line. + * @param {string} line + */ + process_horizline(line) { + // 3 or more of --- or ___ or *** followed by space + // some online notes seemed to indicate spaces at end, so accepting same + if (line.match(/^[-]{3,}|[*]{3,}|[_]{3,}\s*$/) != null) { + this.unwind_list() + this.html += "
            \n" + return true + } + return false + } + /** * Process a line from markdown content * @param {string} lineRaw @@ -316,24 +346,16 @@ export class MarkDown { } else { line = lineRaw } - let lineA = line.split(' ') if (this.process_pre_fenced(line)) { return } if (this.process_table_line(line)) { return } - // 3 or more of --- or ___ or *** followed by space - // some online notes seemed to indicate spaces at end, so accepting same - if (line.match(/^[-]{3,}|[*]{3,}|[_]{3,}\s*$/) != null) { - this.unwind_list() - this.html += "
            \n" + if (this.process_horizline(line)) { return } - if (line.startsWith ("#")) { - this.unwind_list() - let hLevel = lineA[0].length - this.html += `${line.slice(hLevel)}\n` + if (this.process_headline(line)) { return } if (this.process_blockquote(lineRaw, line)) { From 11eab92d08a71a3ef3e6cde1aa38294d647a1b38 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 27 Nov 2025 00:43:45 +0530 Subject: [PATCH 396/446] SimpleChatTCRV:TC:FetchUrlRaw: Rename to avoid confusion Given that now fetch_web_url_raw can also fetch local files, if local file access scheme is enabled in simpleproxy.py, so rename this tool call by dropping web from its name, given that some ai models were getting confused because of the same. --- .../public_simplechat/docs/changelog.md | 8 +++--- .../server/public_simplechat/docs/details.md | 4 +-- tools/server/public_simplechat/readme.md | 2 +- tools/server/public_simplechat/toolweb.mjs | 28 +++++++++---------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 66bdffa20f..747803d6bc 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -300,11 +300,11 @@ Chat Session specific settings * the user can change the default behaviour of tools being disabled and sliding window of 1 * program restart will reset these back to the default * Ui module cleanup to avoid duplicated/unneeded boiler plates, including using updated jsdoc annotations -* A simple minded basic Markdown to Html logic with support for - * headings +* A simple minded basic Markdown to Html logic with support for below to some extent + * headings, horiz line, * lists (ordered, unordered, intermixed at diff leves) - * tables - * fenced code blocks + * tables, fenced code blocks, blockquotes +* Rename fetch_web_url_raw to fetch_url_raw, avoids confusion and matchs semantic of access to local and web. ## ToDo diff --git a/tools/server/public_simplechat/docs/details.md b/tools/server/public_simplechat/docs/details.md index 42023e3ade..2a702dfbb4 100644 --- a/tools/server/public_simplechat/docs/details.md +++ b/tools/server/public_simplechat/docs/details.md @@ -510,7 +510,7 @@ Either way always remember to cross check tool requests and generated responses ##### using bundled simpleproxy.py (helps bypass browser cors restriction, ...) -* fetch_web_url_raw - fetch contents of the requested url through a proxy server +* fetch_url_raw - fetch contents of the requested url through a proxy server * fetch_html_text - fetch text parts of the html content from the requested url through a proxy server. Related logic tries to strip html response of html tags and also head, script, style, header,footer, @@ -577,7 +577,7 @@ In future it can be further extended to help with other relatively simple yet us fetch_rss and so. * for now fetch_rss can be indirectly achieved using - * fetch_web_url_raw or better still + * fetch_url_raw or better still * xmlfiltered and its tagDropREs #### Extending with new tools diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index ec578f1c6c..99707d5dff 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -97,7 +97,7 @@ A lightweight simple minded ai chat client with a web front-end that supports mu - data_store brings in browser IndexedDB based persistant key/value storage across sessions - in collaboration with included python based simpleproxy.py, these additional tool calls are supported - - `search_web_text`, `fetch_web_url_raw`, `fetch_html_text`, `fetch_pdf_as_text`, `fetch_xml_filtered` + - `search_web_text`, `fetch_url_raw`, `fetch_html_text`, `fetch_pdf_as_text`, `fetch_xml_filtered` - these built‑in tool calls (via SimpleProxy) help fetch PDFs, HTML, XML or perform web search - PDF tool also returns an outline with numbering, if available - result is truncated to `iResultMaxDataLength` (default 128 kB) diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/toolweb.mjs index 1e02f22daa..3933489923 100644 --- a/tools/server/public_simplechat/toolweb.mjs +++ b/tools/server/public_simplechat/toolweb.mjs @@ -1,6 +1,6 @@ //@ts-check // ALERT - Simple Stupid flow - Using from a discardable VM is better -// Helpers to handle tools/functions calling related to web access, pdf, etal +// Helpers to handle tools/functions calling related to local/web access, pdf, etal // which work in sync with the bundled simpleproxy.py server logic. // Uses the js specific web worker path. // by Humans for All @@ -100,21 +100,21 @@ async function proxyserver_tc_setup(tag, chatId, tcPath, tcName, tcsData, tcs) { // -// Fetch Web Url Raw +// Fetch Url Raw // -let fetchweburlraw_meta = { +let fetchurlraw_meta = { "type": "function", "function": { - "name": "fetch_web_url_raw", - "description": "Fetch the requested web url through a proxy server and return the got content as is, in few seconds", + "name": "fetch_url_raw", + "description": "Fetch contents of the requested url (local file path / web based) through a proxy server and return the got content as is, in few seconds. Mainly useful for getting textual non binary contents", "parameters": { "type": "object", "properties": { "url":{ "type":"string", - "description":"url of the web page to fetch from the internet" + "description":"url of the local file / web content to fetch" } }, "required": ["url"] @@ -124,7 +124,7 @@ let fetchweburlraw_meta = { /** - * Implementation of the fetch web url raw logic. + * Implementation of the fetch url raw logic. * Expects a simple minded proxy server to be running locally * * listening on a configured port * * expecting http requests @@ -136,22 +136,22 @@ let fetchweburlraw_meta = { * @param {string} toolname * @param {any} obj */ -function fetchweburlraw_run(chatid, toolcallid, toolname, obj) { +function fetchurlraw_run(chatid, toolcallid, toolname, obj) { // maybe filter out any key other than 'url' in obj return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urlraw'); } /** - * Setup fetch_web_url_raw for tool calling + * Setup fetch_url_raw for tool calling * NOTE: Currently the logic is setup for the bundled simpleproxy.py * @param {mToolsMgr.TCSwitch} tcs * @param {string} chatId */ -async function fetchweburlraw_setup(tcs, chatId) { - return proxyserver_tc_setup('FetchWebUrlRaw', chatId, 'urlraw', 'fetch_web_url_raw', { - "handler": fetchweburlraw_run, - "meta": fetchweburlraw_meta, +async function fetchurlraw_setup(tcs, chatId) { + return proxyserver_tc_setup('FetchUrlRaw', chatId, 'urlraw', 'fetch_url_raw', { + "handler": fetchurlraw_run, + "meta": fetchurlraw_meta, "result": "" }, tcs); } @@ -437,7 +437,7 @@ export async function setup(chatId) { * @type {mToolsMgr.TCSwitch} tcs */ let tc_switch = {} - await fetchweburlraw_setup(tc_switch, chatId) + await fetchurlraw_setup(tc_switch, chatId) await fetchhtmltext_setup(tc_switch, chatId) await searchwebtext_setup(tc_switch, chatId) await fetchpdftext_setup(tc_switch, chatId) From 82d436b5373eb66ff3a24f81da453598a881cc10 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 27 Nov 2025 12:05:05 +0530 Subject: [PATCH 397/446] SimpleChatTCRV:Markdown: flexible unwind list --- .../public_simplechat/docs/changelog.md | 7 +++++ tools/server/public_simplechat/typemd.mjs | 28 ++++++++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 747803d6bc..9d642de0a1 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -339,3 +339,10 @@ their respective functionalities. Add support for base64 encoded pdf passing to ai models, when the models and llama engine gain that capability in turn using openai file - file-data type sub block within content array or so ... + +See why the ai streamed response not showing up in TCExternalAi chat session ui, even thou the content is getting +appended to its DivStream. IE why it is hidden. + +Markdown if a line which doesnt have any list marker appears at the same offset level as the last list item, +that too after a new line before this ambiguous line, then maybe pop out 1 level wrt the list. + diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 2b07f417bf..da42938063 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -109,15 +109,23 @@ export class MarkDown { } } - unwind_list() { - while (true) { + /** + * Unwind till the specified offset level. + * @param {number} unwindTillOffset + */ + unwind_list(unwindTillOffset=-1) { + if (this.in.list.offsets.length == 0) { + return { done: true, remaining: 0 } + } + while (this.in.list.offsets[this.in.list.offsets.length-1] > unwindTillOffset) { + this.in.list.offsets.pop() let popped = this.in.list.endType.pop() - if (popped == undefined) { + this.html += popped; + if (this.in.list.offsets.length == 0) { break } - this.html += popped } - this.in.list.offsets.length = 0 + return { done: true, remaining: this.in.list.offsets.length } } /** @@ -154,20 +162,14 @@ export class MarkDown { this.in.list.endType.push("
          \n") } } else if (lastOffset > curOffset){ - while (this.in.list.offsets[this.in.list.offsets.length-1] > curOffset) { - this.in.list.offsets.pop() - let popped = this.in.list.endType.pop() - this.html += popped; - if (this.in.list.offsets.length == 0) { - break - } - } + this.unwind_list(curOffset) } this.html += `
        • ${matchList[3]}
        • \n` return true } else { if (this.in.list.offsets.length > 0) { if (emptyTracker.cur > 0) { + // skip empty line return true } let matchOffset = line.match(/^([ ]*)(.*)$/); From 6358a2083d7a0f6eb3b53bbf3fa89563785e8a17 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 27 Nov 2025 14:46:04 +0530 Subject: [PATCH 398/446] SimpleChatTCRV:Markdown:List items without list marker cleanup If lines immidately follows a list item, without the list marker at their begining, but with a offset matching the list item, then these lines will be appended to that list item. If a empty line is there between a list item and a new line with some content, but without a list marker * if the content offset is less than the last list item, then unwind the lists before such a line. * if the content offset is larger than the last list item, then the line will be added as a new list item at the same level as the last list item. * if the content offset is same as the last list tiem, then unwind the list by one level and then insert this line as a new list item at this new unwound level. --- .../public_simplechat/docs/changelog.md | 5 +- tools/server/public_simplechat/typemd.mjs | 47 +++++++++++++++---- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 9d642de0a1..98010de0d9 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -303,6 +303,7 @@ Chat Session specific settings * A simple minded basic Markdown to Html logic with support for below to some extent * headings, horiz line, * lists (ordered, unordered, intermixed at diff leves) + accomodate lines without list markers inbetween list items to some extent, hopefully in a sane way. * tables, fenced code blocks, blockquotes * Rename fetch_web_url_raw to fetch_url_raw, avoids confusion and matchs semantic of access to local and web. @@ -342,7 +343,3 @@ in turn using openai file - file-data type sub block within content array or so See why the ai streamed response not showing up in TCExternalAi chat session ui, even thou the content is getting appended to its DivStream. IE why it is hidden. - -Markdown if a line which doesnt have any list marker appears at the same offset level as the last list item, -that too after a new line before this ambiguous line, then maybe pop out 1 level wrt the list. - diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index da42938063..5243167fc6 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -130,14 +130,24 @@ export class MarkDown { /** * Process list one line at a time. - * * Account for ordered lists as well as unordered lists, including intermixing of the lists. - * at different list hierarchy levels. - * * Allow a list item line to be split into multiple lines provided the split lines retain - * the same or more line offset compared to the starting line of the item to which they belong. - * * if there is a empty line in between, then the new line will be treated as a new item. - * * allows for empty lines inbetween items. - * * currently there is no limit on the number of empty lines. - * but may bring in a limit later. + * + * Account for ordered lists as well as unordered lists, including intermixing of the lists. + * * inturn at different list hierarchy levels. + * + * Allow a list item line to be split into multiple lines provided the split lines retain + * the same or more line offset compared to the starting line of the item to which they belong. + * * these following split lines wont have the list marker in front of them. + * + * Allows for empty lines inbetween items (ie lines with list marker) + * * currently there is no limit on the number of empty lines, but may bring in a limit later. + * + * If empty line between a list item and new line with some content, but without a list marker + * * if content offset less than last list item, then unwind the lists before such a line. + * * if content offset larger than last list item, then line will be added as new list item + * at the same level as the last list item. + * * if content offset same as last list item, then unwind list by one level and insert line + * as a new list item at this new unwound level. + * * @param {string} line */ process_list(line) { @@ -168,6 +178,7 @@ export class MarkDown { return true } else { if (this.in.list.offsets.length > 0) { + if (emptyTracker.cur > 0) { // skip empty line return true @@ -180,8 +191,24 @@ export class MarkDown { if (matchOffset[1].length < lastOffset) { return false } - this.extend_else_appendnew(matchOffset[2], "\n", '
        • ', emptyTracker) - return true + + if (emptyTracker.prev == 0) { + if (this.html.endsWith("
        • \n")) { + this.extend(matchOffset[2], "\n") + return true + } + } else { + if (matchOffset[1].length > lastOffset) { + this.appendnew(matchOffset[2], "
        • ", "
        • \n") + return true + } + let uw = this.unwind_list(lastOffset-1) + if (uw.remaining > 0) { + this.appendnew(matchOffset[2], "
        • ", "
        • \n") + return true + } + } + return false } } return false From e9546168ae7015bb5d52213dfe049dc4200cf457 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 27 Nov 2025 15:15:52 +0530 Subject: [PATCH 399/446] SimpleChatTCRV:Markdown:OrderedLists - allow only number markers --- tools/server/public_simplechat/typemd.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/typemd.mjs b/tools/server/public_simplechat/typemd.mjs index 5243167fc6..e5dd865fca 100644 --- a/tools/server/public_simplechat/typemd.mjs +++ b/tools/server/public_simplechat/typemd.mjs @@ -153,7 +153,7 @@ export class MarkDown { process_list(line) { let emptyTracker = this.empty_tracker("list", line) // spaces followed by - or + or * followed by a space and actual list item - let matchList = line.match(/^([ ]*)([-+*]|[a-zA-Z0-9]\.)[ ](.*)$/); + let matchList = line.match(/^([ ]*)([-+*]|[0-9]+\.)[ ](.*)$/); if (matchList != null) { let listLvl = 0 let curOffset = matchList[1].length From d97147568fa481b251cac6a53ae218427753bd3b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 27 Nov 2025 16:39:27 +0530 Subject: [PATCH 400/446] SimpleChatTCRV:Markdown: Usage flow cleanup Move all markdown configs into a single object field. Add always flag, which if set, all roles' message contents will be treated as markdown, else only ai assistant's messages will be treated as markdown. --- .../server/public_simplechat/docs/details.md | 13 ++++++----- tools/server/public_simplechat/simplechat.js | 22 +++++++++++++++---- tools/server/public_simplechat/ui.mjs | 2 +- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/tools/server/public_simplechat/docs/details.md b/tools/server/public_simplechat/docs/details.md index 2a702dfbb4..7be58683da 100644 --- a/tools/server/public_simplechat/docs/details.md +++ b/tools/server/public_simplechat/docs/details.md @@ -266,9 +266,12 @@ It is attached to the document object. Some of these can also be updated using t * NOTE: the latest user message (query/response/...) for which we need a ai response, will also be counted as belonging to the iRecentUserMsgCnt. - * bMarkdown - text contents in the messages are interpreted as Markdown based text and inturn converted to html form for viewing by the end user. + * Markdown - * bMarkdownHtmlSanitize - the text content is sanitized using the browser's dom parser, so that any html tags get converted to normal visually equivalent text representation, before processing by the markdown to html conversion logic. + - enabled: whether markdown support is enabled or not. + - always: if true, all messages text content interpreted as Markdown based text and converted to html for viewing. + if false, then interpret only ai assistant's text content as markdown. + - htmlSanitize: text content sanitized using browser's dom parser, so html/xml tags get converted to normal visually equivalent text representation, before processing by markdown to html conversion logic. * bCompletionFreshChatAlways - whether Completion mode collates complete/sliding-window history when communicating with the server or only sends the latest user query/message. @@ -312,11 +315,11 @@ It is attached to the document object. Some of these can also be updated using t * this is specified in seconds, so that users by default will normally not overload any website through the proxy server. - the builtin tools' meta data is sent to the ai model in the requests sent to it. + 1. the builtin tools' meta data is sent to the ai model in the requests sent to it. - inturn if the ai model requests a tool call to be made, the same will be done and the response sent back to the ai model, under user control, by default. + 2. inturn if the ai model requests a tool call to be made, the same will be done and the response sent back to the ai model, under user control, by default. - as tool calling will involve a bit of back and forth between ai assistant and end user, it is recommended to set iRecentUserMsgCnt to 10 or so, so that enough context is retained during chatting with ai models with tool support. Decide based on your available system and video ram and the type of chat you are having. + 3. as tool calling will involve a bit of back and forth between ai assistant and end user, it is recommended to set iRecentUserMsgCnt to 10 or so, so that enough context is retained during chatting with ai models with tool support. Decide based on your available system and video ram and the type of chat you are having. * apiRequestOptions - maintains the list of options/fields to send along with api request, irrespective of whether /chat/completions or /completions endpoint. diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 676c3385df..56725ad3e4 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1495,11 +1495,22 @@ class MultiChatUI { if (msg.ns.getContent().trim().length > 0) { showList.push(['content', msg.ns.getContent().trim()]) } + let chatSessionMarkdown = this.simpleChats[chatId].cfg.chatProps.markdown for (const [name, content] of showList) { if (content.length > 0) { - if ((name == "content") && (this.simpleChats[chatId].cfg.chatProps.bMarkdown)) { + let bMarkdown = false + if ((name == "content") && (chatSessionMarkdown.enabled)) { + if (chatSessionMarkdown.always) { + bMarkdown = true + } else { + if (msg.ns.role == Roles.Assistant) { + bMarkdown = true + } + } + } + if (bMarkdown) { entry = document.createElement('div') - let md = new mMD.MarkDown(this.simpleChats[chatId].cfg.chatProps.bMarkdownHtmlSanitize) + let md = new mMD.MarkDown(chatSessionMarkdown.htmlSanitize) md.process(content) entry.innerHTML = md.html secContents.appendChild(entry) @@ -2105,8 +2116,11 @@ export class Config { * * only user messages following the latest system prompt is considered. */ iRecentUserMsgCnt: 5, - bMarkdown: true, - bMarkdownHtmlSanitize: true, + markdown: { + enabled: true, + always: false, + htmlSanitize: true, + }, bCompletionFreshChatAlways: true, bCompletionInsertStandardRolePrefix: false, bTrimGarbage: true, diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 92984e4beb..13eefb4976 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -51,7 +51,7 @@ export function el_create_button(id, callback, name=undefined, innerText=undefin /** * Create a para and set it up. Optionaly append it to a passed parent. - * @param {string} text + * @param {string} text - assigned to innerText * @param {HTMLElement | undefined} elParent * @param {string | undefined} id */ From 701ebca4772866f6a40abbb369b2c2d1bca1b1de Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 28 Nov 2025 14:28:31 +0530 Subject: [PATCH 401/446] SimpleChatTCRV:CMContentTextFormat: towards user fine adjustable Add a textFormat field wrt ChatMessageEx. User can be allowed to change how to interpret the text content at a individual message level. --- tools/server/public_simplechat/simplechat.js | 33 ++++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 56725ad3e4..6511bd71f6 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -275,6 +275,15 @@ export class NSChatMessage { } +class Format { + static Text = { + Plain: "plain", + Html: "html", + Markdown: "markdown", + } + static AllowedText = [ this.Text.Plain, this.Text.Markdown ] +} + export class ChatMessageEx { static uniqCounter = 0 @@ -300,6 +309,7 @@ export class ChatMessageEx { } this.uniqId = ChatMessageEx.getUniqId() this.trimmedContent = trimmedContent; + this.textFormat = Format.Text.Plain } /** @@ -307,12 +317,15 @@ export class ChatMessageEx { * @param {ChatMessageEx} old */ static newFrom(old) { - return new ChatMessageEx(new NSChatMessage(old.ns.role, old.ns.content, old.ns.reasoning_content, old.ns.tool_calls, old.ns.tool_call_id, old.ns.name, old.ns.image_urls), old.trimmedContent) + let newCMEx = new ChatMessageEx(new NSChatMessage(old.ns.role, old.ns.content, old.ns.reasoning_content, old.ns.tool_calls, old.ns.tool_call_id, old.ns.name, old.ns.image_urls), old.trimmedContent) + newCMEx.textFormat = old.textFormat + return newCMEx } clear() { this.ns = new NSChatMessage() this.trimmedContent = undefined; + this.textFormat = Format.Text.Plain } /** @@ -1482,33 +1495,33 @@ class MultiChatUI { // Add the content let showList = [] if (msg.ns.has_reasoning()) { - showList.push(['reasoning', `!!!Reasoning: ${msg.ns.getReasoningContent()} !!!\n\n`]) + showList.push(['reasoning', Format.Text.Plain, `!!!Reasoning: ${msg.ns.getReasoningContent()} !!!\n\n`]) } if (msg.ns.has_toolresponse()) { if (msg.ns.tool_call_id) { - showList.push(['toolcallid', `tool-call-id: ${msg.ns.tool_call_id}`]) + showList.push(['toolcallid', Format.Text.Plain, `tool-call-id: ${msg.ns.tool_call_id}`]) } if (msg.ns.name) { - showList.push(['toolname', `tool-name: ${msg.ns.name}`]) + showList.push(['toolname', Format.Text.Plain, `tool-name: ${msg.ns.name}`]) } } if (msg.ns.getContent().trim().length > 0) { - showList.push(['content', msg.ns.getContent().trim()]) + showList.push(['content', msg.textFormat, msg.ns.getContent().trim()]) } let chatSessionMarkdown = this.simpleChats[chatId].cfg.chatProps.markdown - for (const [name, content] of showList) { + for (const [name, stype, content] of showList) { + let sftype = stype if (content.length > 0) { - let bMarkdown = false if ((name == "content") && (chatSessionMarkdown.enabled)) { if (chatSessionMarkdown.always) { - bMarkdown = true + sftype = Format.Text.Markdown } else { if (msg.ns.role == Roles.Assistant) { - bMarkdown = true + sftype = Format.Text.Markdown } } } - if (bMarkdown) { + if (sftype == Format.Text.Markdown) { entry = document.createElement('div') let md = new mMD.MarkDown(chatSessionMarkdown.htmlSanitize) md.process(content) From c68316bf07e04723d2645fc5866e7e7901dab819 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 28 Nov 2025 15:05:49 +0530 Subject: [PATCH 402/446] SimpleChatTCRV:CMTextFormat: PopOver & Message UI adjust Add format selection box to the popover. Update show_message logic to allow refreshing a existing message ui element, rather than creating a new one. Trigger refresh of the message ui element, when format selection changes. --- tools/server/public_simplechat/index.html | 4 ++ tools/server/public_simplechat/simplechat.js | 43 ++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/index.html b/tools/server/public_simplechat/index.html index 217b025039..d99489a24a 100644 --- a/tools/server/public_simplechat/index.html +++ b/tools/server/public_simplechat/index.html @@ -47,6 +47,10 @@
    +
    diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 6511bd71f6..f61fa5780a 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -1232,6 +1232,7 @@ class MultiChatUI { this.elPopoverChatMsg = /** @type{HTMLElement} */(document.getElementById("popover-chatmsg")); this.elPopoverChatMsgCopyBtn = /** @type{HTMLElement} */(document.getElementById("popover-chatmsg-copy")); this.elPopoverChatMsgDelBtn = /** @type{HTMLElement} */(document.getElementById("popover-chatmsg-del")); + this.elPopoverChatMsgFormatSelect = /** @type{HTMLSelectElement} */(document.getElementById("popover-chatmsg-format")); this.uniqIdChatMsgPO = -1; // image popover menu @@ -1439,13 +1440,24 @@ class MultiChatUI { * * Assistant which contains a tool req, shows tool call ui if needed. ie * * if it is the last message OR * * if it is the last but one message and there is a ToolTemp message next + * + * If secMainRefresh is + * * undefined : create and append a new message related ui element + * * provided : refresh its contents as needed + * + * To refresh a existing message related ui element without bothering about + * or messing with tool call edit/trigger or response related handling, + * remember to pass a large number wrt iFromLast and undefined wrt nextMsg + * * rather wrt current logic any number greater than 1 will do. + * * @param {HTMLElement | undefined} elParent * @param {string} chatId * @param {ChatMessageEx} msg * @param {number} iFromLast * @param {ChatMessageEx | undefined} nextMsg + * @param {HTMLElement | undefined} secMainRefresh - allow one to update a existing instance of the message ui element. */ - show_message(elParent, chatId, msg, iFromLast, nextMsg) { + show_message(elParent, chatId, msg, iFromLast, nextMsg, secMainRefresh=undefined) { // Handle ToolTemp if (iFromLast == 0) { if (msg.ns.role === Roles.ToolTemp) { @@ -1456,7 +1468,12 @@ class MultiChatUI { this.elInUser.dataset.role = (msg.ns.role == Roles.ToolTemp) ? Roles.ToolTemp : Roles.User } // Create main section - let secMain = document.createElement('section') + let secMain = secMainRefresh + if (!secMain) { + secMain = document.createElement('section') + } else { + secMain.replaceChildren() + } secMain.id = `cmuid${msg.uniqId}` secMain.dataset.cmuid = String(msg.uniqId) secMain.classList.add(`role-${msg.ns.role}`) @@ -1482,7 +1499,9 @@ class MultiChatUI { secMain.addEventListener('mouseleave', (ev)=>{ console.debug(`DBUG:MCUI:ChatMessageMLeave:${msg.uniqId}`) }) - elParent?.append(secMain) + if (!secMainRefresh) { + elParent?.append(secMain) + } secMain.setAttribute("CMUniqId", String(msg.uniqId)) this.elLastChatMessage = secMain; // Create role para @@ -1558,7 +1577,7 @@ class MultiChatUI { this.ui_toolcallvalidated_as_needed(msg, chatId, bAuto); } } - // Handle tool call message show + // Handle showing of tool calls in the message if (msg.ns.tool_calls) { for (const tc of msg.ns.tool_calls) { this.show_message_toolcall(secContents, tc) @@ -1846,6 +1865,22 @@ class MultiChatUI { navigator.clipboard.write([item]) }) + this.elPopoverChatMsgFormatSelect.addEventListener('change', (ev) =>{ + console.log(`DBUG:SimpleChat:MCUI:ChatMsgPO:Format:${this.curChatId}:${this.uniqIdChatMsgPO}:${this.elPopoverChatMsgFormatSelect.value}`) + let chatSession = this.simpleChats[this.curChatId] + let index = chatSession.get_chatmessage_index(this.uniqIdChatMsgPO) + let chat = chatSession.xchat[index] + if (!chat.ns.has_content()) { + return + } + chat.textFormat = this.elPopoverChatMsgFormatSelect.value + /** @type {HTMLElement | null} */ + let elMsg = this.elDivChat.querySelector(`[id="cmuid${this.uniqIdChatMsgPO}"]`) + if (elMsg) { + this.show_message(this.elDivChat, chatSession.chatId, chat, 123, undefined, elMsg) + } + }) + } /** From afd6365ecc27caca6941e503c9148ea0a9cc9348 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 28 Nov 2025 20:12:17 +0530 Subject: [PATCH 403/446] SimpleChatTCRV:CMTextFormat: Common flow accounting User and Auto If user explicitly makes a content text format selection, the same will be used. Else based on session settings, a format will be used. Now when the popover menu is shown, the current message's format type is reflected in the popover menu. --- tools/server/public_simplechat/docs/changelog.md | 1 + tools/server/public_simplechat/docs/details.md | 3 ++- tools/server/public_simplechat/index.html | 1 + tools/server/public_simplechat/readme.md | 3 ++- tools/server/public_simplechat/simplechat.js | 16 +++++++++++++--- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 98010de0d9..b2a40d8a60 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -305,6 +305,7 @@ Chat Session specific settings * lists (ordered, unordered, intermixed at diff leves) accomodate lines without list markers inbetween list items to some extent, hopefully in a sane way. * tables, fenced code blocks, blockquotes + * User given control to enable markdown implicitly at a session level, or explicitly set wrt individual msgs. * Rename fetch_web_url_raw to fetch_url_raw, avoids confusion and matchs semantic of access to local and web. diff --git a/tools/server/public_simplechat/docs/details.md b/tools/server/public_simplechat/docs/details.md index 7be58683da..022aaffe86 100644 --- a/tools/server/public_simplechat/docs/details.md +++ b/tools/server/public_simplechat/docs/details.md @@ -268,7 +268,8 @@ It is attached to the document object. Some of these can also be updated using t * Markdown - - enabled: whether markdown support is enabled or not. + - enabled: whether auto markdown support is enabled or not at a session level. + - user can always override explicitly wrt any chat message, as they see fit. - always: if true, all messages text content interpreted as Markdown based text and converted to html for viewing. if false, then interpret only ai assistant's text content as markdown. - htmlSanitize: text content sanitized using browser's dom parser, so html/xml tags get converted to normal visually equivalent text representation, before processing by markdown to html conversion logic. diff --git a/tools/server/public_simplechat/index.html b/tools/server/public_simplechat/index.html index d99489a24a..8bc582459a 100644 --- a/tools/server/public_simplechat/index.html +++ b/tools/server/public_simplechat/index.html @@ -48,6 +48,7 @@
    diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 99707d5dff..e33e0e2c11 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -133,7 +133,8 @@ A lightweight simple minded ai chat client with a web front-end that supports mu - Client side Sliding window Context control, using `iRecentUserMsgCnt`, helps limit context sent to ai model - Optional - - simple minded markdown parsing of chat message text contents (default) + - simple minded markdown parsing of chat message text contents (default wrt assistant messages/responses) + - user can override, if needed globally or at a individual message level - auto trimming of trailing garbage from model outputs - Follows responsive design to try adapt to any screen size diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index f61fa5780a..8b95eee128 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -277,6 +277,7 @@ export class NSChatMessage { class Format { static Text = { + Default: "default", Plain: "plain", Html: "html", Markdown: "markdown", @@ -309,7 +310,7 @@ export class ChatMessageEx { } this.uniqId = ChatMessageEx.getUniqId() this.trimmedContent = trimmedContent; - this.textFormat = Format.Text.Plain + this.textFormat = Format.Text.Default } /** @@ -325,7 +326,7 @@ export class ChatMessageEx { clear() { this.ns = new NSChatMessage() this.trimmedContent = undefined; - this.textFormat = Format.Text.Plain + this.textFormat = Format.Text.Default } /** @@ -1531,7 +1532,7 @@ class MultiChatUI { for (const [name, stype, content] of showList) { let sftype = stype if (content.length > 0) { - if ((name == "content") && (chatSessionMarkdown.enabled)) { + if ((name == "content") && (sftype == Format.Text.Default) && (chatSessionMarkdown.enabled)) { if (chatSessionMarkdown.always) { sftype = Format.Text.Markdown } else { @@ -1848,6 +1849,15 @@ class MultiChatUI { // ChatMessage edit popover menu + this.elPopoverChatMsg.addEventListener('beforetoggle', (tev)=>{ + let chatSession = this.simpleChats[this.curChatId] + let index = chatSession.get_chatmessage_index(this.uniqIdChatMsgPO) + let chat = chatSession.xchat[index] + if (chat.ns.has_content()) { + this.elPopoverChatMsgFormatSelect.value = chat.textFormat + } + }) + this.elPopoverChatMsgDelBtn.addEventListener('click', (ev) => { console.log(`DBUG:SimpleChat:MCUI:ChatMsgPO:Del:${this.curChatId}:${this.uniqIdChatMsgPO}`) this.chatmsg_del_uiupdate(this.curChatId, this.uniqIdChatMsgPO) From 29f9abede9016692f4be89b06995b3da11053410 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 29 Nov 2025 01:47:45 +0530 Subject: [PATCH 404/446] SimpleChatTCRV:UICleanup: Show own divstream mostly, ShowInfo all Always show all the info wihen show_info is called, inturn avoid the corresponding all info enable flag wrt show_info as well as chat_show. Now chat_show gives the option to its caller to enable showing of its own chat session divStream. This is in addition to the handle multipart response also calling corresponding divStream show. Previously chat_show would have not only cleared corresponding chat session's divStream contents but would have also hidden divStream. Now except for the clearChat case, in all other cases own divStream is unhidden, when chat_show is called. Without this, when a tool call takes too much time and inturn a chat session times out the tool call and inturn user switches between chat sessions, if the tool call was external_ai, then its related live ai response would no longer be visible in any of the chat sessions, including the external_ai special chat session, if the user had switched to this external_ai special chat session. But now in the external_ai special chat session, the live response will be visible. TODO: With this new semantic wrt chat_show, where a end user can always peek into a chat session's ai live stream response if any, as long as that chat session's ai server handshake is still active, So now After tool call timeout, which allows users to switch between sessions, it is better to disable the external ai live divStream in other chat sessions, when user switches into them. This ensures that 1. if user doesnt switch out of the chat session which triggered external_ai, for now the user can continue to see the ext ai live response stream. 2. Switching out of the chat session which triggered ext ai, will automatically disable viewing of external ai live response from all chat sessions except for the external ai's special chat session. IE I need to explicitly clear not just the own divStream, but also the external ai related divStream, which is appened to end of all chat session's UI. This will tidy up the usage flow and ui and avoid forcefully showing external ai tool call's ai live response in other chat sessions, which didnt trigger the ext ai tool call. And also in the chat session which triggered ext ai, it will stop showing if user exits out of that chat session. Same time user can always look at the ext ai live response stream in the special chat session corresponding to ext ai. --- tools/server/public_simplechat/simplechat.js | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 8b95eee128..9594cf52cb 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -539,11 +539,18 @@ class ELDivStream { this.divData = elDivData } + /** + * Unhide ElDivStream. + */ show() { this.div.hidden = false this.div.style.visibility = "visible" } + /** + * Clear contents from the immidiate Child (ie Role and Data) elements. + * Hide ElDivStream. + */ clear() { this.divRole.replaceChildren() this.divData.replaceChildren() @@ -1603,9 +1610,9 @@ class MultiChatUI { * * @param {string} chatId * @param {boolean} bClear - * @param {boolean} bShowInfoAll + * @param {boolean} bShowOwnDivStream */ - chat_show(chatId, bClear=true, bShowInfoAll=false) { + chat_show(chatId, bClear=true, bShowOwnDivStream=false) { if (chatId != this.curChatId) { return false } @@ -1615,6 +1622,9 @@ class MultiChatUI { this.ui_userinput_reset() this.elDivStreams[chatId]?.clear() } + if (bShowOwnDivStream) { + this.elDivStreams[chatId]?.show() + } this.ui_toolcallvalidated_as_needed(new ChatMessageEx()); this.elLastChatMessage = null let chatToShow = chat.recent_chat(chat.cfg.chatProps.iRecentUserMsgCnt); @@ -1634,7 +1644,7 @@ class MultiChatUI { if (bClear) { this.elDivChat.innerHTML = usage_note(chat.cfg.get_sRecentUserMsgCnt()); this.me.setup_load(this.elDivChat, chat); - chat.cfg.show_info(this.elDivChat, bShowInfoAll); + chat.cfg.show_info(this.elDivChat); this.me.show_title(this.elDivChat); } } @@ -1842,7 +1852,7 @@ class MultiChatUI { this.elInSystem.value = value.substring(0,value.length-1); let chat = this.simpleChats[this.curChatId]; chat.add_system_anytime(this.elInSystem.value, this.curChatId); - this.chat_show(chat.chatId) + this.chat_show(chat.chatId, true, true) ev.preventDefault(); } }); @@ -2226,13 +2236,9 @@ export class Config { /** * Show the configurable parameters info in the passed Div element. * @param {HTMLDivElement} elDiv - * @param {boolean} bAll */ - show_info(elDiv, bAll=false) { + show_info(elDiv) { let props = ["baseURL", "modelInfo","headers", "tools", "apiRequestOptions", "chatProps"]; - if (!bAll) { - props = [ "baseURL", "modelInfo", "tools", "chatProps" ]; - } let elInfo = document.createElement("div") elInfo.id = "DefaultInfo" elDiv.appendChild(elInfo) From 2981033fac869306e79b7d07349898c8fec7fe58 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 29 Nov 2025 04:42:20 +0530 Subject: [PATCH 405/446] SimpleChatTCRV:UICleanup:Ai Reasoning/Live response Implement todo noted in last commit, and bit more. This brings in clearing of the external ai tool call special chat session divStream during chat show, which ensures that it gets hidden by default wrt other chat sessions and inturn only get enabled if user triggers a new tool call involving external ai tool call. This patch also ensures that if ext ai tool call takes too much time and so logic gives you back control with a timed out response as a possible response back to ai wrt the tool call, then the external ai tool call's ai live response is no longer visible in the current chat session ui. So user can go ahead with the timed out response or some other user decided response as the response to the tool call. And take the chat in a different direction of their and ai's choosing. Or else, if they want to they can switch to the External Ai specific special chat session and continue to monitor the response from the tool call there, to understand what the final response would have been wrt that tool call. Rather this should keep the ui flow clean. ALERT: If the user triggers a new ext ai tool call, when the old one is still alive in the background, then response from both will be in a race for user visibility, so beware of it. --- .../public_simplechat/docs/changelog.md | 5 +++++ tools/server/public_simplechat/simplechat.js | 11 ++++++++++ tools/server/public_simplechat/ui.mjs | 21 +++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index b2a40d8a60..e85b9e39da 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -307,6 +307,11 @@ Chat Session specific settings * tables, fenced code blocks, blockquotes * User given control to enable markdown implicitly at a session level, or explicitly set wrt individual msgs. * Rename fetch_web_url_raw to fetch_url_raw, avoids confusion and matchs semantic of access to local and web. +* Now external_ai specific special chat session's and inturn external ai tool call's ai live response stream + is visible in the chat session which triggered external ai, only till one gets respose or the tool call times + out. In turn if the tool call times out, one can send the timeout message as the response to the tool call + or what ever they see fit. Parallely, they can always look into the external ai specific special chat session + tab to see the ai response live stream and the progress wrt the tool call that timed out. ## ToDo diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 9594cf52cb..1c782b60f4 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -537,6 +537,8 @@ class ELDivStream { }); elDiv.appendChild(elDivData) this.divData = elDivData + /** @type {string | undefined} */ + this.styleDisplay = ui.ss_get(0, '.chat-message', 'display') } /** @@ -545,6 +547,9 @@ class ELDivStream { show() { this.div.hidden = false this.div.style.visibility = "visible" + if (this.styleDisplay) { + this.div.style.display = this.styleDisplay + } } /** @@ -556,6 +561,7 @@ class ELDivStream { this.divData.replaceChildren() this.div.hidden = true this.div.style.visibility = "collapse" + this.div.style.display = "none" } } @@ -1621,6 +1627,7 @@ class MultiChatUI { this.elDivChat.replaceChildren(); this.ui_userinput_reset() this.elDivStreams[chatId]?.clear() + this.elDivStreams[AI_TC_SESSIONNAME].clear() } if (bShowOwnDivStream) { this.elDivStreams[chatId]?.show() @@ -1862,6 +1869,9 @@ class MultiChatUI { this.elPopoverChatMsg.addEventListener('beforetoggle', (tev)=>{ let chatSession = this.simpleChats[this.curChatId] let index = chatSession.get_chatmessage_index(this.uniqIdChatMsgPO) + if (index == -1) { + return + } let chat = chatSession.xchat[index] if (chat.ns.has_content()) { this.elPopoverChatMsgFormatSelect.value = chat.textFormat @@ -2021,6 +2031,7 @@ class MultiChatUI { this.chatmsg_addsmart_uishow(chat.chatId, new ChatMessageEx(NSChatMessage.new_tool_response(Roles.ToolTemp, toolCallId, toolname, toolResult))) } else { this.timers.toolcallResponseTimeout = setTimeout(() => { + this.elDivStreams[AI_TC_SESSIONNAME].clear() this.me.toolsMgr.toolcallpending_found_cleared(chat.chatId, toolCallId, 'MCUI:HandleToolRun:TimeOut') this.chatmsg_addsmart_uishow(chat.chatId, new ChatMessageEx(NSChatMessage.new_tool_response(Roles.ToolTemp, toolCallId, toolname, `Tool/Function call ${toolname} taking too much time, aborting...`))) }, chat.cfg.tools.toolCallResponseTimeoutMS) diff --git a/tools/server/public_simplechat/ui.mjs b/tools/server/public_simplechat/ui.mjs index 13eefb4976..69308ab7fa 100644 --- a/tools/server/public_simplechat/ui.mjs +++ b/tools/server/public_simplechat/ui.mjs @@ -374,3 +374,24 @@ export function remove_els(sSelectorsTemplate) { el?.remove() } } + + +/** + * Get value of specified property belonging to specified css rule and stylesheet. + * @param {number} ssIndex + * @param {string} selectorText + * @param {string} property + */ +export function ss_get(ssIndex, selectorText, property) { + for (const rule of document.styleSheets[ssIndex].cssRules) { + if (rule.constructor.name == "CSSStyleRule") { + let sr = /** @type {CSSStyleRule} */(rule) + if (sr.selectorText.trim() != selectorText) { + continue + } + // @ts-ignore + return sr.style[property] + } + } + return undefined +} From 593e83117582c84c945f1d033d77d917991f7964 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 4 Dec 2025 11:36:49 +0530 Subject: [PATCH 406/446] SimpleSallap:Cleanup notes --- tools/server/public_simplechat/docs/changelog.md | 3 --- tools/server/public_simplechat/main.js | 8 +++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index e85b9e39da..a3895a337e 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -346,6 +346,3 @@ their respective functionalities. Add support for base64 encoded pdf passing to ai models, when the models and llama engine gain that capability in turn using openai file - file-data type sub block within content array or so ... - -See why the ai streamed response not showing up in TCExternalAi chat session ui, even thou the content is getting -appended to its DivStream. IE why it is hidden. diff --git a/tools/server/public_simplechat/main.js b/tools/server/public_simplechat/main.js index d60fc91034..b3d6608345 100644 --- a/tools/server/public_simplechat/main.js +++ b/tools/server/public_simplechat/main.js @@ -1,7 +1,9 @@ // @ts-check -// A simple implementation of GenAi/LLM chat web client ui / front end logic. -// It handshake with ai server's completions and chat/completions endpoints -// and helps with basic usage and testing. +// A simple minded GenAi/LLM chat web client implementation. +// Handshakes with +// * ai server's completions and chat/completions endpoints +// * simpleproxy tool calls provider +// Helps with basic usage and testing. // by Humans for All From c4e0c031072e8621033897215f0e2f2f2d25c8f2 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Thu, 4 Dec 2025 13:16:30 +0530 Subject: [PATCH 407/446] SimpleSallap:SimpleProxy: Enable https mode --- .../server/public_simplechat/docs/details.md | 10 ++++- .../local.tools/simpleproxy.py | 38 ++++++++++++++++--- .../local.tools/test-gen-self-signed.sh | 3 ++ tools/server/public_simplechat/readme.md | 1 + 4 files changed, 45 insertions(+), 7 deletions(-) create mode 100755 tools/server/public_simplechat/local.tools/test-gen-self-signed.sh diff --git a/tools/server/public_simplechat/docs/details.md b/tools/server/public_simplechat/docs/details.md index 022aaffe86..7ff53e7860 100644 --- a/tools/server/public_simplechat/docs/details.md +++ b/tools/server/public_simplechat/docs/details.md @@ -109,6 +109,8 @@ remember to * the white list of allowed.domains * review and update this to match your needs. * the shared bearer token between simpleproxy server and client ui + * the public certificate and private key files to enable https mode + * sec.certfile and sec.keyfile * other builtin tool / function calls like datetime, calculator, javascript runner, DataStore, external ai dont require the simpleproxy.py helper. @@ -294,7 +296,7 @@ It is attached to the document object. Some of these can also be updated using t * proxyAuthInsecure - shared token between simpleproxy.py server and client ui, for accessing service provided by it. - * Shared token is currently hashed with the current year and inturn handshaked over the network. In future if required one could also include a dynamic token provided by simpleproxy server during /aum handshake and running counter or so into hashed token. ALERT: However do remember that currently the handshake occurs over http and not https, so others can snoop the network and get token. Per client ui running counter and random dynamic token can help mitigate things to some extent, if required in future. + * Shared token is currently hashed with the current year and inturn handshaked over the network. In future if required one could also include a dynamic token provided by simpleproxy server during /aum handshake and running counter or so into hashed token. ALERT: However do remember that currently by default handshake occurs over http and not https, so others can snoop the network and get token. Per client ui running counter and random dynamic token can help mitigate things to some extent, if required in future. Remember to enable https mode by specifying a valid public certificate and private key. * searchUrl - specify the search engine's search url template along with the tag SEARCHWORDS in place where the search words should be substituted at runtime. @@ -570,6 +572,12 @@ The bundled simple proxy a provision for shared bearer token to be specified by the end user. One could even control what schemes are supported wrt the urls. +* by default runs in http mode. If valid sec.keyfile and sec.certfile options are specified, logic + will run in https mode. + * Remember to also update tools->proxyUrl wrt the chat session settings. + * the new url will be used for subsequent tool handshakes, however remember that the list of + tool calls supported wont get updated, till the client web ui is refreshed/reloaded. + * it tries to mimic the client/browser making the request to it by propogating header entries like user-agent, accept and accept-language from the got request to the generated request during proxying so that websites will hopefully respect the request rather than blindly rejecting it as coming from diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index fb55482cb3..2f83bbe214 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -22,8 +22,10 @@ import sys import http.server import urllib.parse import time -import urlvalidator as uv +import ssl +import traceback from typing import Callable +import urlvalidator as uv import pdfmagic as mPdf import webmagic as mWeb import debug as mDebug @@ -43,7 +45,9 @@ gConfigType = { '--debug': 'bool', '--allowed.schemes': 'list', '--allowed.domains': 'list', - '--bearer.insecure': 'str' + '--bearer.insecure': 'str', + '--sec.keyfile': 'str', + '--sec.certfile': 'str' } gConfigNeeded = [ '--allowed.schemes', '--allowed.domains', '--bearer.insecure' ] @@ -131,7 +135,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): """ Handle GET requests """ - print(f"\n\n\nDBUG:ProxyHandler:GET:{self.address_string()}:{self.path}") + print(f"DBUG:ProxyHandler:GET:{self.address_string()}:{self.path}") print(f"DBUG:PH:Get:Headers:{self.headers}") pr = urllib.parse.urlparse(self.path) print(f"DBUG:ProxyHandler:GET:{pr}") @@ -158,6 +162,10 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): self.send_response(200) self.send_headers_common() + def handle(self) -> None: + print(f"\n\n\nDBUG:ProxyHandler:Handle:RequestFrom:{self.client_address}") + return super().handle() + def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): """ @@ -265,12 +273,30 @@ def process_args(args: list[str]): uv.validator_setup(gMe['--allowed.schemes'], gMe['--allowed.domains']) - -def run(): +def setup_server(): + """ + Helps setup a http/https server + """ try: gMe['serverAddr'] = ('', gMe['--port']) gMe['server'] = http.server.HTTPServer(gMe['serverAddr'], ProxyHandler) - print(f"INFO:Run:Starting on {gMe['serverAddr']}") + if gMe.get('--sec.keyfile') and gMe.get('--sec.certfile'): + sslCtxt = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + sslCtxt.load_cert_chain(certfile=gMe['--sec.certfile'], keyfile=gMe['--sec.keyfile']) + sslCtxt.minimum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED + sslCtxt.maximum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED + gMe['server'].socket = sslCtxt.wrap_socket(gMe['server'].socket, server_side=True) + print(f"INFO:SetupServer:Starting on {gMe['serverAddr']}:Https mode") + else: + print(f"INFO:SetupServer:Starting on {gMe['serverAddr']}:Http mode") + except Exception as exc: + print(f"ERRR:SetupServer:{traceback.format_exc()}") + raise RuntimeError(f"SetupServer:{exc}") from exc + + +def run(): + try: + setup_server() gMe['server'].serve_forever() except KeyboardInterrupt: print("INFO:Run:Shuting down...") diff --git a/tools/server/public_simplechat/local.tools/test-gen-self-signed.sh b/tools/server/public_simplechat/local.tools/test-gen-self-signed.sh new file mode 100755 index 0000000000..53183fbd61 --- /dev/null +++ b/tools/server/public_simplechat/local.tools/test-gen-self-signed.sh @@ -0,0 +1,3 @@ +openssl req -new -x509 -days 365 -noenc -out /tmp/test-cert.crt -keyout /tmp/test-priv.key -subj '/C=IN/ST=TEST/O=AnveshikaSallap/OU=SimpleProxyTEST/CN=127.0.0.1' -addext "subjectAltName = DNS:localhost, IP:127.0.0.1" +openssl x509 -in /tmp/test-cert.crt -text -noout +#openssl s_client -connect 127.0.0.1:3128 -showcerts diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index e33e0e2c11..4c74221158 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -31,6 +31,7 @@ cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config - `--debug True` enables debug mode which captures internet handshake data - port defaults to 3128, can be changed from simpleproxy.json, if needed +- add sec.keyfile and sec.certfile to simpleproxy.json, for https mode ### Client From e52a7aa3047dbe27b395be54b367a0626e1365c8 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 5 Dec 2025 01:53:12 +0530 Subject: [PATCH 408/446] SimpleSallap:SimpleProxy: MultiThreading Given that default HTTPServer handles only one connection and inturn request at any given time, so if a client opens connection and then doesnt do anything with it, it will block other clients by putting their requests into network queue for long. So to overcome the above issue switch to ThreadingHTTPServer, which starts a new thread for each request. Given that previously ssl wrapping was done wrt the main server socket, even with switching to ThreadingHTTPServer, the handshake for ssl/tls still occurs in the main thread before a child thread is started for parallel request handling, thus the ssl handshake phase blocking other client requests. So now avoid wrapping ssl wrt the main server socket, instead wait for ThreadingHttpServer to start the new thread for a client request ie after a connection is accepted for the client, before trying to wrap the connection in ssl. This ensures that the ssl handshake occurs in this child (ie client request related) thread. So some rogue entity opening a http connection and not doing ssl handshake wont block. Inturn in this case the rfile and wfile instances within the proxy handler need to be remapped to the new ssl wrapped socket. --- .../public_simplechat/local.tools/simpleproxy.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 2f83bbe214..23a7ac7e1c 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -36,7 +36,8 @@ gMe = { '--config': '/dev/null', '--debug': False, 'bearer.transformed.year': "", - 'server': None + 'server': None, + 'sslContext': None, } gConfigType = { @@ -164,6 +165,14 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): def handle(self) -> None: print(f"\n\n\nDBUG:ProxyHandler:Handle:RequestFrom:{self.client_address}") + try: + if (gMe['sslContext']): + self.request = gMe['sslContext'].wrap_socket(self.request, server_side=True) + self.rfile = self.request.makefile('rb', self.rbufsize) + self.wfile = self.request.makefile('wb', self.wbufsize) + except: + print(f"ERRR:ProxyHandler:SSLHS:{traceback.format_exception_only(sys.exception())}") + return return super().handle() @@ -279,13 +288,13 @@ def setup_server(): """ try: gMe['serverAddr'] = ('', gMe['--port']) - gMe['server'] = http.server.HTTPServer(gMe['serverAddr'], ProxyHandler) + gMe['server'] = http.server.ThreadingHTTPServer(gMe['serverAddr'], ProxyHandler) if gMe.get('--sec.keyfile') and gMe.get('--sec.certfile'): sslCtxt = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) sslCtxt.load_cert_chain(certfile=gMe['--sec.certfile'], keyfile=gMe['--sec.keyfile']) sslCtxt.minimum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED sslCtxt.maximum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED - gMe['server'].socket = sslCtxt.wrap_socket(gMe['server'].socket, server_side=True) + gMe['sslContext'] = sslCtxt print(f"INFO:SetupServer:Starting on {gMe['serverAddr']}:Https mode") else: print(f"INFO:SetupServer:Starting on {gMe['serverAddr']}:Http mode") From 05697afc1550e8d4488980c083fa99ab58bb989b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 5 Dec 2025 02:47:04 +0530 Subject: [PATCH 409/446] SimpleSallap:SimpleProxy:Trap all GET request handling otherwise aum path was not handled immidiately wrt exceptions. this also ensures any future changes wrt get request handling also get handled immidiately wrt exceptions, that may be missed by any targetted exception handling. --- tools/server/public_simplechat/docs/changelog.md | 4 ++++ .../public_simplechat/local.tools/simpleproxy.py | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index a3895a337e..7a1b7200d5 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -312,6 +312,10 @@ Chat Session specific settings out. In turn if the tool call times out, one can send the timeout message as the response to the tool call or what ever they see fit. Parallely, they can always look into the external ai specific special chat session tab to see the ai response live stream and the progress wrt the tool call that timed out. +* SimpleProxy + * add ssl ie https support and restrict it to latest supported ssl/tls version + * enable multi threaded ssl and client request handling, so that rogue clients cant mount simple DoS + by opening connection and then missing in action. ## ToDo diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 23a7ac7e1c..ca2ffaad28 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -132,7 +132,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): except Exception as e: self.send_error(400, f"ERRR:ProxyHandler:{e}") - def do_GET(self): + def _do_GET(self): """ Handle GET requests """ @@ -155,6 +155,16 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): print(f"WARN:ProxyHandler:GET:UnknownPath{pr.path}") self.send_error(400, f"WARN:UnknownPath:{pr.path}") + def do_GET(self): + """ + Catch all / trap any exceptions wrt actual get based request handling. + """ + try: + self._do_GET() + except: + print(f"ERRR:PH:TheGET:{traceback.format_exception_only(sys.exception())}") + self.send_error(500, f"ERRR: handling request") + def do_OPTIONS(self): """ Handle OPTIONS for CORS preflights (just in case from browser) @@ -164,6 +174,9 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): self.send_headers_common() def handle(self) -> None: + """ + Helps handle ssl setup in the client specific thread, if in https mode + """ print(f"\n\n\nDBUG:ProxyHandler:Handle:RequestFrom:{self.client_address}") try: if (gMe['sslContext']): From 4e7c7374d7628506c611af510efd9c9f6325753b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 5 Dec 2025 16:45:05 +0530 Subject: [PATCH 410/446] SimpleSallap:SimpleProxy:Make Config dataclass driven - p1 Instead of maintaining the config and some of the runtime states identified as gMe as a generic literal dictionary which grows at runtime with fields as required, try create it as a class of classes. Inturn use dataclass annotation to let biolerplate code get auto generated. A config module created with above, however remaining part of the code not yet updated to work with this new structure. process_args and load_config moved into the new Config class. --- .../public_simplechat/local.tools/config.py | 148 ++++++++++++++++++ .../local.tools/simpleproxy.py | 111 +------------ 2 files changed, 155 insertions(+), 104 deletions(-) create mode 100644 tools/server/public_simplechat/local.tools/config.py diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py new file mode 100644 index 0000000000..f452ade9af --- /dev/null +++ b/tools/server/public_simplechat/local.tools/config.py @@ -0,0 +1,148 @@ +# Config entries +# by Humans for All +# + +from dataclasses import dataclass +from typing import Any +import http.server +import ssl +import sys +import urlvalidator as mUV +import debug as mDebug + + +gConfigNeeded = [ 'acl.schemes', 'acl.domains', 'sec.bearer' ] + + +@dataclass(frozen=True) +class Sec(dict): + """ + Used to store security related config entries + """ + certFile: str = "" + keyFile: str = "" + bearerAuth: str = "" + +@dataclass +class ACL(dict): + schemes: list[str] = [] + domains: list[str] = [] + +@dataclass +class Network(dict): + port: int = 3128 + addr: str = '' + +@dataclass +class Op(dict): + configFile: str = "/dev/null" + debug: bool = False + server: http.server.ThreadingHTTPServer|None = None + sslContext: ssl.SSLContext|None = None + bearerTransformed: str = "" + bearerTransformedYear: str = "" + +@dataclass +class Config(dict): + op: Op = Op() + sec: Sec = Sec() + acl: ACL = ACL() + nw: Network = Network() + + def get_type(self, keyTree: str): + cKeyList = keyTree.split('.') + cur = self + for k in cKeyList[:-1]: + cur = self[k] + return type(cur[cKeyList[-1]]) + + def get_value(self, keyTree: str): + cKeyList = keyTree.split('.') + cur = self + for k in cKeyList[:-1]: + cur = self[k] + return cur[cKeyList[-1]] + + def set_value(self, keyTree: str, value: Any): + cKeyList = keyTree.split('.') + cur = self + for k in cKeyList[:-1]: + cur = self[k] + cur[cKeyList[-1]] = value + + def validate(self): + for k in gConfigNeeded: + if self.get_value(k) == None: + print(f"ERRR:ProcessArgs:Missing:{k}:did you forget to pass the config file...") + exit(104) + mDebug.setup(self.op.debug) + if (self.acl.schemes and self.acl.domains): + mUV.validator_setup(self.acl.schemes, self.acl.domains) + + def load_config(self, configFile: str): + """ + Allow loading of a json based config file + + The config entries should be named same as their equivalent cmdline argument + entries but without the -- prefix. + + As far as the logic is concerned the entries could either come from cmdline + or from a json based config file. + """ + import json + self.op.configFile = configFile + with open(self.op.configFile) as f: + cfgs: dict[str, Any] = json.load(f) + for cfg in cfgs: + print(f"DBUG:LoadConfig:{cfg}") + try: + neededType = self.get_type(cfg) + gotValue = cfgs[cfg] + gotType = type(gotValue) + if gotType.__name__ != neededType.__name__: + print(f"ERRR:LoadConfig:{cfg}:expected type [{neededType}] got type [{gotType}]") + exit(112) + self.set_value(cfg, gotValue) + except KeyError: + print(f"ERRR:LoadConfig:{cfg}:UnknownCommand") + exit(113) + + def process_args(self, args: list[str]): + """ + Helper to process command line arguments. + + Flow setup below such that + * location of --config in commandline will decide whether command line or config file will get + priority wrt setting program parameters. + * str type values in cmdline are picked up directly, without running them through ast.literal_eval, + bcas otherwise one will have to ensure throught the cmdline arg mechanism that string quote is + retained for literal_eval + """ + import ast + print(self) + iArg = 1 + while iArg < len(args): + cArg = args[iArg] + if (not cArg.startswith("--")): + print(f"ERRR:ProcessArgs:{iArg}:{cArg}:MalformedCommandOr???") + exit(101) + cArg = cArg[2:] + print(f"DBUG:ProcessArgs:{iArg}:{cArg}") + try: + aTypeCheck = self.get_type(cArg) + aValue = args[iArg+1] + if aTypeCheck.__name__ != 'str': + aValue = ast.literal_eval(aValue) + aType = type(aValue) + if aType.__name__ != aTypeCheck.__name__: + print(f"ERRR:ProcessArgs:{iArg}:{cArg}:expected type [{aTypeCheck}] got type [{aType}]") + exit(102) + self.set_value(cArg, aValue) + iArg += 2 + if cArg == 'op.configFile': + self.load_config(aValue) + except KeyError: + print(f"ERRR:ProcessArgs:{iArg}:{cArg}:UnknownCommand:{sys.exception()}") + exit(103) + print(self) + self.validate() diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index ca2ffaad28..cdaf6e4d38 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -25,33 +25,13 @@ import time import ssl import traceback from typing import Callable -import urlvalidator as uv import pdfmagic as mPdf import webmagic as mWeb -import debug as mDebug +import config as mConfig -gMe = { - '--port': 3128, - '--config': '/dev/null', - '--debug': False, - 'bearer.transformed.year': "", - 'server': None, - 'sslContext': None, -} +gMe = mConfig.Config() -gConfigType = { - '--port': 'int', - '--config': 'str', - '--debug': 'bool', - '--allowed.schemes': 'list', - '--allowed.domains': 'list', - '--bearer.insecure': 'str', - '--sec.keyfile': 'str', - '--sec.certfile': 'str' -} - -gConfigNeeded = [ '--allowed.schemes', '--allowed.domains', '--bearer.insecure' ] gAllowedCalls = { "xmlfiltered": [], @@ -68,13 +48,13 @@ def bearer_transform(): """ global gMe year = str(time.gmtime().tm_year) - if gMe['bearer.transformed.year'] == year: + if gMe.op.bearerTransformedYear == year: return import hashlib s256 = hashlib.sha256(year.encode('utf-8')) - s256.update(gMe['--bearer.insecure'].encode('utf-8')) - gMe['--bearer.transformed'] = s256.hexdigest() - gMe['bearer.transformed.year'] = year + s256.update(gMe.sec.bearerAuth.encode('utf-8')) + gMe.op.bearerTransformed = s256.hexdigest() + gMe.op.bearerTransformedYear = year class ProxyHandler(http.server.BaseHTTPRequestHandler): @@ -218,83 +198,6 @@ def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): ph.end_headers() -def load_config(): - """ - Allow loading of a json based config file - - The config entries should be named same as their equivalent cmdline argument - entries but without the -- prefix. They will be loaded into gMe after adding - -- prefix. - - As far as the program is concerned the entries could either come from cmdline - or from a json based config file. - """ - global gMe - import json - with open(gMe['--config']) as f: - cfg = json.load(f) - for k in cfg: - print(f"DBUG:LoadConfig:{k}") - try: - cArg = f"--{k}" - aTypeCheck = gConfigType[cArg] - aValue = cfg[k] - aType = type(aValue).__name__ - if aType != aTypeCheck: - print(f"ERRR:LoadConfig:{k}:expected type [{aTypeCheck}] got type [{aType}]") - exit(112) - gMe[cArg] = aValue - except KeyError: - print(f"ERRR:LoadConfig:{k}:UnknownCommand") - exit(113) - - -def process_args(args: list[str]): - """ - Helper to process command line arguments. - - Flow setup below such that - * location of --config in commandline will decide whether command line or config file will get - priority wrt setting program parameters. - * str type values in cmdline are picked up directly, without running them through ast.literal_eval, - bcas otherwise one will have to ensure throught the cmdline arg mechanism that string quote is - retained for literal_eval - """ - import ast - import json - global gMe - iArg = 1 - while iArg < len(args): - cArg = args[iArg] - if (not cArg.startswith("--")): - print(f"ERRR:ProcessArgs:{iArg}:{cArg}:MalformedCommandOr???") - exit(101) - print(f"DBUG:ProcessArgs:{iArg}:{cArg}") - try: - aTypeCheck = gConfigType[cArg] - aValue = args[iArg+1] - if aTypeCheck != 'str': - aValue = ast.literal_eval(aValue) - aType = type(aValue).__name__ - if aType != aTypeCheck: - print(f"ERRR:ProcessArgs:{iArg}:{cArg}:expected type [{aTypeCheck}] got type [{aType}]") - exit(102) - gMe[cArg] = aValue - iArg += 2 - if cArg == '--config': - load_config() - except KeyError: - print(f"ERRR:ProcessArgs:{iArg}:{cArg}:UnknownCommand") - exit(103) - print(json.dumps(gMe, indent=4)) - for k in gConfigNeeded: - if gMe.get(k) == None: - print(f"ERRR:ProcessArgs:{k}:missing, did you forget to pass the config file...") - exit(104) - mDebug.setup(gMe['--debug']) - uv.validator_setup(gMe['--allowed.schemes'], gMe['--allowed.domains']) - - def setup_server(): """ Helps setup a http/https server @@ -333,5 +236,5 @@ def run(): if __name__ == "__main__": - process_args(sys.argv) + gMe.process_args(sys.argv) run() From 55608400995fb509067f2bc916969219419ef259 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 5 Dec 2025 18:36:43 +0530 Subject: [PATCH 411/446] SimpleSallap:SimpleProxy:DataclassDict driven Config - p2 Assigning defaut values wrt compound type class members --- .../public_simplechat/local.tools/config.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index f452ade9af..0fe797a3ac 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -2,8 +2,8 @@ # by Humans for All # -from dataclasses import dataclass -from typing import Any +from dataclasses import dataclass, field +from typing import Any, Optional import http.server import ssl import sys @@ -25,8 +25,8 @@ class Sec(dict): @dataclass class ACL(dict): - schemes: list[str] = [] - domains: list[str] = [] + schemes: Optional[list[str]] = None + domains: list[str] = field(default_factory=list) @dataclass class Network(dict): @@ -44,10 +44,10 @@ class Op(dict): @dataclass class Config(dict): - op: Op = Op() - sec: Sec = Sec() - acl: ACL = ACL() - nw: Network = Network() + op: Op = field(default_factory=Op) + sec: Sec = field(default_factory=Sec) + acl: ACL = field(default_factory=ACL) + nw: Network = field(default_factory=Network) def get_type(self, keyTree: str): cKeyList = keyTree.split('.') From 4f790cb64620b26f6236054bf56c9f67df3e670b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 5 Dec 2025 20:47:33 +0530 Subject: [PATCH 412/446] SimpleSallap:SimpleProxy:DataClass Config - P3 Add a helper base class to try map data class's attributes into underlying dict. TODO: this potentially duplicates data in both normal attribute space as well as dict items space. And will require additional standard helper logics to be overridden to ensure sync between both space etal. Rather given distance from python internals for a long time now, on pausing and thinking a bit, better to move into a simpler arch where attributes are directly worked on for dict [] style access. --- .../public_simplechat/local.tools/config.py | 36 +++++++++++++------ .../local.tools/simpleproxy.json | 6 ++-- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index 0fe797a3ac..682bde1229 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -2,7 +2,7 @@ # by Humans for All # -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields from typing import Any, Optional import http.server import ssl @@ -11,11 +11,25 @@ import urlvalidator as mUV import debug as mDebug -gConfigNeeded = [ 'acl.schemes', 'acl.domains', 'sec.bearer' ] +gConfigNeeded = [ 'acl.schemes', 'acl.domains', 'sec.bearerAuth' ] + +@dataclass +class DictDataclassMixin(dict): + """ + Mixin to ensure dataclass attributes get mapped as dict keys in __post_init__ + """ + + def __post_init__(self): + """ + Skip if already set using maybe say dict init mechanism or ... + """ + for f in fields(self): + if f.name not in self: + self[f.name] = getattr(self, f.name) -@dataclass(frozen=True) -class Sec(dict): +@dataclass +class Sec(DictDataclassMixin, dict): """ Used to store security related config entries """ @@ -24,17 +38,17 @@ class Sec(dict): bearerAuth: str = "" @dataclass -class ACL(dict): - schemes: Optional[list[str]] = None +class ACL(DictDataclassMixin, dict): + schemes: list[str] = field(default_factory=list) domains: list[str] = field(default_factory=list) @dataclass -class Network(dict): +class Network(DictDataclassMixin, dict): port: int = 3128 addr: str = '' @dataclass -class Op(dict): +class Op(DictDataclassMixin, dict): configFile: str = "/dev/null" debug: bool = False server: http.server.ThreadingHTTPServer|None = None @@ -43,7 +57,7 @@ class Op(dict): bearerTransformedYear: str = "" @dataclass -class Config(dict): +class Config(DictDataclassMixin, dict): op: Op = field(default_factory=Op) sec: Sec = field(default_factory=Sec) acl: ACL = field(default_factory=ACL) @@ -104,7 +118,7 @@ class Config(dict): exit(112) self.set_value(cfg, gotValue) except KeyError: - print(f"ERRR:LoadConfig:{cfg}:UnknownCommand") + print(f"ERRR:LoadConfig:{cfg}:UnknownConfig!") exit(113) def process_args(self, args: list[str]): @@ -142,7 +156,7 @@ class Config(dict): if cArg == 'op.configFile': self.load_config(aValue) except KeyError: - print(f"ERRR:ProcessArgs:{iArg}:{cArg}:UnknownCommand:{sys.exception()}") + print(f"ERRR:ProcessArgs:{iArg}:{cArg}:UnknownArgCommand!:{sys.exception()}") exit(103) print(self) self.validate() diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simpleproxy.json index 77261bd1a2..1e3dcc67fc 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.json +++ b/tools/server/public_simplechat/local.tools/simpleproxy.json @@ -1,10 +1,10 @@ { - "allowed.schemes": [ + "acl.schemes": [ "file", "http", "https" ], - "allowed.domains": [ + "acl.domains": [ ".*\\.wikipedia\\.org$", ".*\\.bing\\.com$", "^www\\.bing\\.com$", @@ -51,5 +51,5 @@ "^github\\.com$", ".*\\.github\\.com$" ], - "bearer.insecure": "NeverSecure" + "sec.bearerAuth": "NeverSecure" } From 277225dddd4b8286e3a9c42eebe2752d5e29a886 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 5 Dec 2025 21:00:45 +0530 Subject: [PATCH 413/446] SimpleSallap:SimpleProxy:DataClass Config - p4 Minimal skeleton to allow dict [] style access to dataclass based class's attributes/fields. Also get member function similar to dict. This simplifies the flow and avoids duplicating data between attribute and dict data related name and data spaces. --- .../public_simplechat/local.tools/config.py | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index 682bde1229..0db0ed3611 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -13,23 +13,29 @@ import debug as mDebug gConfigNeeded = [ 'acl.schemes', 'acl.domains', 'sec.bearerAuth' ] + @dataclass -class DictDataclassMixin(dict): +class DictyDataclassMixin(): """ - Mixin to ensure dataclass attributes get mapped as dict keys in __post_init__ + Mixin to ensure dataclass attributes are also accessible through + dict's [] style syntax and get helper. """ - def __post_init__(self): - """ - Skip if already set using maybe say dict init mechanism or ... - """ - for f in fields(self): - if f.name not in self: - self[f.name] = getattr(self, f.name) + def __getitem__(self, key: str) -> Any: + return getattr(self, key) + + def __setitem__(self, key: str, value: Any) -> None: + setattr(self, key, value) + + def get(self, key, default=None): + try: + return self[key] + except: + return default @dataclass -class Sec(DictDataclassMixin, dict): +class Sec(DictyDataclassMixin): """ Used to store security related config entries """ @@ -37,18 +43,30 @@ class Sec(DictDataclassMixin, dict): keyFile: str = "" bearerAuth: str = "" + @dataclass -class ACL(DictDataclassMixin, dict): +class ACL(DictyDataclassMixin): + """ + Used to store access control related config entries + """ schemes: list[str] = field(default_factory=list) domains: list[str] = field(default_factory=list) + @dataclass -class Network(DictDataclassMixin, dict): +class Network(DictyDataclassMixin): + """ + Used to store network related config entries + """ port: int = 3128 addr: str = '' + @dataclass -class Op(DictDataclassMixin, dict): +class Op(DictyDataclassMixin): + """ + Used to store runtime operation related config entries and states + """ configFile: str = "/dev/null" debug: bool = False server: http.server.ThreadingHTTPServer|None = None @@ -56,8 +74,9 @@ class Op(DictDataclassMixin, dict): bearerTransformed: str = "" bearerTransformedYear: str = "" + @dataclass -class Config(DictDataclassMixin, dict): +class Config(DictyDataclassMixin): op: Op = field(default_factory=Op) sec: Sec = field(default_factory=Sec) acl: ACL = field(default_factory=ACL) From d470d7e47d0e191dde0b9e68f41df23ca9db5c83 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 5 Dec 2025 21:27:08 +0530 Subject: [PATCH 414/446] SimpleSallap:SimpleProxy:DataClass Config simpleproxy updated --- .../public_simplechat/docs/changelog.md | 1 + .../public_simplechat/local.tools/config.py | 3 ++ .../local.tools/simpleproxy.py | 31 ++++++++++--------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 7a1b7200d5..e67233bca8 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -316,6 +316,7 @@ Chat Session specific settings * add ssl ie https support and restrict it to latest supported ssl/tls version * enable multi threaded ssl and client request handling, so that rogue clients cant mount simple DoS by opening connection and then missing in action. + * switch to a Dicty DataClass based Config with better type validation and usage, instead of literal dict++ ## ToDo diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index 0db0ed3611..4e9e330bb6 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -61,6 +61,9 @@ class Network(DictyDataclassMixin): port: int = 3128 addr: str = '' + def server_address(self): + return (self.addr, self.port) + @dataclass class Op(DictyDataclassMixin): diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index cdaf6e4d38..75fe8e4113 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -95,7 +95,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): return { 'AllOk': False, 'Msg': "Invalid auth line" } if authlineA[0] != 'Bearer': return { 'AllOk': False, 'Msg': "Invalid auth type" } - if authlineA[1] != gMe['--bearer.transformed']: + if authlineA[1] != gMe.op.bearerTransformed: return { 'AllOk': False, 'Msg': "Invalid auth" } return { 'AllOk': True, 'Msg': "Auth Ok" } @@ -159,8 +159,8 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): """ print(f"\n\n\nDBUG:ProxyHandler:Handle:RequestFrom:{self.client_address}") try: - if (gMe['sslContext']): - self.request = gMe['sslContext'].wrap_socket(self.request, server_side=True) + if (gMe.op.sslContext): + self.request = gMe.op.sslContext.wrap_socket(self.request, server_side=True) self.rfile = self.request.makefile('rb', self.rbufsize) self.wfile = self.request.makefile('wb', self.wbufsize) except: @@ -203,17 +203,16 @@ def setup_server(): Helps setup a http/https server """ try: - gMe['serverAddr'] = ('', gMe['--port']) - gMe['server'] = http.server.ThreadingHTTPServer(gMe['serverAddr'], ProxyHandler) - if gMe.get('--sec.keyfile') and gMe.get('--sec.certfile'): + gMe.op.server = http.server.ThreadingHTTPServer(gMe.nw.server_address(), ProxyHandler) + if gMe.sec.get('keyFile') and gMe.sec.get('certFile'): sslCtxt = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - sslCtxt.load_cert_chain(certfile=gMe['--sec.certfile'], keyfile=gMe['--sec.keyfile']) + sslCtxt.load_cert_chain(certfile=gMe.sec.certFile, keyfile=gMe.sec.keyFile) sslCtxt.minimum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED sslCtxt.maximum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED - gMe['sslContext'] = sslCtxt - print(f"INFO:SetupServer:Starting on {gMe['serverAddr']}:Https mode") + gMe.op.sslContext = sslCtxt + print(f"INFO:SetupServer:Starting on {gMe.nw.server_address()}:Https mode") else: - print(f"INFO:SetupServer:Starting on {gMe['serverAddr']}:Http mode") + print(f"INFO:SetupServer:Starting on {gMe.nw.server_address()}:Http mode") except Exception as exc: print(f"ERRR:SetupServer:{traceback.format_exc()}") raise RuntimeError(f"SetupServer:{exc}") from exc @@ -222,16 +221,18 @@ def setup_server(): def run(): try: setup_server() - gMe['server'].serve_forever() + if not gMe.op.server: + raise RuntimeError("Server missing!!!") + gMe.op.server.serve_forever() except KeyboardInterrupt: print("INFO:Run:Shuting down...") - if (gMe['server']): - gMe['server'].server_close() + if gMe.op.server: + gMe.op.server.server_close() sys.exit(0) except Exception as exc: print(f"ERRR:Run:Exiting:Exception:{exc}") - if (gMe['server']): - gMe['server'].server_close() + if gMe.op.server: + gMe.op.server.server_close() sys.exit(1) From a52ac5dddeb3e9521289289089fd6b4f199f90f0 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Fri, 5 Dec 2025 22:28:55 +0530 Subject: [PATCH 415/446] SimpleSallap:SimpleProxy:use RequestHandler's setup after ssl hs Instead of manually setting up rfile and wfile after switching to ssl mode wrt a client request, now use the builtin setup provided by the RequestHandler logic, so that these and any other needed things will be setup as needed after the ssl hs based new socket, just in case new things are needed in future. --- tools/server/public_simplechat/local.tools/simpleproxy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/local.tools/simpleproxy.py index 75fe8e4113..036630a31f 100644 --- a/tools/server/public_simplechat/local.tools/simpleproxy.py +++ b/tools/server/public_simplechat/local.tools/simpleproxy.py @@ -161,8 +161,9 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): try: if (gMe.op.sslContext): self.request = gMe.op.sslContext.wrap_socket(self.request, server_side=True) - self.rfile = self.request.makefile('rb', self.rbufsize) - self.wfile = self.request.makefile('wb', self.wbufsize) + self.setup() + #self.rfile = self.request.makefile('rb', self.rbufsize) + #self.wfile = self.request.makefile('wb', self.wbufsize) except: print(f"ERRR:ProxyHandler:SSLHS:{traceback.format_exception_only(sys.exception())}") return From 452e6100956ed6695f926bceeb558b894b000369 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 00:08:31 +0530 Subject: [PATCH 416/446] SimpleSallap:SimpleMCP:Skeletons for a toolcall class --- .../public_simplechat/local.tools/toolcall.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tools/server/public_simplechat/local.tools/toolcall.py diff --git a/tools/server/public_simplechat/local.tools/toolcall.py b/tools/server/public_simplechat/local.tools/toolcall.py new file mode 100644 index 0000000000..f2568d03ec --- /dev/null +++ b/tools/server/public_simplechat/local.tools/toolcall.py @@ -0,0 +1,83 @@ +# Tool Call Base +# by Humans for All + +from typing import Any, TypeAlias +from dataclasses import dataclass + + +# +# A sample tool call meta +# + +fetchurlraw_meta = { + "type": "function", + "function": { + "name": "fetch_url_raw", + "description": "Fetch contents of the requested url (local file path / web based) through a proxy server and return the got content as is, in few seconds. Mainly useful for getting textual non binary contents", + "parameters": { + "type": "object", + "properties": { + "url":{ + "type":"string", + "description":"url of the local file / web content to fetch" + } + }, + "required": ["url"] + } + } + } + + +# +# Dataclasses to help with Tool Calls +# + +@dataclass +class TCInProperty(): + type: str + description: str + +TCInProperties: TypeAlias = dict[str, TCInProperty] + +@dataclass +class TCInParameters(): + type: str = "object" + properties: TCInProperties = {} + required: list[str] = [] + +@dataclass +class TCFunction(): + name: str + description: str + parameters: TCInParameters + +@dataclass +class ToolCallMeta(): + type: str = "function" + function: TCFunction|None = None + +@dataclass +class TollCallResponse(): + status: bool + tcid: str + name: str + content: str = "" + + +class ToolCall(): + + name: str = "" + + def tcf_meta(self) -> TCFunction|None: + return None + + def tc_handle(self, args: TCInProperties) -> tuple[bool, str]: + return (False, "") + + def meta(self) -> ToolCallMeta: + tcf = self.tcf_meta() + return ToolCallMeta("function", tcf) + + def handler(self, callId: str, args: Any) -> TollCallResponse: + got = self.tc_handle(args) + return TollCallResponse(got[0], callId, self.name, got[1]) From 47bd2bbc90e0155649a67ccb2526d03ab5f47dab Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 01:43:31 +0530 Subject: [PATCH 417/446] SimpleSallap:SimpleMCP:Update toolcall to suite calls needed --- .../public_simplechat/local.tools/toolcall.py | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/toolcall.py b/tools/server/public_simplechat/local.tools/toolcall.py index f2568d03ec..b281eb5b1f 100644 --- a/tools/server/public_simplechat/local.tools/toolcall.py +++ b/tools/server/public_simplechat/local.tools/toolcall.py @@ -3,6 +3,9 @@ from typing import Any, TypeAlias from dataclasses import dataclass +import http +import http.client +import urllib.parse # @@ -32,6 +35,8 @@ fetchurlraw_meta = { # Dataclasses to help with Tool Calls # +TCInArgs: TypeAlias = dict[str, Any] + @dataclass class TCInProperty(): type: str @@ -63,21 +68,32 @@ class TollCallResponse(): name: str content: str = "" +@dataclass(frozen=True) +class TCOutResponse: + """ + Used to return result from tool call. + """ + callOk: bool + statusCode: int + statusMsg: str = "" + contentType: str = "" + contentData: bytes = b"" + +@dataclass class ToolCall(): - - name: str = "" + name: str def tcf_meta(self) -> TCFunction|None: return None - def tc_handle(self, args: TCInProperties) -> tuple[bool, str]: - return (False, "") + def tc_handle(self, args: TCInArgs, inHeaders: http.client.HTTPMessage) -> TCOutResponse: + return TCOutResponse(False, 500) def meta(self) -> ToolCallMeta: tcf = self.tcf_meta() return ToolCallMeta("function", tcf) - def handler(self, callId: str, args: Any) -> TollCallResponse: - got = self.tc_handle(args) - return TollCallResponse(got[0], callId, self.name, got[1]) + def handler(self, callId: str, args: Any, inHeaders: http.client.HTTPMessage) -> TollCallResponse: + got = self.tc_handle(args, inHeaders) + return TollCallResponse(got.callOk, callId, self.name, got.contentData.decode('utf-8')) From cbb632eec0417214007ed8c1a0144f52d8840f86 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 01:45:56 +0530 Subject: [PATCH 418/446] SimpleSallap:SimpleMCP:TCWeb: Duplicate webmagic starting point To help with switching to tool call class++ based flow --- .../public_simplechat/local.tools/tcweb.py | 308 ++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 tools/server/public_simplechat/local.tools/tcweb.py diff --git a/tools/server/public_simplechat/local.tools/tcweb.py b/tools/server/public_simplechat/local.tools/tcweb.py new file mode 100644 index 0000000000..9bde105aba --- /dev/null +++ b/tools/server/public_simplechat/local.tools/tcweb.py @@ -0,0 +1,308 @@ +# Helper to manage web related requests +# by Humans for All + +import urllib.parse +import urlvalidator as uv +from dataclasses import dataclass +import html.parser +import debug +import filemagic as mFile +import json +import re +from typing import TYPE_CHECKING, Any, cast + +if TYPE_CHECKING: + from simpleproxy import ProxyHandler + + + +@dataclass(frozen=True) +class UrlReqResp: + """ + Used to return result wrt urlreq helper below. + """ + callOk: bool + httpStatus: int + httpStatusMsg: str = "" + contentType: str = "" + contentData: str = "" + + +def handle_urlreq(ph: 'ProxyHandler', pr: urllib.parse.ParseResult, tag: str): + """ + Common part of the url request handling used by both urlraw and urltext. + + Verify the url being requested is allowed. + + Include User-Agent, Accept-Language and Accept in the generated request using + equivalent values got in the request being proxied, so as to try mimic the + real client, whose request we are proxying. In case a header is missing in the + got request, fallback to using some possibly ok enough defaults. + + Fetch the requested url. + """ + tag=f"UrlReq:{tag}" + queryParams = urllib.parse.parse_qs(pr.query) + url = queryParams['url'] + print(f"DBUG:{tag}:Url:{url}") + url = url[0] + gotVU = uv.validate_url(url, tag) + if not gotVU.callOk: + return UrlReqResp(gotVU.callOk, gotVU.statusCode, gotVU.statusMsg) + try: + hUA = ph.headers.get('User-Agent', None) + hAL = ph.headers.get('Accept-Language', None) + hA = ph.headers.get('Accept', None) + headers = { + 'User-Agent': hUA, + 'Accept': hA, + 'Accept-Language': hAL + } + # Get requested url + gotFile = mFile.get_file(url, tag, "text/html", headers) + return UrlReqResp(gotFile.callOk, gotFile.statusCode, gotFile.statusMsg, gotFile.contentType, gotFile.contentData.decode('utf-8')) + except Exception as exc: + return UrlReqResp(False, 502, f"WARN:{tag}:Failed:{exc}") + + +def handle_urlraw(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): + try: + # Get requested url + got = handle_urlreq(ph, pr, "HandleUrlRaw") + if not got.callOk: + ph.send_error(got.httpStatus, got.httpStatusMsg) + return + # Send back to client + ph.send_response(got.httpStatus) + ph.send_header('Content-Type', got.contentType) + # Add CORS for browser fetch, just in case + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + ph.wfile.write(got.contentData.encode('utf-8')) + except Exception as exc: + ph.send_error(502, f"WARN:UrlRawFailed:{exc}") + + +class TextHtmlParser(html.parser.HTMLParser): + """ + A simple minded logic used to strip html content of + * all the html tags as well as + * all the contents belonging to below predefined tags like script, style, header, ... + + NOTE: if the html content/page uses any javascript for client side manipulation/generation of + html content, that logic wont be triggered, so also such client side dynamic content wont be + got. + + Supports one to specify a list of tags and their corresponding id attributes, so that contents + within such specified blocks will be dropped. + + * this works properly only if the html being processed has proper opening and ending tags + around the area of interest. + * remember to specify non overlapping tag blocks, if more than one specified for dropping. + * this path not tested, but should logically work + + This helps return a relatively clean textual representation of the html file/content being parsed. + """ + + def __init__(self, tagDrops: list[dict[str, Any]]): + super().__init__() + self.tagDrops = tagDrops + print(f"DBUG:TextHtmlParser:{self.tagDrops}") + self.inside = { + 'body': False, + 'script': False, + 'style': False, + 'header': False, + 'footer': False, + 'nav': False, + } + self.monitored = [ 'body', 'script', 'style', 'header', 'footer', 'nav' ] + self.bCapture = False + self.text = "" + self.textStripped = "" + self.droptagType = None + self.droptagCount = 0 + + def do_capture(self): + """ + Helps decide whether to capture contents or discard them. + """ + if self.inside['body'] and not (self.inside['script'] or self.inside['style'] or self.inside['header'] or self.inside['footer'] or self.inside['nav'] or (self.droptagCount > 0)): + return True + return False + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]): + if tag in self.monitored: + self.inside[tag] = True + for tagMeta in self.tagDrops: + if tag != tagMeta['tag']: + continue + if (self.droptagCount > 0) and (self.droptagType == tag): + self.droptagCount += 1 + continue + for attr in attrs: + if attr[0] != 'id': + continue + if attr[1] == tagMeta['id']: + self.droptagCount += 1 + self.droptagType = tag + print(f"DBUG:THP:Start:Tag found [{tag}:{attr[1]}]...") + + def handle_endtag(self, tag: str): + if tag in self.monitored: + self.inside[tag] = False + if self.droptagType and (tag == self.droptagType): + self.droptagCount -= 1 + if self.droptagCount == 0: + self.droptagType = None + print("DBUG:THP:End:Tag found...") + if self.droptagCount < 0: + self.droptagCount = 0 + + def handle_data(self, data: str): + if self.do_capture(): + self.text += f"{data}\n" + + def syncup(self): + self.textStripped = self.text + + def strip_adjacent_newlines(self): + oldLen = -99 + newLen = len(self.textStripped) + aStripped = self.textStripped; + while oldLen != newLen: + oldLen = newLen + aStripped = aStripped.replace("\n\n\n","\n") + newLen = len(aStripped) + self.textStripped = aStripped + + def strip_whitespace_lines(self): + aLines = self.textStripped.splitlines() + self.textStripped = "" + for line in aLines: + if (len(line.strip())==0): + self.textStripped += "\n" + continue + self.textStripped += f"{line}\n" + + def get_stripped_text(self): + self.syncup() + self.strip_whitespace_lines() + self.strip_adjacent_newlines() + return self.textStripped + + +def handle_htmltext(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): + try: + # Get requested url + got = handle_urlreq(ph, pr, "HandleHtmlText") + if not got.callOk: + ph.send_error(got.httpStatus, got.httpStatusMsg) + return + # Extract Text + tagDrops = ph.headers.get('htmltext-tag-drops') + if not tagDrops: + tagDrops = [] + else: + tagDrops = cast(list[dict[str,Any]], json.loads(tagDrops)) + textHtml = TextHtmlParser(tagDrops) + textHtml.feed(got.contentData) + # Send back to client + ph.send_response(got.httpStatus) + ph.send_header('Content-Type', got.contentType) + # Add CORS for browser fetch, just in case + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + ph.wfile.write(textHtml.get_stripped_text().encode('utf-8')) + debug.dump({ 'op': 'WebMagic.HtmlText', 'RawText': 'yes', 'StrippedText': 'yes' }, { 'RawText': textHtml.text, 'StrippedText': textHtml.get_stripped_text() }) + except Exception as exc: + ph.send_error(502, f"WARN:HtmlText:Failed:{exc}") + + +class XMLFilterParser(html.parser.HTMLParser): + """ + A simple minded logic used to strip xml content of + * unwanted tags and their contents, using re + * this works properly only if the xml being processed has + proper opening and ending tags around the area of interest. + + This can help return a cleaned up xml file. + """ + + def __init__(self, tagDropREs: list[str]): + """ + tagDropREs - allows one to specify a list of tags related REs, + to help drop the corresponding tags and their contents fully. + + To drop a tag, specify regular expression + * that matches the corresponding heirarchy of tags involved + * where the tag names should be in lower case and suffixed with : + * if interested in dropping a tag independent of where it appears use + ".*:tagname:.*" re template + """ + super().__init__() + self.tagDropREs = list(map(str.lower, tagDropREs)) + print(f"DBUG:XMLFilterParser:{self.tagDropREs}") + self.text = "" + self.prefixTags = [] + self.prefix = "" + self.lastTrackedCB = "" + + def do_capture(self): + """ + Helps decide whether to capture contents or discard them. + """ + curTagH = f'{":".join(self.prefixTags)}:' + for dropRE in self.tagDropREs: + if re.match(dropRE, curTagH): + return False + return True + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]): + self.prefixTags.append(tag) + if not self.do_capture(): + return + self.lastTrackedCB = "starttag" + self.prefix += "\t" + self.text += f"\n{self.prefix}<{tag}>" + + def handle_endtag(self, tag: str): + if self.do_capture(): + if (self.lastTrackedCB == "endtag"): + self.text += f"\n{self.prefix}" + else: + self.text += f"" + self.lastTrackedCB = "endtag" + self.prefix = self.prefix[:-1] + self.prefixTags.pop() + + def handle_data(self, data: str): + if self.do_capture(): + self.text += f"{data}" + + +def handle_xmlfiltered(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): + try: + # Get requested url + got = handle_urlreq(ph, pr, "HandleXMLFiltered") + if not got.callOk: + ph.send_error(got.httpStatus, got.httpStatusMsg) + return + # Extract Text + tagDropREs = ph.headers.get('xmlfiltered-tagdrop-res') + if not tagDropREs: + tagDropREs = [] + else: + tagDropREs = cast(list[str], json.loads(tagDropREs)) + xmlFiltered = XMLFilterParser(tagDropREs) + xmlFiltered.feed(got.contentData) + # Send back to client + ph.send_response(got.httpStatus) + ph.send_header('Content-Type', got.contentType) + # Add CORS for browser fetch, just in case + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + ph.wfile.write(xmlFiltered.text.encode('utf-8')) + debug.dump({ 'XMLFiltered': 'yes' }, { 'RawText': xmlFiltered.text }) + except Exception as exc: + ph.send_error(502, f"WARN:XMLFiltered:Failed:{exc}") From b17cd18bc5ba9a11273f3d41bd6eff5403bfa58b Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 01:54:56 +0530 Subject: [PATCH 419/446] SimpleSallap:SimpleMCP:TCWeb:Update TCUrlRaw + Helper Now generic handle_urlreq and handle_urlraw updated to work with the new ToolCall flow --- .../public_simplechat/local.tools/tcweb.py | 78 +++++++++---------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/tcweb.py b/tools/server/public_simplechat/local.tools/tcweb.py index 9bde105aba..ed1dad3aee 100644 --- a/tools/server/public_simplechat/local.tools/tcweb.py +++ b/tools/server/public_simplechat/local.tools/tcweb.py @@ -3,32 +3,18 @@ import urllib.parse import urlvalidator as uv -from dataclasses import dataclass import html.parser import debug import filemagic as mFile import json import re -from typing import TYPE_CHECKING, Any, cast - -if TYPE_CHECKING: - from simpleproxy import ProxyHandler +import http.client +from typing import Any, cast +import toolcall as mTC -@dataclass(frozen=True) -class UrlReqResp: - """ - Used to return result wrt urlreq helper below. - """ - callOk: bool - httpStatus: int - httpStatusMsg: str = "" - contentType: str = "" - contentData: str = "" - - -def handle_urlreq(ph: 'ProxyHandler', pr: urllib.parse.ParseResult, tag: str): +def handle_urlreq(url: str, inHeaders: http.client.HTTPMessage, tag: str): """ Common part of the url request handling used by both urlraw and urltext. @@ -42,17 +28,14 @@ def handle_urlreq(ph: 'ProxyHandler', pr: urllib.parse.ParseResult, tag: str): Fetch the requested url. """ tag=f"UrlReq:{tag}" - queryParams = urllib.parse.parse_qs(pr.query) - url = queryParams['url'] print(f"DBUG:{tag}:Url:{url}") - url = url[0] gotVU = uv.validate_url(url, tag) if not gotVU.callOk: - return UrlReqResp(gotVU.callOk, gotVU.statusCode, gotVU.statusMsg) + return mTC.TCOutResponse(gotVU.callOk, gotVU.statusCode, gotVU.statusMsg) try: - hUA = ph.headers.get('User-Agent', None) - hAL = ph.headers.get('Accept-Language', None) - hA = ph.headers.get('Accept', None) + hUA = inHeaders.get('User-Agent', None) + hAL = inHeaders.get('Accept-Language', None) + hA = inHeaders.get('Accept', None) headers = { 'User-Agent': hUA, 'Accept': hA, @@ -60,27 +43,36 @@ def handle_urlreq(ph: 'ProxyHandler', pr: urllib.parse.ParseResult, tag: str): } # Get requested url gotFile = mFile.get_file(url, tag, "text/html", headers) - return UrlReqResp(gotFile.callOk, gotFile.statusCode, gotFile.statusMsg, gotFile.contentType, gotFile.contentData.decode('utf-8')) + return mTC.TCOutResponse(gotFile.callOk, gotFile.statusCode, gotFile.statusMsg, gotFile.contentType, gotFile.contentData) except Exception as exc: - return UrlReqResp(False, 502, f"WARN:{tag}:Failed:{exc}") + return mTC.TCOutResponse(False, 502, f"WARN:{tag}:Failed:{exc}") -def handle_urlraw(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): - try: - # Get requested url - got = handle_urlreq(ph, pr, "HandleUrlRaw") - if not got.callOk: - ph.send_error(got.httpStatus, got.httpStatusMsg) - return - # Send back to client - ph.send_response(got.httpStatus) - ph.send_header('Content-Type', got.contentType) - # Add CORS for browser fetch, just in case - ph.send_header('Access-Control-Allow-Origin', '*') - ph.end_headers() - ph.wfile.write(got.contentData.encode('utf-8')) - except Exception as exc: - ph.send_error(502, f"WARN:UrlRawFailed:{exc}") +class TCUrlRaw(mTC.ToolCall): + + def tcf_meta(self) -> mTC.TCFunction: + return mTC.TCFunction( + self.name, + "Fetch contents of the requested url (local file path / web based) through a proxy server and return the got content as is, in few seconds. Mainly useful for getting textual non binary contents", + mTC.TCInParameters( + "object", + { + "url": mTC.TCInProperty( + "string", + "url of the local file / web content to fetch" + ) + }, + [ "url" ] + ) + ) + + def tc_handle(self, args: mTC.TCInArgs, inHeaders: http.client.HTTPMessage) -> mTC.TCOutResponse: + try: + # Get requested url + got = handle_urlreq(args['url'], inHeaders, "HandleTCUrlRaw") + return got + except Exception as exc: + return mTC.TCOutResponse(False, 502, f"WARN:UrlRawFailed:{exc}") class TextHtmlParser(html.parser.HTMLParser): From 5bf608dedd39aeb7524a793295309351f211c4fe Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 02:17:26 +0530 Subject: [PATCH 420/446] SimpleSallap:SimpleMCP:TCWeb:HtmlText updated for new flow Rather initial go at the new flow, things require to be tweaked later wrt final valid runnable flow --- .../public_simplechat/local.tools/tcweb.py | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/tcweb.py b/tools/server/public_simplechat/local.tools/tcweb.py index ed1dad3aee..b5b20395ba 100644 --- a/tools/server/public_simplechat/local.tools/tcweb.py +++ b/tools/server/public_simplechat/local.tools/tcweb.py @@ -72,7 +72,7 @@ class TCUrlRaw(mTC.ToolCall): got = handle_urlreq(args['url'], inHeaders, "HandleTCUrlRaw") return got except Exception as exc: - return mTC.TCOutResponse(False, 502, f"WARN:UrlRawFailed:{exc}") + return mTC.TCOutResponse(False, 502, f"WARN:UrlRaw:Failed:{exc}") class TextHtmlParser(html.parser.HTMLParser): @@ -184,31 +184,42 @@ class TextHtmlParser(html.parser.HTMLParser): return self.textStripped -def handle_htmltext(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): - try: - # Get requested url - got = handle_urlreq(ph, pr, "HandleHtmlText") - if not got.callOk: - ph.send_error(got.httpStatus, got.httpStatusMsg) - return - # Extract Text - tagDrops = ph.headers.get('htmltext-tag-drops') - if not tagDrops: - tagDrops = [] - else: - tagDrops = cast(list[dict[str,Any]], json.loads(tagDrops)) - textHtml = TextHtmlParser(tagDrops) - textHtml.feed(got.contentData) - # Send back to client - ph.send_response(got.httpStatus) - ph.send_header('Content-Type', got.contentType) - # Add CORS for browser fetch, just in case - ph.send_header('Access-Control-Allow-Origin', '*') - ph.end_headers() - ph.wfile.write(textHtml.get_stripped_text().encode('utf-8')) - debug.dump({ 'op': 'WebMagic.HtmlText', 'RawText': 'yes', 'StrippedText': 'yes' }, { 'RawText': textHtml.text, 'StrippedText': textHtml.get_stripped_text() }) - except Exception as exc: - ph.send_error(502, f"WARN:HtmlText:Failed:{exc}") +class TCHtmlText(mTC.ToolCall): + + def tcf_meta(self) -> mTC.TCFunction: + return mTC.TCFunction( + self.name, + "Fetch html content from given url through a proxy server and return its text content after stripping away the html tags as well as head, script, style, header, footer, nav blocks, in few seconds", + mTC.TCInParameters( + "object", + { + "url": mTC.TCInProperty( + "string", + "url of the html page that needs to be fetched and inturn unwanted stuff stripped from its contents to some extent" + ) + }, + [ "url" ] + ) + ) + + def tc_handle(self, args: mTC.TCInArgs, inHeaders: http.client.HTTPMessage) -> mTC.TCOutResponse: + try: + # Get requested url + got = handle_urlreq(args['url'], inHeaders, "HandleTCHtmlText") + if not got.callOk: + return got + # Extract Text + tagDrops = inHeaders.get('htmltext-tag-drops') + if not tagDrops: + tagDrops = [] + else: + tagDrops = cast(list[dict[str,Any]], json.loads(tagDrops)) + textHtml = TextHtmlParser(tagDrops) + textHtml.feed(got.contentData.decode('utf-8')) + debug.dump({ 'op': 'MCPWeb.HtmlText', 'RawText': 'yes', 'StrippedText': 'yes' }, { 'RawText': textHtml.text, 'StrippedText': textHtml.get_stripped_text() }) + return mTC.TCOutResponse(True, got.statusCode, got.statusMsg, got.contentType, textHtml.get_stripped_text().encode('utf-8')) + except Exception as exc: + return mTC.TCOutResponse(False, 502, f"WARN:HtmlText:Failed:{exc}") class XMLFilterParser(html.parser.HTMLParser): From 66038f99cf6c0d8e17fee961e1329b6c20ec0a57 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 02:47:16 +0530 Subject: [PATCH 421/446] SimpleSallap:SimpleMCP:TCWeb:XMLFiltered initial go wrt new flow Also remember to picks the tagDropREs from passed args object and not from got http header. Even TCHtmlText updated to get the tags to drop from passed args object and not got http header. And inturn allow ai to pass this optional arg, as it sees fit in co-ordination with user. --- .../public_simplechat/local.tools/tcweb.py | 95 +++++++++++++------ 1 file changed, 67 insertions(+), 28 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/tcweb.py b/tools/server/public_simplechat/local.tools/tcweb.py index b5b20395ba..a80789fed5 100644 --- a/tools/server/public_simplechat/local.tools/tcweb.py +++ b/tools/server/public_simplechat/local.tools/tcweb.py @@ -184,18 +184,29 @@ class TextHtmlParser(html.parser.HTMLParser): return self.textStripped +gTagDropsHTMLTextSample = [ { 'tag': 'div', 'id': "header" } ] + class TCHtmlText(mTC.ToolCall): def tcf_meta(self) -> mTC.TCFunction: return mTC.TCFunction( self.name, - "Fetch html content from given url through a proxy server and return its text content after stripping away the html tags as well as head, script, style, header, footer, nav blocks, in few seconds", + "Fetch html content from given url through a proxy server and return its text content after stripping away html tags as well as uneeded blocks like head, script, style, header, footer, nav in few seconds", mTC.TCInParameters( "object", { "url": mTC.TCInProperty( "string", - "url of the html page that needs to be fetched and inturn unwanted stuff stripped from its contents to some extent" + "url of the html page that needs to be fetched and inturn unwanted stuff stripped from its contents to an extent" + ), + "tagDrops": mTC.TCInProperty( + "string", + ( + "Optionally specify a json stringified list of tag-and-id dicts of tag blocks to drop from html." + "For each tag block that needs to be dropped, one needs to specify the tag type and its associated id attribute." + "where the tag types (ie div, span, p, a ...) are always mentioned in lower case." + f"For example when fetching a search web site, one could use {json.dumps(gTagDropsHTMLTextSample)} and so..." + ) ) }, [ "url" ] @@ -209,7 +220,7 @@ class TCHtmlText(mTC.ToolCall): if not got.callOk: return got # Extract Text - tagDrops = inHeaders.get('htmltext-tag-drops') + tagDrops = args.get('tagDrops') if not tagDrops: tagDrops = [] else: @@ -284,28 +295,56 @@ class XMLFilterParser(html.parser.HTMLParser): self.text += f"{data}" -def handle_xmlfiltered(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): - try: - # Get requested url - got = handle_urlreq(ph, pr, "HandleXMLFiltered") - if not got.callOk: - ph.send_error(got.httpStatus, got.httpStatusMsg) - return - # Extract Text - tagDropREs = ph.headers.get('xmlfiltered-tagdrop-res') - if not tagDropREs: - tagDropREs = [] - else: - tagDropREs = cast(list[str], json.loads(tagDropREs)) - xmlFiltered = XMLFilterParser(tagDropREs) - xmlFiltered.feed(got.contentData) - # Send back to client - ph.send_response(got.httpStatus) - ph.send_header('Content-Type', got.contentType) - # Add CORS for browser fetch, just in case - ph.send_header('Access-Control-Allow-Origin', '*') - ph.end_headers() - ph.wfile.write(xmlFiltered.text.encode('utf-8')) - debug.dump({ 'XMLFiltered': 'yes' }, { 'RawText': xmlFiltered.text }) - except Exception as exc: - ph.send_error(502, f"WARN:XMLFiltered:Failed:{exc}") +gRSSTagDropREsDefault = [ + "^rss:channel:item:guid:.*", + "^rss:channel:item:link:.*", + "^rss:channel:item:description:.*", + ".*:image:.*", + ".*:enclosure:.*" +] + +class TCXmlFiltered(mTC.ToolCall): + + def tcf_meta(self) -> mTC.TCFunction: + return mTC.TCFunction( + self.name, + "Fetch requested xml url through a proxy server that can optionally filter out unwanted tags and their contents. Will take few seconds", + mTC.TCInParameters( + "object", + { + "url": mTC.TCInProperty( + "string", + "url of the xml file that will be fetched" + ), + "tagDropREs": mTC.TCInProperty( + "string", + ( + "Optionally specify a json stringified list of xml tag heirarchies to drop." + "For each tag that needs to be dropped, one needs to specify regular expression of the heirarchy of tags involved," + "where the tag names are always mentioned in lower case along with a : as suffix." + f"For example for rss feeds one could use {json.dumps(gRSSTagDropREsDefault)} and so..." + ) + ) + }, + [ "url" ] + ) + ) + + def tc_handle(self, args: mTC.TCInArgs, inHeaders: http.client.HTTPMessage) -> mTC.TCOutResponse: + try: + # Get requested url + got = handle_urlreq(args['url'], inHeaders, "HandleTCXMLFiltered") + if not got.callOk: + return got + # Extract Text + tagDropREs = args.get('tagDropREs') + if not tagDropREs: + tagDropREs = [] + else: + tagDropREs = cast(list[str], json.loads(tagDropREs)) + xmlFiltered = XMLFilterParser(tagDropREs) + xmlFiltered.feed(got.contentData.decode('utf-8')) + debug.dump({ 'op': 'MCPWeb.XMLFiltered' }, { 'RawText': xmlFiltered.text }) + return mTC.TCOutResponse(True, got.statusCode, got.statusMsg, got.contentType, xmlFiltered.text.encode('utf-8')) + except Exception as exc: + return mTC.TCOutResponse(False, 502, f"WARN:XMLFiltered:Failed:{exc}") From 01a7800f51bd8404c9a0b483d7663f0a7e6f540a Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 17:57:26 +0530 Subject: [PATCH 422/446] SimpleSallap:SimpleMCP:TCPdf: Duplicate pdfmagic --- .../public_simplechat/local.tools/tcpdf.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tools/server/public_simplechat/local.tools/tcpdf.py diff --git a/tools/server/public_simplechat/local.tools/tcpdf.py b/tools/server/public_simplechat/local.tools/tcpdf.py new file mode 100644 index 0000000000..963d3d6ecf --- /dev/null +++ b/tools/server/public_simplechat/local.tools/tcpdf.py @@ -0,0 +1,94 @@ +# Helper to manage pdf related requests +# by Humans for All + +import urllib.parse +import urlvalidator as uv +import filemagic as mFile +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from simpleproxy import ProxyHandler + + +PDFOUTLINE_MAXDEPTH=4 + + +def extract_pdfoutline(ol: Any, prefix: list[int]): + """ + Helps extract the pdf outline recursively, along with its numbering. + """ + if (len(prefix) > PDFOUTLINE_MAXDEPTH): + return "" + if type(ol).__name__ != type([]).__name__: + prefix[-1] += 1 + return f"{".".join(map(str,prefix))}:{ol['/Title']}\n" + olText = "" + prefix.append(0) + for (i,iol) in enumerate(ol): + olText += extract_pdfoutline(iol, prefix) + prefix.pop() + return olText + + +def process_pdftext(url: str, startPN: int, endPN: int): + """ + Extract textual content from given pdf. + + * Validate the got url. + * Get the pdf file. + * Extract textual contents of the pdf from given start page number to end page number (inclusive). + * if -1 | 0 is specified wrt startPN, the actual starting page number (rather 1) will be used. + * if -1 | 0 is specified wrt endPN, the actual ending page number will be used. + + NOTE: Page numbers start from 1, while the underlying list data structure index starts from 0 + """ + import pypdf + import io + gotVU = uv.validate_url(url, "HandlePdfText") + if not gotVU.callOk: + return { 'status': gotVU.statusCode, 'msg': gotVU.statusMsg } + gotFile = mFile.get_file(url, "ProcessPdfText", "application/pdf", {}) + if not gotFile.callOk: + return { 'status': gotFile.statusCode, 'msg': gotFile.statusMsg, 'data': gotFile.contentData} + tPdf = "" + oPdf = pypdf.PdfReader(io.BytesIO(gotFile.contentData)) + if (startPN <= 0): + startPN = 1 + if (endPN <= 0) or (endPN > len(oPdf.pages)): + endPN = len(oPdf.pages) + # Add the pdf outline, if available + outlineGot = extract_pdfoutline(oPdf.outline, []) + if outlineGot: + tPdf += f"\n\nOutline Start\n\n{outlineGot}\n\nOutline End\n\n" + # Add the pdf page contents + for i in range(startPN, endPN+1): + pd = oPdf.pages[i-1] + tPdf = tPdf + pd.extract_text() + return { 'status': 200, 'msg': "PdfText Response follows", 'data': tPdf } + + +def handle_pdftext(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): + """ + Handle requests to pdftext path, which is used to extract plain text + from the specified pdf file. + """ + queryParams = urllib.parse.parse_qs(pr.query) + url = queryParams['url'][0] + startP = queryParams.get('startPageNumber', -1) + if isinstance(startP, list): + startP = int(startP[0]) + endP = queryParams.get('endPageNumber', -1) + if isinstance(endP, list): + endP = int(endP[0]) + print(f"INFO:HandlePdfText:Processing:{url}:{startP}:{endP}...") + gotP2T = process_pdftext(url, startP, endP) + if (gotP2T['status'] != 200): + ph.send_error(gotP2T['status'], gotP2T['msg'] ) + return + ph.send_response(gotP2T['status'], gotP2T['msg']) + ph.send_header('Content-Type', 'text/text') + # Add CORS for browser fetch, just in case + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + print(f"INFO:HandlePdfText:ExtractedText:{url}...") + ph.wfile.write(gotP2T['data'].encode('utf-8')) From 4ce55eb0afe25e6b06c955f51838043e0599503d Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 15:27:13 +0530 Subject: [PATCH 423/446] SimpleSallap:SimpleMCP:TCPdf: update Implement pdftext around toolcall class++ flow --- .../public_simplechat/local.tools/tcpdf.py | 77 +++++++++++-------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/tcpdf.py b/tools/server/public_simplechat/local.tools/tcpdf.py index 963d3d6ecf..1d305f4f2b 100644 --- a/tools/server/public_simplechat/local.tools/tcpdf.py +++ b/tools/server/public_simplechat/local.tools/tcpdf.py @@ -4,11 +4,10 @@ import urllib.parse import urlvalidator as uv import filemagic as mFile +import toolcall as mTC +import http.client from typing import TYPE_CHECKING, Any -if TYPE_CHECKING: - from simpleproxy import ProxyHandler - PDFOUTLINE_MAXDEPTH=4 @@ -44,12 +43,12 @@ def process_pdftext(url: str, startPN: int, endPN: int): """ import pypdf import io - gotVU = uv.validate_url(url, "HandlePdfText") + gotVU = uv.validate_url(url, "ProcessPdfText") if not gotVU.callOk: - return { 'status': gotVU.statusCode, 'msg': gotVU.statusMsg } + return mTC.TCOutResponse(False, gotVU.statusCode, gotVU.statusMsg) gotFile = mFile.get_file(url, "ProcessPdfText", "application/pdf", {}) if not gotFile.callOk: - return { 'status': gotFile.statusCode, 'msg': gotFile.statusMsg, 'data': gotFile.contentData} + return mTC.TCOutResponse(False, gotFile.statusCode, gotFile.statusMsg, gotFile.contentType, gotFile.contentData) tPdf = "" oPdf = pypdf.PdfReader(io.BytesIO(gotFile.contentData)) if (startPN <= 0): @@ -64,31 +63,45 @@ def process_pdftext(url: str, startPN: int, endPN: int): for i in range(startPN, endPN+1): pd = oPdf.pages[i-1] tPdf = tPdf + pd.extract_text() - return { 'status': 200, 'msg': "PdfText Response follows", 'data': tPdf } + return mTC.TCOutResponse(True, 200, "PdfText Response follows", "text/text", tPdf.encode('utf-8')) -def handle_pdftext(ph: 'ProxyHandler', pr: urllib.parse.ParseResult): - """ - Handle requests to pdftext path, which is used to extract plain text - from the specified pdf file. - """ - queryParams = urllib.parse.parse_qs(pr.query) - url = queryParams['url'][0] - startP = queryParams.get('startPageNumber', -1) - if isinstance(startP, list): - startP = int(startP[0]) - endP = queryParams.get('endPageNumber', -1) - if isinstance(endP, list): - endP = int(endP[0]) - print(f"INFO:HandlePdfText:Processing:{url}:{startP}:{endP}...") - gotP2T = process_pdftext(url, startP, endP) - if (gotP2T['status'] != 200): - ph.send_error(gotP2T['status'], gotP2T['msg'] ) - return - ph.send_response(gotP2T['status'], gotP2T['msg']) - ph.send_header('Content-Type', 'text/text') - # Add CORS for browser fetch, just in case - ph.send_header('Access-Control-Allow-Origin', '*') - ph.end_headers() - print(f"INFO:HandlePdfText:ExtractedText:{url}...") - ph.wfile.write(gotP2T['data'].encode('utf-8')) +class TCPdfText(mTC.ToolCall): + + def tcf_meta(self) -> mTC.TCFunction: + return mTC.TCFunction( + self.name, + "Fetch pdf from requested local file path / web url through a proxy server and return its text content after converting pdf to text, in few seconds. One is allowed to get a part of the pdf by specifying the starting and ending page numbers", + mTC.TCInParameters( + "object", + { + "url": mTC.TCInProperty( + "string", + "local file path (file://) / web (http/https) based url of the pdf that will be got and inturn converted to text" + ), + "startPageNumber": mTC.TCInProperty( + "integer", + "Specify the starting page number within the pdf, this is optional. If not specified set to first page." + ), + "endPageNumber": mTC.TCInProperty( + "integer", + "Specify the ending page number within the pdf, this is optional. If not specified set to the last page." + ) + }, + [ "url" ] + ) + ) + + def tc_handle(self, args: mTC.TCInArgs, inHeaders: http.client.HTTPMessage) -> mTC.TCOutResponse: + """ + Handle pdftext request, + which is used to extract plain text from the specified pdf file. + """ + try: + url = args['url'] + startP = int(args.get('startPageNumber', -1)) + endP = int(args.get('endPageNumber', -1)) + print(f"INFO:HandlePdfText:Processing:{url}:{startP}:{endP}...") + return process_pdftext(url, startP, endP) + except Exception as exc: + return mTC.TCOutResponse(False, 502, f"WARN:HandlePdfText:Failed:{exc}") From cb1d91999d43271b10a8b8b509c0f94049ead6f1 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 18:14:53 +0530 Subject: [PATCH 424/446] SimpleSallap:SimpleMCP:FileMagic switch to TCOutResponse --- .../local.tools/filemagic.py | 21 +++++-------------- .../public_simplechat/local.tools/tcpdf.py | 2 +- .../public_simplechat/local.tools/tcweb.py | 3 +-- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/filemagic.py b/tools/server/public_simplechat/local.tools/filemagic.py index 8d2224e70f..054989ca39 100644 --- a/tools/server/public_simplechat/local.tools/filemagic.py +++ b/tools/server/public_simplechat/local.tools/filemagic.py @@ -4,21 +4,10 @@ import urllib.request import urllib.parse import debug +import toolcall as mTC from dataclasses import dataclass -@dataclass(frozen=True) -class Response: - """ - Used to return result wrt urlreq helper below. - """ - callOk: bool - statusCode: int - statusMsg: str = "" - contentType: str = "" - contentData: bytes = b"" - - def get_from_web(url: str, tag: str, inContentType: str, inHeaders: dict[str, str|None]): """ @@ -52,9 +41,9 @@ def get_from_web(url: str, tag: str, inContentType: str, inHeaders: dict[str, st contentType = response.getheader('Content-Type') or inContentType print(f"DBUG:FM:GFW:Resp:{response.status}:{response.msg}") debug.dump({ 'op': 'FileMagic.GetFromWeb', 'url': req.full_url, 'req.headers': req.headers, 'resp.headers': response.headers, 'ctype': contentType }, { 'cdata': contentData }) - return Response(True, statusCode, statusMsg, contentType, contentData) + return mTC.TCOutResponse(True, statusCode, statusMsg, contentType, contentData) except Exception as exc: - return Response(False, 502, f"WARN:{tag}:Failed:{exc}") + return mTC.TCOutResponse(False, 502, f"WARN:{tag}:Failed:{exc}") def get_from_local(urlParts: urllib.parse.ParseResult, tag: str, inContentType: str): @@ -64,9 +53,9 @@ def get_from_local(urlParts: urllib.parse.ParseResult, tag: str, inContentType: try: fPdf = open(urlParts.path, 'rb') dPdf = fPdf.read() - return Response(True, 200, "", inContentType, dPdf) + return mTC.TCOutResponse(True, 200, "", inContentType, dPdf) except Exception as exc: - return Response(False, 502, f"WARN:{tag}:Failed:{exc}") + return mTC.TCOutResponse(False, 502, f"WARN:{tag}:Failed:{exc}") def get_file(url: str, tag: str, inContentType: str, inHeaders: dict[str, str|None]={}): diff --git a/tools/server/public_simplechat/local.tools/tcpdf.py b/tools/server/public_simplechat/local.tools/tcpdf.py index 1d305f4f2b..da3e5aedfe 100644 --- a/tools/server/public_simplechat/local.tools/tcpdf.py +++ b/tools/server/public_simplechat/local.tools/tcpdf.py @@ -48,7 +48,7 @@ def process_pdftext(url: str, startPN: int, endPN: int): return mTC.TCOutResponse(False, gotVU.statusCode, gotVU.statusMsg) gotFile = mFile.get_file(url, "ProcessPdfText", "application/pdf", {}) if not gotFile.callOk: - return mTC.TCOutResponse(False, gotFile.statusCode, gotFile.statusMsg, gotFile.contentType, gotFile.contentData) + return gotFile tPdf = "" oPdf = pypdf.PdfReader(io.BytesIO(gotFile.contentData)) if (startPN <= 0): diff --git a/tools/server/public_simplechat/local.tools/tcweb.py b/tools/server/public_simplechat/local.tools/tcweb.py index a80789fed5..03e8caaa7b 100644 --- a/tools/server/public_simplechat/local.tools/tcweb.py +++ b/tools/server/public_simplechat/local.tools/tcweb.py @@ -42,8 +42,7 @@ def handle_urlreq(url: str, inHeaders: http.client.HTTPMessage, tag: str): 'Accept-Language': hAL } # Get requested url - gotFile = mFile.get_file(url, tag, "text/html", headers) - return mTC.TCOutResponse(gotFile.callOk, gotFile.statusCode, gotFile.statusMsg, gotFile.contentType, gotFile.contentData) + return mFile.get_file(url, tag, "text/html", headers) except Exception as exc: return mTC.TCOutResponse(False, 502, f"WARN:{tag}:Failed:{exc}") From 1db3a80f4080ce22e573b65cdafc262223190fd6 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 18:29:39 +0530 Subject: [PATCH 425/446] SimpleSallap:SimpleMCP:Duplicate simpleproxy for mcpish handshake Will be looking at changing the handshake between AnveshikaSallap web tech based client logic and this tool calls server to follow the emerging interoperable MCP standard --- .../local.tools/simplemcp.py | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 tools/server/public_simplechat/local.tools/simplemcp.py diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py new file mode 100644 index 0000000000..036630a31f --- /dev/null +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -0,0 +1,242 @@ +# A simple proxy server +# by Humans for All +# +# Listens on the specified port (defaults to squids 3128) +# * if a url query is got wrt urlraw path +# http://localhost:3128/urlraw?url=http://site.of.interest/path/of/interest +# fetches the contents of the specified url and returns the same to the requester +# * if a url query is got wrt urltext path +# http://localhost:3128/urltext?url=http://site.of.interest/path/of/interest +# fetches the contents of the specified url and returns the same to the requester +# after removing html tags in general as well as contents of tags like style +# script, header, footer, nav ... +# * any request to aum path is used to respond with a predefined text response +# which can help identify this server, in a simple way. +# +# Expects a Bearer authorization line in the http header of the requests got. +# HOWEVER DO KEEP IN MIND THAT ITS A VERY INSECURE IMPLEMENTATION, AT BEST +# + + +import sys +import http.server +import urllib.parse +import time +import ssl +import traceback +from typing import Callable +import pdfmagic as mPdf +import webmagic as mWeb +import config as mConfig + + +gMe = mConfig.Config() + + +gAllowedCalls = { + "xmlfiltered": [], + "htmltext": [], + "urlraw": [], + "pdftext": [ "pypdf" ] + } + + +def bearer_transform(): + """ + Transform the raw bearer token to the network handshaked token, + if and when needed. + """ + global gMe + year = str(time.gmtime().tm_year) + if gMe.op.bearerTransformedYear == year: + return + import hashlib + s256 = hashlib.sha256(year.encode('utf-8')) + s256.update(gMe.sec.bearerAuth.encode('utf-8')) + gMe.op.bearerTransformed = s256.hexdigest() + gMe.op.bearerTransformedYear = year + + +class ProxyHandler(http.server.BaseHTTPRequestHandler): + """ + Implements the logic for handling requests sent to this server. + """ + + def send_headers_common(self): + """ + Common headers to include in responses from this server + """ + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') + self.send_header('Access-Control-Allow-Headers', '*') + self.end_headers() + + def send_error(self, code: int, message: str | None = None, explain: str | None = None) -> None: + """ + Overrides the SendError helper + so that the common headers mentioned above can get added to them + else CORS failure will be triggered by the browser on fetch from browser. + """ + print(f"WARN:PH:SendError:{code}:{message}") + self.send_response(code, message) + self.send_headers_common() + + def auth_check(self): + """ + Simple Bearer authorization + ALERT: For multiple reasons, this is a very insecure implementation. + """ + bearer_transform() + authline = self.headers['Authorization'] + if authline == None: + return { 'AllOk': False, 'Msg': "No auth line" } + authlineA = authline.strip().split(' ') + if len(authlineA) != 2: + return { 'AllOk': False, 'Msg': "Invalid auth line" } + if authlineA[0] != 'Bearer': + return { 'AllOk': False, 'Msg': "Invalid auth type" } + if authlineA[1] != gMe.op.bearerTransformed: + return { 'AllOk': False, 'Msg': "Invalid auth" } + return { 'AllOk': True, 'Msg': "Auth Ok" } + + def auth_and_run(self, pr:urllib.parse.ParseResult, handler:Callable[['ProxyHandler', urllib.parse.ParseResult], None]): + """ + If authorisation is ok for the request, run the specified handler. + """ + acGot = self.auth_check() + if not acGot['AllOk']: + self.send_error(400, f"WARN:{acGot['Msg']}") + else: + try: + handler(self, pr) + except Exception as e: + self.send_error(400, f"ERRR:ProxyHandler:{e}") + + def _do_GET(self): + """ + Handle GET requests + """ + print(f"DBUG:ProxyHandler:GET:{self.address_string()}:{self.path}") + print(f"DBUG:PH:Get:Headers:{self.headers}") + pr = urllib.parse.urlparse(self.path) + print(f"DBUG:ProxyHandler:GET:{pr}") + match pr.path: + case '/urlraw': + self.auth_and_run(pr, mWeb.handle_urlraw) + case '/htmltext': + self.auth_and_run(pr, mWeb.handle_htmltext) + case '/xmlfiltered': + self.auth_and_run(pr, mWeb.handle_xmlfiltered) + case '/pdftext': + self.auth_and_run(pr, mPdf.handle_pdftext) + case '/aum': + handle_aum(self, pr) + case _: + print(f"WARN:ProxyHandler:GET:UnknownPath{pr.path}") + self.send_error(400, f"WARN:UnknownPath:{pr.path}") + + def do_GET(self): + """ + Catch all / trap any exceptions wrt actual get based request handling. + """ + try: + self._do_GET() + except: + print(f"ERRR:PH:TheGET:{traceback.format_exception_only(sys.exception())}") + self.send_error(500, f"ERRR: handling request") + + def do_OPTIONS(self): + """ + Handle OPTIONS for CORS preflights (just in case from browser) + """ + print(f"DBUG:ProxyHandler:OPTIONS:{self.path}") + self.send_response(200) + self.send_headers_common() + + def handle(self) -> None: + """ + Helps handle ssl setup in the client specific thread, if in https mode + """ + print(f"\n\n\nDBUG:ProxyHandler:Handle:RequestFrom:{self.client_address}") + try: + if (gMe.op.sslContext): + self.request = gMe.op.sslContext.wrap_socket(self.request, server_side=True) + self.setup() + #self.rfile = self.request.makefile('rb', self.rbufsize) + #self.wfile = self.request.makefile('wb', self.wbufsize) + except: + print(f"ERRR:ProxyHandler:SSLHS:{traceback.format_exception_only(sys.exception())}") + return + return super().handle() + + +def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): + """ + Handle requests to aum path, which is used in a simple way to + verify that one is communicating with this proxy server + """ + import importlib + queryParams = urllib.parse.parse_qs(pr.query) + url = queryParams['url'] + print(f"DBUG:HandleAUM:Url:{url}") + url = url[0] + if (not url) or (len(url) == 0): + ph.send_error(400, f"WARN:HandleAUM:MissingUrl/UnknownQuery?!") + return + urlParts = url.split('.',1) + if gAllowedCalls.get(urlParts[0], None) == None: + ph.send_error(403, f"WARN:HandleAUM:Forbidden:{urlParts[0]}") + return + for dep in gAllowedCalls[urlParts[0]]: + try: + importlib.import_module(dep) + except ImportError as exc: + ph.send_error(400, f"WARN:HandleAUM:{urlParts[0]}:Support module [{dep}] missing or has issues") + return + print(f"INFO:HandleAUM:Availability ok for:{urlParts[0]}") + ph.send_response_only(200, "bharatavarshe") + ph.send_header('Access-Control-Allow-Origin', '*') + ph.end_headers() + + +def setup_server(): + """ + Helps setup a http/https server + """ + try: + gMe.op.server = http.server.ThreadingHTTPServer(gMe.nw.server_address(), ProxyHandler) + if gMe.sec.get('keyFile') and gMe.sec.get('certFile'): + sslCtxt = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + sslCtxt.load_cert_chain(certfile=gMe.sec.certFile, keyfile=gMe.sec.keyFile) + sslCtxt.minimum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED + sslCtxt.maximum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED + gMe.op.sslContext = sslCtxt + print(f"INFO:SetupServer:Starting on {gMe.nw.server_address()}:Https mode") + else: + print(f"INFO:SetupServer:Starting on {gMe.nw.server_address()}:Http mode") + except Exception as exc: + print(f"ERRR:SetupServer:{traceback.format_exc()}") + raise RuntimeError(f"SetupServer:{exc}") from exc + + +def run(): + try: + setup_server() + if not gMe.op.server: + raise RuntimeError("Server missing!!!") + gMe.op.server.serve_forever() + except KeyboardInterrupt: + print("INFO:Run:Shuting down...") + if gMe.op.server: + gMe.op.server.server_close() + sys.exit(0) + except Exception as exc: + print(f"ERRR:Run:Exiting:Exception:{exc}") + if gMe.op.server: + gMe.op.server.server_close() + sys.exit(1) + + +if __name__ == "__main__": + gMe.process_args(sys.argv) + run() From 0a445c875b41bc4a294106a58c875ae092b2da22 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 19:46:19 +0530 Subject: [PATCH 426/446] SimpleSallap:SimpleMCP:Move towards post json based flow --- .../public_simplechat/local.tools/config.py | 1 + .../local.tools/simplemcp.py | 75 ++++++++----------- 2 files changed, 33 insertions(+), 43 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index 4e9e330bb6..b6c7a11bab 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -60,6 +60,7 @@ class Network(DictyDataclassMixin): """ port: int = 3128 addr: str = '' + maxReadBytes: int = 1*1024*1024 def server_address(self): return (self.addr, self.port) diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 036630a31f..b5c497eb31 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -1,20 +1,13 @@ -# A simple proxy server +# A simple mcp server with a bunch of bundled tool calls # by Humans for All # # Listens on the specified port (defaults to squids 3128) -# * if a url query is got wrt urlraw path -# http://localhost:3128/urlraw?url=http://site.of.interest/path/of/interest -# fetches the contents of the specified url and returns the same to the requester -# * if a url query is got wrt urltext path -# http://localhost:3128/urltext?url=http://site.of.interest/path/of/interest -# fetches the contents of the specified url and returns the same to the requester -# after removing html tags in general as well as contents of tags like style -# script, header, footer, nav ... +# * return the supported tool calls meta data when requested +# * execute the requested tool call and return the results # * any request to aum path is used to respond with a predefined text response # which can help identify this server, in a simple way. # # Expects a Bearer authorization line in the http header of the requests got. -# HOWEVER DO KEEP IN MIND THAT ITS A VERY INSECURE IMPLEMENTATION, AT BEST # @@ -24,9 +17,11 @@ import urllib.parse import time import ssl import traceback +import json from typing import Callable -import pdfmagic as mPdf -import webmagic as mWeb +import tcpdf as mTCPdf +import tcweb as mTCWeb +import toolcall as mTC import config as mConfig @@ -89,60 +84,56 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): bearer_transform() authline = self.headers['Authorization'] if authline == None: - return { 'AllOk': False, 'Msg': "No auth line" } + return mTC.TCOutResponse(False, 400, "WARN:No auth line") authlineA = authline.strip().split(' ') if len(authlineA) != 2: - return { 'AllOk': False, 'Msg': "Invalid auth line" } + return mTC.TCOutResponse(False, 400, "WARN:Invalid auth line") if authlineA[0] != 'Bearer': - return { 'AllOk': False, 'Msg': "Invalid auth type" } + return mTC.TCOutResponse(False, 400, "WARN:Invalid auth type") if authlineA[1] != gMe.op.bearerTransformed: - return { 'AllOk': False, 'Msg': "Invalid auth" } - return { 'AllOk': True, 'Msg': "Auth Ok" } + return mTC.TCOutResponse(False, 400, "WARN:Invalid auth") + return mTC.TCOutResponse(True, 200, "Auth Ok") def auth_and_run(self, pr:urllib.parse.ParseResult, handler:Callable[['ProxyHandler', urllib.parse.ParseResult], None]): """ If authorisation is ok for the request, run the specified handler. """ acGot = self.auth_check() - if not acGot['AllOk']: - self.send_error(400, f"WARN:{acGot['Msg']}") + if not acGot.callOk: + self.send_error(acGot.statusCode, acGot.statusMsg) else: try: handler(self, pr) except Exception as e: self.send_error(400, f"ERRR:ProxyHandler:{e}") - def _do_GET(self): + def _do_POST(self): """ - Handle GET requests + Handle POST requests """ - print(f"DBUG:ProxyHandler:GET:{self.address_string()}:{self.path}") - print(f"DBUG:PH:Get:Headers:{self.headers}") + print(f"DBUG:PH:Post:{self.address_string()}:{self.path}") + print(f"DBUG:PH:Post:Headers:{self.headers}") + acGot = self.auth_check() + if not acGot.callOk: + self.send_error(acGot.statusCode, acGot.statusMsg) pr = urllib.parse.urlparse(self.path) print(f"DBUG:ProxyHandler:GET:{pr}") - match pr.path: - case '/urlraw': - self.auth_and_run(pr, mWeb.handle_urlraw) - case '/htmltext': - self.auth_and_run(pr, mWeb.handle_htmltext) - case '/xmlfiltered': - self.auth_and_run(pr, mWeb.handle_xmlfiltered) - case '/pdftext': - self.auth_and_run(pr, mPdf.handle_pdftext) - case '/aum': - handle_aum(self, pr) - case _: - print(f"WARN:ProxyHandler:GET:UnknownPath{pr.path}") - self.send_error(400, f"WARN:UnknownPath:{pr.path}") + if pr.path != '/mcp': + self.send_error(400, f"WARN:UnknownPath:{pr.path}") + body = self.rfile.read(gMe.nw.maxReadBytes) + if len(body) == gMe.nw.maxReadBytes: + self.send_error(400, f"WARN:RequestOverflow:{pr.path}") + oRPC = json.loads(body) - def do_GET(self): + + def do_POST(self): """ - Catch all / trap any exceptions wrt actual get based request handling. + Catch all / trap any exceptions wrt actual post based request handling. """ try: - self._do_GET() + self._do_POST() except: - print(f"ERRR:PH:TheGET:{traceback.format_exception_only(sys.exception())}") + print(f"ERRR:PH:ThePOST:{traceback.format_exception_only(sys.exception())}") self.send_error(500, f"ERRR: handling request") def do_OPTIONS(self): @@ -162,8 +153,6 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): if (gMe.op.sslContext): self.request = gMe.op.sslContext.wrap_socket(self.request, server_side=True) self.setup() - #self.rfile = self.request.makefile('rb', self.rbufsize) - #self.wfile = self.request.makefile('wb', self.wbufsize) except: print(f"ERRR:ProxyHandler:SSLHS:{traceback.format_exception_only(sys.exception())}") return From 8700d522a5d16f3dc0e26cce99e80caddbfb9ecb Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 21:50:51 +0530 Subject: [PATCH 427/446] SimpleSallap:SimpleMCP:ToolCalls beyond initial go Define a typealias for HttpHeaders and use it where ever needed. Inturn map this to email.message.Message and dict for now. If and when python evolves Http Headers type into better one, need to replace in only one place. Add a ToolManager class which * maintains the list of tool calls and inturn allows any given tool call to be executed and response returned along with needed meta data * generate the overall tool calls meta data * add ToolCallResponseEx which maintains full TCOutResponse for use by tc_handle callers Avoid duplicating handling of some of the basic needed http header entries. Move checking for any dependencies before enabling a tool call into respective tc??? module. * for now this also demotes the logic from the previous fine grained per tool call based dependency check to a more global dep check at the respective module level --- .../local.tools/filemagic.py | 4 +- .../public_simplechat/local.tools/tcpdf.py | 17 ++++-- .../public_simplechat/local.tools/tcweb.py | 24 +++----- .../public_simplechat/local.tools/toolcall.py | 55 ++++++++++++++----- 4 files changed, 64 insertions(+), 36 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/filemagic.py b/tools/server/public_simplechat/local.tools/filemagic.py index 054989ca39..0bf3ce7e18 100644 --- a/tools/server/public_simplechat/local.tools/filemagic.py +++ b/tools/server/public_simplechat/local.tools/filemagic.py @@ -9,7 +9,7 @@ from dataclasses import dataclass -def get_from_web(url: str, tag: str, inContentType: str, inHeaders: dict[str, str|None]): +def get_from_web(url: str, tag: str, inContentType: str, inHeaders: mTC.HttpHeaders): """ Get the url specified from web. @@ -58,7 +58,7 @@ def get_from_local(urlParts: urllib.parse.ParseResult, tag: str, inContentType: return mTC.TCOutResponse(False, 502, f"WARN:{tag}:Failed:{exc}") -def get_file(url: str, tag: str, inContentType: str, inHeaders: dict[str, str|None]={}): +def get_file(url: str, tag: str, inContentType: str, inHeaders: mTC.HttpHeaders={}): """ Based on the scheme specified in the passed url, either get from local file system or from the web. diff --git a/tools/server/public_simplechat/local.tools/tcpdf.py b/tools/server/public_simplechat/local.tools/tcpdf.py index da3e5aedfe..572b05c288 100644 --- a/tools/server/public_simplechat/local.tools/tcpdf.py +++ b/tools/server/public_simplechat/local.tools/tcpdf.py @@ -1,12 +1,10 @@ # Helper to manage pdf related requests # by Humans for All -import urllib.parse import urlvalidator as uv import filemagic as mFile import toolcall as mTC -import http.client -from typing import TYPE_CHECKING, Any +from typing import Any PDFOUTLINE_MAXDEPTH=4 @@ -92,7 +90,7 @@ class TCPdfText(mTC.ToolCall): ) ) - def tc_handle(self, args: mTC.TCInArgs, inHeaders: http.client.HTTPMessage) -> mTC.TCOutResponse: + def tc_handle(self, args: mTC.TCInArgs, inHeaders: mTC.HttpHeaders) -> mTC.TCOutResponse: """ Handle pdftext request, which is used to extract plain text from the specified pdf file. @@ -105,3 +103,14 @@ class TCPdfText(mTC.ToolCall): return process_pdftext(url, startP, endP) except Exception as exc: return mTC.TCOutResponse(False, 502, f"WARN:HandlePdfText:Failed:{exc}") + + +def ok(): + import importlib + dep = "pypdf" + try: + importlib.import_module(dep) + return True + except ImportError as exc: + print(f"WARN:TCPdf:{dep} missing or has issues, so not enabling myself") + return False diff --git a/tools/server/public_simplechat/local.tools/tcweb.py b/tools/server/public_simplechat/local.tools/tcweb.py index 03e8caaa7b..d079fed94d 100644 --- a/tools/server/public_simplechat/local.tools/tcweb.py +++ b/tools/server/public_simplechat/local.tools/tcweb.py @@ -1,20 +1,18 @@ # Helper to manage web related requests # by Humans for All -import urllib.parse import urlvalidator as uv import html.parser import debug import filemagic as mFile import json import re -import http.client from typing import Any, cast import toolcall as mTC -def handle_urlreq(url: str, inHeaders: http.client.HTTPMessage, tag: str): +def handle_urlreq(url: str, inHeaders: mTC.HttpHeaders, tag: str): """ Common part of the url request handling used by both urlraw and urltext. @@ -33,16 +31,8 @@ def handle_urlreq(url: str, inHeaders: http.client.HTTPMessage, tag: str): if not gotVU.callOk: return mTC.TCOutResponse(gotVU.callOk, gotVU.statusCode, gotVU.statusMsg) try: - hUA = inHeaders.get('User-Agent', None) - hAL = inHeaders.get('Accept-Language', None) - hA = inHeaders.get('Accept', None) - headers = { - 'User-Agent': hUA, - 'Accept': hA, - 'Accept-Language': hAL - } # Get requested url - return mFile.get_file(url, tag, "text/html", headers) + return mFile.get_file(url, tag, "text/html", inHeaders) except Exception as exc: return mTC.TCOutResponse(False, 502, f"WARN:{tag}:Failed:{exc}") @@ -65,7 +55,7 @@ class TCUrlRaw(mTC.ToolCall): ) ) - def tc_handle(self, args: mTC.TCInArgs, inHeaders: http.client.HTTPMessage) -> mTC.TCOutResponse: + def tc_handle(self, args: mTC.TCInArgs, inHeaders: mTC.HttpHeaders) -> mTC.TCOutResponse: try: # Get requested url got = handle_urlreq(args['url'], inHeaders, "HandleTCUrlRaw") @@ -212,7 +202,7 @@ class TCHtmlText(mTC.ToolCall): ) ) - def tc_handle(self, args: mTC.TCInArgs, inHeaders: http.client.HTTPMessage) -> mTC.TCOutResponse: + def tc_handle(self, args: mTC.TCInArgs, inHeaders: mTC.HttpHeaders) -> mTC.TCOutResponse: try: # Get requested url got = handle_urlreq(args['url'], inHeaders, "HandleTCHtmlText") @@ -329,7 +319,7 @@ class TCXmlFiltered(mTC.ToolCall): ) ) - def tc_handle(self, args: mTC.TCInArgs, inHeaders: http.client.HTTPMessage) -> mTC.TCOutResponse: + def tc_handle(self, args: mTC.TCInArgs, inHeaders: mTC.HttpHeaders) -> mTC.TCOutResponse: try: # Get requested url got = handle_urlreq(args['url'], inHeaders, "HandleTCXMLFiltered") @@ -347,3 +337,7 @@ class TCXmlFiltered(mTC.ToolCall): return mTC.TCOutResponse(True, got.statusCode, got.statusMsg, got.contentType, xmlFiltered.text.encode('utf-8')) except Exception as exc: return mTC.TCOutResponse(False, 502, f"WARN:XMLFiltered:Failed:{exc}") + + +def ok(): + return True diff --git a/tools/server/public_simplechat/local.tools/toolcall.py b/tools/server/public_simplechat/local.tools/toolcall.py index b281eb5b1f..3b3c297ff1 100644 --- a/tools/server/public_simplechat/local.tools/toolcall.py +++ b/tools/server/public_simplechat/local.tools/toolcall.py @@ -1,11 +1,11 @@ # Tool Call Base # by Humans for All -from typing import Any, TypeAlias +from typing import Any, TypeAlias, TYPE_CHECKING from dataclasses import dataclass -import http -import http.client -import urllib.parse + +if TYPE_CHECKING: + import email.message # @@ -61,13 +61,6 @@ class ToolCallMeta(): type: str = "function" function: TCFunction|None = None -@dataclass -class TollCallResponse(): - status: bool - tcid: str - name: str - content: str = "" - @dataclass(frozen=True) class TCOutResponse: """ @@ -79,6 +72,21 @@ class TCOutResponse: contentType: str = "" contentData: bytes = b"" +@dataclass +class ToolCallResponseEx(): + tcid: str + name: str + response: TCOutResponse + +@dataclass +class ToolCallResponse(): + status: bool + tcid: str + name: str + content: str = "" + +HttpHeaders: TypeAlias = dict[str, str] | email.message.Message[str, str] + @dataclass class ToolCall(): @@ -87,13 +95,30 @@ class ToolCall(): def tcf_meta(self) -> TCFunction|None: return None - def tc_handle(self, args: TCInArgs, inHeaders: http.client.HTTPMessage) -> TCOutResponse: + def tc_handle(self, args: TCInArgs, inHeaders: HttpHeaders) -> TCOutResponse: return TCOutResponse(False, 500) def meta(self) -> ToolCallMeta: tcf = self.tcf_meta() return ToolCallMeta("function", tcf) - def handler(self, callId: str, args: Any, inHeaders: http.client.HTTPMessage) -> TollCallResponse: - got = self.tc_handle(args, inHeaders) - return TollCallResponse(got.callOk, callId, self.name, got.contentData.decode('utf-8')) + +class ToolManager(): + + def __init__(self) -> None: + self.toolcalls: dict[str, ToolCall] = {} + + def tc_add(self, fName: str, tc: ToolCall): + self.toolcalls[fName] = tc + + def meta(self): + oMeta = {} + for tcName in self.toolcalls.keys(): + oMeta[tcName] = self.toolcalls[tcName].meta() + + def tc_handle(self, tcName: str, callId: str, tcArgs: TCInArgs, inHeaders: HttpHeaders) -> ToolCallResponseEx: + try: + response = self.toolcalls[tcName].tc_handle(tcArgs, inHeaders) + return ToolCallResponseEx(callId, tcName, response) + except KeyError: + return ToolCallResponseEx(callId, tcName, TCOutResponse(False, 400, "Unknown tool call")) From 69be7f20296bd3786bccab490f3b696f13e89159 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sat, 6 Dec 2025 22:38:57 +0530 Subject: [PATCH 428/446] SimpleSallap:SimpleMCP: Use ToolManager for some of needed logics Build the list of tool calls Trap some of the MCP post json based requests and map to related handlers. Inturn implement the tool call execution handler. Add some helper dataclasses wrt expected MCP response structure TOTHINK: For now maintain id has a string and not int, with idea to map it directly to callid wrt tool call handshake by ai model. TOCHECK: For now suffle the order of fields wrt jsonrpc and type wrt MCP response related structures, assuming the order shouldnt matter. Need to cross check. --- .../public_simplechat/docs/changelog.md | 6 ++ .../public_simplechat/local.tools/config.py | 2 + .../local.tools/simplemcp.py | 86 ++++++++++--------- .../public_simplechat/local.tools/toolcall.py | 19 ++-- 4 files changed, 67 insertions(+), 46 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index e67233bca8..644e76ee03 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -317,6 +317,12 @@ Chat Session specific settings * enable multi threaded ssl and client request handling, so that rogue clients cant mount simple DoS by opening connection and then missing in action. * switch to a Dicty DataClass based Config with better type validation and usage, instead of literal dict++ +* ToolCall, ToolManager and related classes based flow wrt the tool calls. + * all existing tool calls duplicated and updated to support and build on this new flow. +* Initial skeleton towards SimpleMCP, a post and json based handshake flow, so that the tool calls supported + through SimpleProxy can be exposed through a MCP standardish mechanism. + * can allow others beyond AnveshikaSallap client to use the corresponding tool calls + * can allow AnveshikaSallap client to support other MCP servers and their exposed tool calls in future. ## ToDo diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index b6c7a11bab..1979db015a 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -9,6 +9,7 @@ import ssl import sys import urlvalidator as mUV import debug as mDebug +import toolcall as mTC gConfigNeeded = [ 'acl.schemes', 'acl.domains', 'sec.bearerAuth' ] @@ -75,6 +76,7 @@ class Op(DictyDataclassMixin): debug: bool = False server: http.server.ThreadingHTTPServer|None = None sslContext: ssl.SSLContext|None = None + toolManager: mTC.ToolManager|None = None bearerTransformed: str = "" bearerTransformedYear: str = "" diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index b5c497eb31..2d1e098a7f 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -18,7 +18,7 @@ import time import ssl import traceback import json -from typing import Callable +from typing import Any import tcpdf as mTCPdf import tcweb as mTCWeb import toolcall as mTC @@ -28,14 +28,6 @@ import config as mConfig gMe = mConfig.Config() -gAllowedCalls = { - "xmlfiltered": [], - "htmltext": [], - "urlraw": [], - "pdftext": [ "pypdf" ] - } - - def bearer_transform(): """ Transform the raw bearer token to the network handshaked token, @@ -94,18 +86,44 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): return mTC.TCOutResponse(False, 400, "WARN:Invalid auth") return mTC.TCOutResponse(True, 200, "Auth Ok") - def auth_and_run(self, pr:urllib.parse.ParseResult, handler:Callable[['ProxyHandler', urllib.parse.ParseResult], None]): + def mcp_toolscall(self, oRPC: Any): """ If authorisation is ok for the request, run the specified handler. """ - acGot = self.auth_check() - if not acGot.callOk: - self.send_error(acGot.statusCode, acGot.statusMsg) + try: + if not gMe.op.toolManager: + raise RuntimeError("DBUG:PH:TCRun:ToolManager uninitialised") + resp = gMe.op.toolManager.tc_handle(oRPC["id"], oRPC["params"]["name"], oRPC["params"]["arguments"], self.headers) + if not resp.response.callOk: + self.send_error(resp.response.statusCode, resp.response.statusMsg) + return + tcresp = mTC.MCPToolCallResponse( + resp.tcid, + resp.name, + mTC.MCPTCRResult([ + mTC.MCPTCRContentText(resp.response.contentData.decode('utf-8')) + ]) + ) + self.send_response(resp.response.statusCode, resp.response.statusMsg) + self.send_header('Content-Type', "application/json") + # Add CORS for browser fetch, just in case + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(json.dumps(tcresp).encode('utf-8')) + except Exception as e: + self.send_error(400, f"ERRR:PH:{e}") + + def mcp_toolslist(self): + + pass + + def mcp_run(self, oRPC: Any): + if oRPC["method"] == "tools/call": + self.mcp_toolscall(oRPC) + elif oRPC["method"] == "tools/list": + self.mcp_toolslist() else: - try: - handler(self, pr) - except Exception as e: - self.send_error(400, f"ERRR:ProxyHandler:{e}") + self.send_error(400, f"ERRR:PH:MCP:Unknown") def _do_POST(self): """ @@ -124,7 +142,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): if len(body) == gMe.nw.maxReadBytes: self.send_error(400, f"WARN:RequestOverflow:{pr.path}") oRPC = json.loads(body) - + self.mcp_run(oRPC) def do_POST(self): """ @@ -159,33 +177,18 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): return super().handle() -def handle_aum(ph: ProxyHandler, pr: urllib.parse.ParseResult): +def setup_toolmanager(): """ Handle requests to aum path, which is used in a simple way to verify that one is communicating with this proxy server """ - import importlib - queryParams = urllib.parse.parse_qs(pr.query) - url = queryParams['url'] - print(f"DBUG:HandleAUM:Url:{url}") - url = url[0] - if (not url) or (len(url) == 0): - ph.send_error(400, f"WARN:HandleAUM:MissingUrl/UnknownQuery?!") - return - urlParts = url.split('.',1) - if gAllowedCalls.get(urlParts[0], None) == None: - ph.send_error(403, f"WARN:HandleAUM:Forbidden:{urlParts[0]}") - return - for dep in gAllowedCalls[urlParts[0]]: - try: - importlib.import_module(dep) - except ImportError as exc: - ph.send_error(400, f"WARN:HandleAUM:{urlParts[0]}:Support module [{dep}] missing or has issues") - return - print(f"INFO:HandleAUM:Availability ok for:{urlParts[0]}") - ph.send_response_only(200, "bharatavarshe") - ph.send_header('Access-Control-Allow-Origin', '*') - ph.end_headers() + gMe.op.toolManager = mTC.ToolManager() + if mTCWeb.ok(): + gMe.op.toolManager.tc_add("fetch_url_raw", mTCWeb.TCUrlRaw("fetch_url_raw")) + gMe.op.toolManager.tc_add("fetch_html_text", mTCWeb.TCHtmlText("fetch_html_text")) + gMe.op.toolManager.tc_add("fetch_xml_filtered", mTCWeb.TCXmlFiltered("fetch_xml_filtered")) + if mTCPdf.ok(): + gMe.op.toolManager.tc_add("fetch_pdf_text", mTCPdf.TCPdfText("fetch_pdf_text")) def setup_server(): @@ -228,4 +231,5 @@ def run(): if __name__ == "__main__": gMe.process_args(sys.argv) + setup_toolmanager() run() diff --git a/tools/server/public_simplechat/local.tools/toolcall.py b/tools/server/public_simplechat/local.tools/toolcall.py index 3b3c297ff1..39d40820eb 100644 --- a/tools/server/public_simplechat/local.tools/toolcall.py +++ b/tools/server/public_simplechat/local.tools/toolcall.py @@ -78,12 +78,21 @@ class ToolCallResponseEx(): name: str response: TCOutResponse +@dataclass(frozen=True) +class MCPTCRContentText: + text: str + type: str = "text" + @dataclass -class ToolCallResponse(): - status: bool - tcid: str +class MCPTCRResult: + content: list[MCPTCRContentText] + +@dataclass +class MCPToolCallResponse: + id: str name: str - content: str = "" + result: MCPTCRResult + jsonrpc: str = "2.0" HttpHeaders: TypeAlias = dict[str, str] | email.message.Message[str, str] @@ -116,7 +125,7 @@ class ToolManager(): for tcName in self.toolcalls.keys(): oMeta[tcName] = self.toolcalls[tcName].meta() - def tc_handle(self, tcName: str, callId: str, tcArgs: TCInArgs, inHeaders: HttpHeaders) -> ToolCallResponseEx: + def tc_handle(self, callId: str, tcName: str, tcArgs: TCInArgs, inHeaders: HttpHeaders) -> ToolCallResponseEx: try: response = self.toolcalls[tcName].tc_handle(tcArgs, inHeaders) return ToolCallResponseEx(callId, tcName, response) From fac947f9cddaf915318a5211e6d5d80562cb2c23 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 00:22:45 +0530 Subject: [PATCH 429/446] SimpleSallap:SimpleMCP:tools/list Fix a oversight wrt ToolManager.meta, where I had created a dict of name-keyed toolcall metas, instead of a simple list of toolcall metas. Rather I blindly duplicated structure I used for storing the tool calls in the tc_switch in the anveshika sallap client side code. Add dataclasses to mimic the MCP tools/list response. However wrt the 2 odd differences between the MCP structure and OpenAi tools handshake structure, for now I have retained the OpenAi tools hs structure. Add a common helper send_mcp to ProxyHandler given that both mcp_toolscall and mcp_toolslist and even others like mcp_initialise in future require a common response mechanism. With above and bit more implement initial go at tools/list response. --- .../local.tools/simplemcp.py | 27 +++++++++++-------- .../public_simplechat/local.tools/toolcall.py | 20 +++++++++++--- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 2d1e098a7f..34c7a9cf15 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -86,13 +86,21 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): return mTC.TCOutResponse(False, 400, "WARN:Invalid auth") return mTC.TCOutResponse(True, 200, "Auth Ok") + def send_mcp(self, statusCode: int, statusMessage: str, body: Any): + self.send_response(statusCode, statusMessage) + self.send_header('Content-Type', "application/json") + # Add CORS for browser fetch, just in case + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(json.dumps(body).encode('utf-8')) + def mcp_toolscall(self, oRPC: Any): """ If authorisation is ok for the request, run the specified handler. """ try: if not gMe.op.toolManager: - raise RuntimeError("DBUG:PH:TCRun:ToolManager uninitialised") + raise RuntimeError("DBUG:PH:MCPToolsCall:ToolManager uninitialised") resp = gMe.op.toolManager.tc_handle(oRPC["id"], oRPC["params"]["name"], oRPC["params"]["arguments"], self.headers) if not resp.response.callOk: self.send_error(resp.response.statusCode, resp.response.statusMsg) @@ -104,24 +112,21 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): mTC.MCPTCRContentText(resp.response.contentData.decode('utf-8')) ]) ) - self.send_response(resp.response.statusCode, resp.response.statusMsg) - self.send_header('Content-Type', "application/json") - # Add CORS for browser fetch, just in case - self.send_header('Access-Control-Allow-Origin', '*') - self.end_headers() - self.wfile.write(json.dumps(tcresp).encode('utf-8')) + self.send_mcp(resp.response.statusCode, resp.response.statusMsg, tcresp) except Exception as e: self.send_error(400, f"ERRR:PH:{e}") - def mcp_toolslist(self): - - pass + def mcp_toolslist(self, oRPC: Any): + if not gMe.op.toolManager: + raise RuntimeError("DBUG:PH:MCPToolsList:ToolManager uninitialised") + tcl = mTC.MCPToolsList(oRPC["id"], mTC.MCPTLResult(gMe.op.toolManager.meta())) + self.send_mcp(200, "tools/list follows", tcl) def mcp_run(self, oRPC: Any): if oRPC["method"] == "tools/call": self.mcp_toolscall(oRPC) elif oRPC["method"] == "tools/list": - self.mcp_toolslist() + self.mcp_toolslist(oRPC) else: self.send_error(400, f"ERRR:PH:MCP:Unknown") diff --git a/tools/server/public_simplechat/local.tools/toolcall.py b/tools/server/public_simplechat/local.tools/toolcall.py index 39d40820eb..102b058bd9 100644 --- a/tools/server/public_simplechat/local.tools/toolcall.py +++ b/tools/server/public_simplechat/local.tools/toolcall.py @@ -54,10 +54,10 @@ class TCInParameters(): class TCFunction(): name: str description: str - parameters: TCInParameters + parameters: TCInParameters ### Delta wrt naming btw OpenAi Tools HS (parameters) and MCP(inputSchema) @dataclass -class ToolCallMeta(): +class ToolCallMeta(): ### Delta wrt tree btw OpenAi Tools HS (Needs this wrapper) and MCP (directly use TCFunction) type: str = "function" function: TCFunction|None = None @@ -112,6 +112,17 @@ class ToolCall(): return ToolCallMeta("function", tcf) +@dataclass +class MCPTLResult: + tools: list[ToolCallMeta] + +@dataclass +class MCPToolsList: + id: str + result: MCPTLResult + jsonrpc: str = "2.0" + + class ToolManager(): def __init__(self) -> None: @@ -121,9 +132,10 @@ class ToolManager(): self.toolcalls[fName] = tc def meta(self): - oMeta = {} + lMeta: list[ToolCallMeta]= [] for tcName in self.toolcalls.keys(): - oMeta[tcName] = self.toolcalls[tcName].meta() + lMeta.append(self.toolcalls[tcName].meta()) + return lMeta def tc_handle(self, callId: str, tcName: str, tcArgs: TCInArgs, inHeaders: HttpHeaders) -> ToolCallResponseEx: try: From 9d6daaed8c10e705032ed96405e701cf41451f1c Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 02:26:45 +0530 Subject: [PATCH 430/446] SimpleSallap:SimpleMCP:Body Bytes to Json within mcp_run Given that there could be other service paths beyond /mcp exposed in future, and given that it is not necessary that their post body contain json data, so move conversion to json to within mcp_run handler. While retaining reading of the body in the generic do_POST ensures that the read size limit is implicitly enforced, whether /mcp now or any other path in future. --- tools/server/public_simplechat/local.tools/simplemcp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 34c7a9cf15..c2c167dba0 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -122,7 +122,8 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): tcl = mTC.MCPToolsList(oRPC["id"], mTC.MCPTLResult(gMe.op.toolManager.meta())) self.send_mcp(200, "tools/list follows", tcl) - def mcp_run(self, oRPC: Any): + def mcp_run(self, body: bytes): + oRPC = json.loads(body) if oRPC["method"] == "tools/call": self.mcp_toolscall(oRPC) elif oRPC["method"] == "tools/list": @@ -140,14 +141,13 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): if not acGot.callOk: self.send_error(acGot.statusCode, acGot.statusMsg) pr = urllib.parse.urlparse(self.path) - print(f"DBUG:ProxyHandler:GET:{pr}") + print(f"DBUG:PH:Post:{pr}") if pr.path != '/mcp': self.send_error(400, f"WARN:UnknownPath:{pr.path}") body = self.rfile.read(gMe.nw.maxReadBytes) if len(body) == gMe.nw.maxReadBytes: self.send_error(400, f"WARN:RequestOverflow:{pr.path}") - oRPC = json.loads(body) - self.mcp_run(oRPC) + self.mcp_run(body) def do_POST(self): """ From 79cfbbfc8ae7413d1d1c1f5e5f3fddc7258b4471 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 02:34:05 +0530 Subject: [PATCH 431/446] SimpleSallap:SimpleMCP:Allow auth check to be bypassed, if needed By default bearer based auth check is done always whether in https or http mode. However by updating the sec.bAuthAlways config entry to false, the bearer auth check will be carried out only in https mode. --- tools/server/public_simplechat/local.tools/config.py | 1 + tools/server/public_simplechat/local.tools/simplemcp.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index 1979db015a..74fcb2c9ef 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -43,6 +43,7 @@ class Sec(DictyDataclassMixin): certFile: str = "" keyFile: str = "" bearerAuth: str = "" + bAuthAlways: bool = True @dataclass diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index c2c167dba0..6375a96a9d 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -137,9 +137,10 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): """ print(f"DBUG:PH:Post:{self.address_string()}:{self.path}") print(f"DBUG:PH:Post:Headers:{self.headers}") - acGot = self.auth_check() - if not acGot.callOk: - self.send_error(acGot.statusCode, acGot.statusMsg) + if gMe.op.sslContext or gMe.sec.bAuthAlways: + acGot = self.auth_check() + if not acGot.callOk: + self.send_error(acGot.statusCode, acGot.statusMsg) pr = urllib.parse.urlparse(self.path) print(f"DBUG:PH:Post:{pr}") if pr.path != '/mcp': From e9dbe21c67cea9f618fccfb696005e43245c7243 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 03:14:21 +0530 Subject: [PATCH 432/446] SimpleSallap:SimpleMCP:Cleanup initial go by running and seeing As expected dataclass field member mutable default values needing default_factory. Dont forget returning after sending error response. TypeAlias type hinting flow seems to go beyond TYPE_CHECKING. Also email.message.Message[str,str] not accepted, so keep things simple wrt HttpHeaders for now. --- .../public_simplechat/local.tools/simplemcp.py | 6 +++++- .../public_simplechat/local.tools/toolcall.py | 13 ++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 6375a96a9d..8030241dbd 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -101,7 +101,8 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): try: if not gMe.op.toolManager: raise RuntimeError("DBUG:PH:MCPToolsCall:ToolManager uninitialised") - resp = gMe.op.toolManager.tc_handle(oRPC["id"], oRPC["params"]["name"], oRPC["params"]["arguments"], self.headers) + inHeaders: Any = self.headers + resp = gMe.op.toolManager.tc_handle(oRPC["id"], oRPC["params"]["name"], oRPC["params"]["arguments"], inHeaders) if not resp.response.callOk: self.send_error(resp.response.statusCode, resp.response.statusMsg) return @@ -141,13 +142,16 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): acGot = self.auth_check() if not acGot.callOk: self.send_error(acGot.statusCode, acGot.statusMsg) + return pr = urllib.parse.urlparse(self.path) print(f"DBUG:PH:Post:{pr}") if pr.path != '/mcp': self.send_error(400, f"WARN:UnknownPath:{pr.path}") + return body = self.rfile.read(gMe.nw.maxReadBytes) if len(body) == gMe.nw.maxReadBytes: self.send_error(400, f"WARN:RequestOverflow:{pr.path}") + return self.mcp_run(body) def do_POST(self): diff --git a/tools/server/public_simplechat/local.tools/toolcall.py b/tools/server/public_simplechat/local.tools/toolcall.py index 102b058bd9..c73b20e116 100644 --- a/tools/server/public_simplechat/local.tools/toolcall.py +++ b/tools/server/public_simplechat/local.tools/toolcall.py @@ -1,11 +1,9 @@ # Tool Call Base # by Humans for All -from typing import Any, TypeAlias, TYPE_CHECKING -from dataclasses import dataclass +from typing import Any, TypeAlias +from dataclasses import dataclass, field -if TYPE_CHECKING: - import email.message # @@ -47,8 +45,8 @@ TCInProperties: TypeAlias = dict[str, TCInProperty] @dataclass class TCInParameters(): type: str = "object" - properties: TCInProperties = {} - required: list[str] = [] + properties: TCInProperties = field(default_factory=dict) + required: list[str] = field(default_factory=list) @dataclass class TCFunction(): @@ -94,7 +92,8 @@ class MCPToolCallResponse: result: MCPTCRResult jsonrpc: str = "2.0" -HttpHeaders: TypeAlias = dict[str, str] | email.message.Message[str, str] +#HttpHeaders: TypeAlias = dict[str, str] | email.message.Message[str, str] +HttpHeaders: TypeAlias = dict[str, str] @dataclass From bc9dd580b9096d29e6c8cc49ca11bfc5f357b9a0 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 18:34:53 +0530 Subject: [PATCH 433/446] SimpleSallap:SimpleMCP:InitalGoCleanup Limit read to ContentLength Also enforce need for kind of a sane Content-Length header entry in our case. NOTE: it does allow for 0 or other small content lengths, which isnt necessarily valid. --- .../server/public_simplechat/local.tools/simplemcp.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 8030241dbd..1072aa8abc 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -148,10 +148,17 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): if pr.path != '/mcp': self.send_error(400, f"WARN:UnknownPath:{pr.path}") return - body = self.rfile.read(gMe.nw.maxReadBytes) - if len(body) == gMe.nw.maxReadBytes: + bytesToRead = min(int(self.headers.get('Content-Length', -1)), gMe.nw.maxReadBytes) + if bytesToRead <= -1: + self.send_error(400, f"WARN:ContentLength missing:{pr.path}") + return + if bytesToRead == gMe.nw.maxReadBytes: self.send_error(400, f"WARN:RequestOverflow:{pr.path}") return + body = self.rfile.read(bytesToRead) + if len(body) != bytesToRead: + self.send_error(400, f"WARN:ContentLength mismatch:{pr.path}") + return self.mcp_run(body) def do_POST(self): From f75f93f8d9f761364fcf58e60c65cdb25ca6f114 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 19:36:07 +0530 Subject: [PATCH 434/446] SimpleSallap:SimpleMCP:SendMcp expects dataclass and uses asdict --- tools/server/public_simplechat/local.tools/simplemcp.py | 5 ++++- tools/server/public_simplechat/local.tools/toolcall.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 1072aa8abc..b3e5282040 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -19,12 +19,14 @@ import ssl import traceback import json from typing import Any +from dataclasses import asdict import tcpdf as mTCPdf import tcweb as mTCWeb import toolcall as mTC import config as mConfig + gMe = mConfig.Config() @@ -92,7 +94,8 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): # Add CORS for browser fetch, just in case self.send_header('Access-Control-Allow-Origin', '*') self.end_headers() - self.wfile.write(json.dumps(body).encode('utf-8')) + data = asdict(body) + self.wfile.write(json.dumps(data).encode('utf-8')) def mcp_toolscall(self, oRPC: Any): """ diff --git a/tools/server/public_simplechat/local.tools/toolcall.py b/tools/server/public_simplechat/local.tools/toolcall.py index c73b20e116..618b91182e 100644 --- a/tools/server/public_simplechat/local.tools/toolcall.py +++ b/tools/server/public_simplechat/local.tools/toolcall.py @@ -111,9 +111,11 @@ class ToolCall(): return ToolCallMeta("function", tcf) +MCPTLTools: TypeAlias = list[ToolCallMeta] + @dataclass class MCPTLResult: - tools: list[ToolCallMeta] + tools: MCPTLTools @dataclass class MCPToolsList: @@ -131,7 +133,7 @@ class ToolManager(): self.toolcalls[fName] = tc def meta(self): - lMeta: list[ToolCallMeta]= [] + lMeta: MCPTLTools = [] for tcName in self.toolcalls.keys(): lMeta.append(self.toolcalls[tcName].meta()) return lMeta From 37651dc7ddc8dff06b86340d739b531bbd3e7943 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 19:52:42 +0530 Subject: [PATCH 435/446] SimpleSallap:SimpleMCP:Cleanup, rename; TestMcpCmdline helper Given toolcall.py maintains ToolCall, ToolManager and MCP related types and base classes, so rename to toolcalls.py Also add the bash script with curl used for testing the tools/list mcp command. Remove the sample function meta ref, as tools/list is working ok. --- .../public_simplechat/docs/changelog.md | 4 ++- .../public_simplechat/local.tools/config.py | 2 +- .../local.tools/filemagic.py | 2 +- .../local.tools/simplemcp.py | 2 +- .../public_simplechat/local.tools/tcpdf.py | 2 +- .../public_simplechat/local.tools/tcweb.py | 2 +- .../local.tools/test-mcp-cmdline.sh | 10 ++++++ .../local.tools/{toolcall.py => toolcalls.py} | 32 ++----------------- 8 files changed, 21 insertions(+), 35 deletions(-) create mode 100644 tools/server/public_simplechat/local.tools/test-mcp-cmdline.sh rename tools/server/public_simplechat/local.tools/{toolcall.py => toolcalls.py} (74%) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 644e76ee03..8cd4e121eb 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -319,10 +319,12 @@ Chat Session specific settings * switch to a Dicty DataClass based Config with better type validation and usage, instead of literal dict++ * ToolCall, ToolManager and related classes based flow wrt the tool calls. * all existing tool calls duplicated and updated to support and build on this new flow. -* Initial skeleton towards SimpleMCP, a post and json based handshake flow, so that the tool calls supported +* Initial skeleton towards SimpleMCP, a post and json rpcish based handshake flow, so that tool calls supported through SimpleProxy can be exposed through a MCP standardish mechanism. * can allow others beyond AnveshikaSallap client to use the corresponding tool calls * can allow AnveshikaSallap client to support other MCP servers and their exposed tool calls in future. + Mcp command tools/list implemented and verified at a basic level + Mcp command tools/call implemented, need to verify and update the initial go version ## ToDo diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index 74fcb2c9ef..448ec8bed5 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -9,7 +9,7 @@ import ssl import sys import urlvalidator as mUV import debug as mDebug -import toolcall as mTC +import toolcalls as mTC gConfigNeeded = [ 'acl.schemes', 'acl.domains', 'sec.bearerAuth' ] diff --git a/tools/server/public_simplechat/local.tools/filemagic.py b/tools/server/public_simplechat/local.tools/filemagic.py index 0bf3ce7e18..4ed834f8cf 100644 --- a/tools/server/public_simplechat/local.tools/filemagic.py +++ b/tools/server/public_simplechat/local.tools/filemagic.py @@ -4,7 +4,7 @@ import urllib.request import urllib.parse import debug -import toolcall as mTC +import toolcalls as mTC from dataclasses import dataclass diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index b3e5282040..0233c1f2ce 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -22,7 +22,7 @@ from typing import Any from dataclasses import asdict import tcpdf as mTCPdf import tcweb as mTCWeb -import toolcall as mTC +import toolcalls as mTC import config as mConfig diff --git a/tools/server/public_simplechat/local.tools/tcpdf.py b/tools/server/public_simplechat/local.tools/tcpdf.py index 572b05c288..a6103e1650 100644 --- a/tools/server/public_simplechat/local.tools/tcpdf.py +++ b/tools/server/public_simplechat/local.tools/tcpdf.py @@ -3,7 +3,7 @@ import urlvalidator as uv import filemagic as mFile -import toolcall as mTC +import toolcalls as mTC from typing import Any diff --git a/tools/server/public_simplechat/local.tools/tcweb.py b/tools/server/public_simplechat/local.tools/tcweb.py index d079fed94d..fe4b4239c3 100644 --- a/tools/server/public_simplechat/local.tools/tcweb.py +++ b/tools/server/public_simplechat/local.tools/tcweb.py @@ -8,7 +8,7 @@ import filemagic as mFile import json import re from typing import Any, cast -import toolcall as mTC +import toolcalls as mTC diff --git a/tools/server/public_simplechat/local.tools/test-mcp-cmdline.sh b/tools/server/public_simplechat/local.tools/test-mcp-cmdline.sh new file mode 100644 index 0000000000..d48ddeeb63 --- /dev/null +++ b/tools/server/public_simplechat/local.tools/test-mcp-cmdline.sh @@ -0,0 +1,10 @@ +echo "DONT FORGET TO RUN simplemcp.py with auth always disabled and in http mode" +echo "Note: sudo tcpdump -i lo -s 0 -vvv -A host 127.0.0.1 and port 3128 | tee /tmp/td.log can be used to capture the hs" +curl http://localhost:3128/mcp --trace - --header "Content-Type: application/json" -d '{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list" +}' + +exit + diff --git a/tools/server/public_simplechat/local.tools/toolcall.py b/tools/server/public_simplechat/local.tools/toolcalls.py similarity index 74% rename from tools/server/public_simplechat/local.tools/toolcall.py rename to tools/server/public_simplechat/local.tools/toolcalls.py index 618b91182e..e11430936c 100644 --- a/tools/server/public_simplechat/local.tools/toolcall.py +++ b/tools/server/public_simplechat/local.tools/toolcalls.py @@ -1,4 +1,4 @@ -# Tool Call Base +# ToolCalls and MCP related types and bases # by Humans for All from typing import Any, TypeAlias @@ -6,29 +6,6 @@ from dataclasses import dataclass, field -# -# A sample tool call meta -# - -fetchurlraw_meta = { - "type": "function", - "function": { - "name": "fetch_url_raw", - "description": "Fetch contents of the requested url (local file path / web based) through a proxy server and return the got content as is, in few seconds. Mainly useful for getting textual non binary contents", - "parameters": { - "type": "object", - "properties": { - "url":{ - "type":"string", - "description":"url of the local file / web content to fetch" - } - }, - "required": ["url"] - } - } - } - - # # Dataclasses to help with Tool Calls # @@ -106,10 +83,6 @@ class ToolCall(): def tc_handle(self, args: TCInArgs, inHeaders: HttpHeaders) -> TCOutResponse: return TCOutResponse(False, 500) - def meta(self) -> ToolCallMeta: - tcf = self.tcf_meta() - return ToolCallMeta("function", tcf) - MCPTLTools: TypeAlias = list[ToolCallMeta] @@ -135,7 +108,8 @@ class ToolManager(): def meta(self): lMeta: MCPTLTools = [] for tcName in self.toolcalls.keys(): - lMeta.append(self.toolcalls[tcName].meta()) + tcfMeta = self.toolcalls[tcName].tcf_meta() + lMeta.append(ToolCallMeta("function", tcfMeta)) return lMeta def tc_handle(self, callId: str, tcName: str, tcArgs: TCInArgs, inHeaders: HttpHeaders) -> ToolCallResponseEx: From 5f895b8abf51a9f43aa2fa5be0361e228ddbc264 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 20:48:57 +0530 Subject: [PATCH 436/446] SimpleSallap:SimpleMCP:duplicate toolweb.mjs for mcpish client --- tools/server/public_simplechat/toolmcp.mjs | 446 +++++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 tools/server/public_simplechat/toolmcp.mjs diff --git a/tools/server/public_simplechat/toolmcp.mjs b/tools/server/public_simplechat/toolmcp.mjs new file mode 100644 index 0000000000..3933489923 --- /dev/null +++ b/tools/server/public_simplechat/toolmcp.mjs @@ -0,0 +1,446 @@ +//@ts-check +// ALERT - Simple Stupid flow - Using from a discardable VM is better +// Helpers to handle tools/functions calling related to local/web access, pdf, etal +// which work in sync with the bundled simpleproxy.py server logic. +// Uses the js specific web worker path. +// by Humans for All +// + +// +// The simpleproxy.py server is expected to provide the below services +// urlraw - fetch the request url content as is +// htmltext - fetch the requested html content and provide plain text version +// after stripping it of tag blocks like head, script, style, header, footer, nav, ... +// pdftext - fetch the requested pdf and provide the plain text version +// xmlfiltered - fetch the requested xml content and provide a optionally filtered version of same +// + + +import * as mChatMagic from './simplechat.js' +import * as mToolsMgr from './tools.mjs' + + +/** + * @type {mChatMagic.Me} + */ +let gMe = /** @type{mChatMagic.Me} */(/** @type {unknown} */(null)); + + +/** + * For now hash the shared secret with the year. + * @param {mChatMagic.SimpleChat} chat + */ +async function bearer_transform(chat) { + let data = `${new Date().getUTCFullYear()}${chat.cfg.tools.proxyAuthInsecure}` + const ab = await crypto.subtle.digest('sha-256', new TextEncoder().encode(data)); + return Array.from(new Uint8Array(ab)).map(b => b.toString(16).padStart(2, '0')).join(''); +} + +/** + * Helper http get logic wrt the bundled SimpleProxy server, + * which helps execute a given proxy dependent tool call. + * Expects the simple minded proxy server to be running locally + * * listening on a configured port + * * expecting http requests + * * with a predefined query token and value wrt a predefined path + * NOTE: Initial go, handles textual data type. + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} objSearchParams + * @param {string} path + * @param {any} objHeaders + */ +async function proxyserver_get_anyargs(chatid, toolcallid, toolname, objSearchParams, path, objHeaders={}) { + let chat = gMe.multiChat.simpleChats[chatid] + if (gMe.toolsMgr.workers.js.onmessage != null) { + let params = new URLSearchParams(objSearchParams) + let newUrl = `${chat.cfg.tools.proxyUrl}/${path}?${params}` + let headers = new Headers(objHeaders) + let btoken = await bearer_transform(chat) + headers.append('Authorization', `Bearer ${btoken}`) + fetch(newUrl, { headers: headers}).then(resp => { + if (!resp.ok) { + throw new Error(`${resp.status}:${resp.statusText}`); + } + return resp.text() + }).then(data => { + gMe.toolsMgr.workers_postmessage_for_main(gMe.toolsMgr.workers.js, chatid, toolcallid, toolname, data); + }).catch((err)=>{ + gMe.toolsMgr.workers_postmessage_for_main(gMe.toolsMgr.workers.js, chatid, toolcallid, toolname, `Error:${err}`); + }) + } +} + + +/** + * Setup a proxy server dependent tool call + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {string} tag + * @param {string} chatId + * @param {string} tcPath + * @param {string} tcName + * @param {{ [x: string]: any; }} tcsData + * @param {mToolsMgr.TCSwitch} tcs + */ +async function proxyserver_tc_setup(tag, chatId, tcPath, tcName, tcsData, tcs) { + tag = `${tag}:${chatId}` + let chat = gMe.multiChat.simpleChats[chatId] + await fetch(`${chat.cfg.tools.proxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ + if (resp.statusText != 'bharatavarshe') { + console.log(`WARN:ToolWeb:${tag}:Dont forget to run the bundled local.tools/simpleproxy.py to enable me`) + return + } else { + console.log(`INFO:ToolWeb:${tag}:Enabling...`) + } + tcs[tcName] = tcsData; + }).catch(err=>console.log(`WARN:ToolWeb:${tag}:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) +} + + +// +// Fetch Url Raw +// + + +let fetchurlraw_meta = { + "type": "function", + "function": { + "name": "fetch_url_raw", + "description": "Fetch contents of the requested url (local file path / web based) through a proxy server and return the got content as is, in few seconds. Mainly useful for getting textual non binary contents", + "parameters": { + "type": "object", + "properties": { + "url":{ + "type":"string", + "description":"url of the local file / web content to fetch" + } + }, + "required": ["url"] + } + } + } + + +/** + * Implementation of the fetch url raw logic. + * Expects a simple minded proxy server to be running locally + * * listening on a configured port + * * expecting http requests + * * with a query token named url wrt the path urlraw + * which gives the actual url to fetch + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function fetchurlraw_run(chatid, toolcallid, toolname, obj) { + // maybe filter out any key other than 'url' in obj + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urlraw'); +} + + +/** + * Setup fetch_url_raw for tool calling + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {mToolsMgr.TCSwitch} tcs + * @param {string} chatId + */ +async function fetchurlraw_setup(tcs, chatId) { + return proxyserver_tc_setup('FetchUrlRaw', chatId, 'urlraw', 'fetch_url_raw', { + "handler": fetchurlraw_run, + "meta": fetchurlraw_meta, + "result": "" + }, tcs); +} + + +// +// Fetch html Text +// + + +let fetchhtmltext_meta = { + "type": "function", + "function": { + "name": "fetch_html_text", + "description": "Fetch html content from given url through a proxy server and return its text content after stripping away the html tags as well as head, script, style, header, footer, nav blocks, in few seconds", + "parameters": { + "type": "object", + "properties": { + "url":{ + "type":"string", + "description":"url of the html page that needs to be fetched and inturn unwanted stuff stripped from its contents to some extent" + } + }, + "required": ["url"] + } + } + } + + +/** + * Implementation of the fetch html text logic. + * Expects the simple minded simpleproxy server to be running locally, + * providing service for htmltext path. + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function fetchhtmltext_run(chatid, toolcallid, toolname, obj) { + // maybe filter out any key other than 'url' in obj + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'htmltext'); +} + + +/** + * Setup fetch_html_text for tool calling + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {mToolsMgr.TCSwitch} tcs + * @param {string} chatId + */ +async function fetchhtmltext_setup(tcs, chatId) { + return proxyserver_tc_setup('FetchHtmlText', chatId, 'htmltext', 'fetch_html_text', { + "handler": fetchhtmltext_run, + "meta": fetchhtmltext_meta, + "result": "" + }, tcs); +} + + +// +// Search Web Text +// + + +let searchwebtext_meta = { + "type": "function", + "function": { + "name": "search_web_text", + "description": "search web for given words and return the plain text content after stripping the html tags as well as head, script, style, header, footer, nav blocks from got html result page, in few seconds", + "parameters": { + "type": "object", + "properties": { + "words":{ + "type":"string", + "description":"the words to search for on the web" + } + }, + "required": ["words"] + } + } + } + + +/** + * Implementation of the search web text logic. Initial go. + * Builds on htmltext path service of the bundled simpleproxy.py. + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function searchwebtext_run(chatid, toolcallid, toolname, obj) { + let chat = gMe.multiChat.simpleChats[chatid] + /** @type {string} */ + let searchUrl = chat.cfg.tools.searchUrl; + searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)); + delete(obj.words) + obj['url'] = searchUrl + let headers = { 'htmltext-tag-drops': JSON.stringify(chat.cfg.tools.searchDrops) } + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'htmltext', headers); +} + + +/** + * Setup search_web_text for tool calling + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {mToolsMgr.TCSwitch} tcs + * @param {string} chatId + */ +async function searchwebtext_setup(tcs, chatId) { + return proxyserver_tc_setup('SearchWebText', chatId, 'htmltext', 'search_web_text', { + "handler": searchwebtext_run, + "meta": searchwebtext_meta, + "result": "" + }, tcs); +} + + +// +// FetchPdfText +// + + +let fetchpdftext_meta = { + "type": "function", + "function": { + "name": "fetch_pdf_as_text", + "description": "Fetch pdf from requested local file path / web url through a proxy server and return its text content after converting pdf to text, in few seconds. One is allowed to get a part of the pdf by specifying the starting and ending page numbers", + "parameters": { + "type": "object", + "properties": { + "url":{ + "type":"string", + "description":"local file path (file://) / web (http/https) based url of the pdf that will be got and inturn converted to text" + }, + "startPageNumber":{ + "type":"integer", + "description":"Specify the starting page number within the pdf, this is optional. If not specified set to first page." + }, + "endPageNumber":{ + "type":"integer", + "description":"Specify the ending page number within the pdf, this is optional. If not specified set to the last page." + }, + }, + "required": ["url"] + } + } + } + + +/** + * Implementation of the fetch pdf as text logic. + * Expects a simple minded proxy server to be running locally + * * listening on a configured port + * * expecting http requests + * * with a query token named url wrt pdftext path, + * which gives the actual url to fetch + * * gets the requested pdf and converts to text, before returning same. + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function fetchpdftext_run(chatid, toolcallid, toolname, obj) { + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'pdftext'); +} + + +/** + * Setup fetchpdftext for tool calling + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {mToolsMgr.TCSwitch} tcs + * @param {string} chatId + */ +async function fetchpdftext_setup(tcs, chatId) { + return proxyserver_tc_setup('FetchPdfAsText', chatId, 'pdftext', 'fetch_pdf_as_text', { + "handler": fetchpdftext_run, + "meta": fetchpdftext_meta, + "result": "" + }, tcs); +} + + +// +// Fetch XML Filtered +// + + +let gRSSTagDropsDefault = [ + "^rss:channel:item:guid:.*", + "^rss:channel:item:link:.*", + "^rss:channel:item:description:.*", + ".*:image:.*", + ".*:enclosure:.*" +]; + +let fetchxmlfiltered_meta = { + "type": "function", + "function": { + "name": "fetch_xml_filtered", + "description": "Fetch requested xml url through a proxy server that can optionally filter out unwanted tags and their contents. Will take few seconds", + "parameters": { + "type": "object", + "properties": { + "url":{ + "type":"string", + "description":"url of the xml file that will be fetched" + }, + "tagDropREs":{ + "type":"string", + "description":`Optionally specify a json stringified list of xml tag heirarchies to drop. + For each tag that needs to be dropped, one needs to specify regular expression of the heirarchy of tags involved, + where the tag names are always mentioned in lower case along with a : as suffix. + For example for rss feeds one could use ${JSON.stringify(gRSSTagDropsDefault)} and so...` + } + }, + "required": ["url"] + } + } + } + + +/** + * Implementation of the fetch xml filtered logic. + * Expects simpleproxy to be running at specified url and providing xmltext service + * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * @param {string} chatid + * @param {string} toolcallid + * @param {string} toolname + * @param {any} obj + */ +function fetchxmlfiltered_run(chatid, toolcallid, toolname, obj) { + let tagDropREs = obj.tagDropREs + if (tagDropREs == undefined) { + tagDropREs = JSON.stringify([]) // JSON.stringify(gRSSTagDropsDefault) + } + let headers = { 'xmlfiltered-tagdrop-res': tagDropREs } + return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'xmlfiltered', headers); +} + + +/** + * Setup fetch_xml_filtered for tool calling + * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * @param {mToolsMgr.TCSwitch} tcs + * @param {string} chatId + */ +async function fetchxmlfiltered_setup(tcs, chatId) { + return proxyserver_tc_setup('FetchXmlFiltered', chatId, 'xmlfiltered', 'fetch_xml_filtered', { + "handler": fetchxmlfiltered_run, + "meta": fetchxmlfiltered_meta, + "result": "" + }, tcs); +} + + +// +// Entry point +// + + +/** + * Used to get hold of the web worker to use for running tool/function call related code. + * @param {mChatMagic.Me} me + */ +export async function init(me) { + gMe = me +} + + +/** + * Return the tool call switch with supported / enabled / available tool calls + * Allows to verify / setup tool calls, which need to cross check things at runtime + * before getting allowed, like maybe bcas they depend on a config wrt specified + * chat session. + * @param {string} chatId + */ +export async function setup(chatId) { + /** + * @type {mToolsMgr.TCSwitch} tcs + */ + let tc_switch = {} + await fetchurlraw_setup(tc_switch, chatId) + await fetchhtmltext_setup(tc_switch, chatId) + await searchwebtext_setup(tc_switch, chatId) + await fetchpdftext_setup(tc_switch, chatId) + await fetchxmlfiltered_setup(tc_switch, chatId) + return tc_switch +} From 091262d8cf295848a4f436e2bdcfb24a50e343e1 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 21:40:37 +0530 Subject: [PATCH 437/446] SimpleSallap:ToolMCP: Initial skeletons at mcpish client Add logic to fetch tools/list from mcp server and pass it to tools manager. --- tools/server/public_simplechat/toolmcp.mjs | 303 +++------------------ 1 file changed, 44 insertions(+), 259 deletions(-) diff --git a/tools/server/public_simplechat/toolmcp.mjs b/tools/server/public_simplechat/toolmcp.mjs index 3933489923..b10487a6e1 100644 --- a/tools/server/public_simplechat/toolmcp.mjs +++ b/tools/server/public_simplechat/toolmcp.mjs @@ -1,18 +1,14 @@ //@ts-check // ALERT - Simple Stupid flow - Using from a discardable VM is better -// Helpers to handle tools/functions calling related to local/web access, pdf, etal -// which work in sync with the bundled simpleproxy.py server logic. -// Uses the js specific web worker path. +// simple mcpish client to handle tool/function calling provided by bundled simplemcp.py server logic. +// Currently it provides tool calls related to local/web access, pdf, etal // by Humans for All // // -// The simpleproxy.py server is expected to provide the below services -// urlraw - fetch the request url content as is -// htmltext - fetch the requested html content and provide plain text version -// after stripping it of tag blocks like head, script, style, header, footer, nav, ... -// pdftext - fetch the requested pdf and provide the plain text version -// xmlfiltered - fetch the requested xml content and provide a optionally filtered version of same +// The simplemcp.py mcpish server is expected to provide the below on /mcp service path +// tools/list - to get the meta of list of functions supported through simplemcp +// tools/call - to run the specified tool call // @@ -75,140 +71,51 @@ async function proxyserver_get_anyargs(chatid, toolcallid, toolname, objSearchPa /** - * Setup a proxy server dependent tool call - * NOTE: Currently the logic is setup for the bundled simpleproxy.py + * fetch supported tool calls meta data. + * NOTE: Currently the logic is setup for the bundled simplemcp.py * @param {string} tag * @param {string} chatId - * @param {string} tcPath - * @param {string} tcName - * @param {{ [x: string]: any; }} tcsData * @param {mToolsMgr.TCSwitch} tcs */ -async function proxyserver_tc_setup(tag, chatId, tcPath, tcName, tcsData, tcs) { +async function mcpserver_toolslist(tag, chatId, tcs) { tag = `${tag}:${chatId}` - let chat = gMe.multiChat.simpleChats[chatId] - await fetch(`${chat.cfg.tools.proxyUrl}/aum?url=${tcPath}.jambudweepe.akashaganga.multiverse.987654321123456789`).then(resp=>{ - if (resp.statusText != 'bharatavarshe') { - console.log(`WARN:ToolWeb:${tag}:Dont forget to run the bundled local.tools/simpleproxy.py to enable me`) + try { + let chat = gMe.multiChat.simpleChats[chatId] + + let id = new Date().getTime() + let ibody = { + jsonrpc: "2.0", + id: id, + method: "tools/list" + } + let headers = new Headers(); + headers.append("Content-Type", "application/json") + let resp = await fetch(`${chat.cfg.tools.proxyUrl}/mcp`, { + method: "POST", + headers: headers, + body: JSON.stringify(ibody), + }); + if (resp.status != 200) { + console.log`WARN:${tag}:ToolsList:MCP server says:${resp.status}:${resp.statusText}` return - } else { - console.log(`INFO:ToolWeb:${tag}:Enabling...`) } - tcs[tcName] = tcsData; - }).catch(err=>console.log(`WARN:ToolWeb:${tag}:ProxyServer missing?:${err}\nDont forget to run the bundled local.tools/simpleproxy.py`)) -} - - -// -// Fetch Url Raw -// - - -let fetchurlraw_meta = { - "type": "function", - "function": { - "name": "fetch_url_raw", - "description": "Fetch contents of the requested url (local file path / web based) through a proxy server and return the got content as is, in few seconds. Mainly useful for getting textual non binary contents", - "parameters": { - "type": "object", - "properties": { - "url":{ - "type":"string", - "description":"url of the local file / web content to fetch" - } - }, - "required": ["url"] + let obody = await resp.json() + if ((obody.results) && (obody.results.tools)) { + for(const tcmeta of obody.results.tools) { + if (!tcmeta.function) { + continue + } + console.log`INFO:${tag}:ToolsList:${tcmeta.function.name}` + tcs[tcmeta.function.name] = { + "handler": mcpserver_toolcall, + "meta": tcmeta, + "result": "" + } } } + } catch (err) { + console.log(`ERRR:${tag}:ToolsList:MCP server hs failed:${err}\nDont forget to run bundled local.tools/simplemcp.py`) } - - -/** - * Implementation of the fetch url raw logic. - * Expects a simple minded proxy server to be running locally - * * listening on a configured port - * * expecting http requests - * * with a query token named url wrt the path urlraw - * which gives the actual url to fetch - * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful - * @param {string} chatid - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function fetchurlraw_run(chatid, toolcallid, toolname, obj) { - // maybe filter out any key other than 'url' in obj - return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'urlraw'); -} - - -/** - * Setup fetch_url_raw for tool calling - * NOTE: Currently the logic is setup for the bundled simpleproxy.py - * @param {mToolsMgr.TCSwitch} tcs - * @param {string} chatId - */ -async function fetchurlraw_setup(tcs, chatId) { - return proxyserver_tc_setup('FetchUrlRaw', chatId, 'urlraw', 'fetch_url_raw', { - "handler": fetchurlraw_run, - "meta": fetchurlraw_meta, - "result": "" - }, tcs); -} - - -// -// Fetch html Text -// - - -let fetchhtmltext_meta = { - "type": "function", - "function": { - "name": "fetch_html_text", - "description": "Fetch html content from given url through a proxy server and return its text content after stripping away the html tags as well as head, script, style, header, footer, nav blocks, in few seconds", - "parameters": { - "type": "object", - "properties": { - "url":{ - "type":"string", - "description":"url of the html page that needs to be fetched and inturn unwanted stuff stripped from its contents to some extent" - } - }, - "required": ["url"] - } - } - } - - -/** - * Implementation of the fetch html text logic. - * Expects the simple minded simpleproxy server to be running locally, - * providing service for htmltext path. - * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful - * @param {string} chatid - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function fetchhtmltext_run(chatid, toolcallid, toolname, obj) { - // maybe filter out any key other than 'url' in obj - return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'htmltext'); -} - - -/** - * Setup fetch_html_text for tool calling - * NOTE: Currently the logic is setup for the bundled simpleproxy.py - * @param {mToolsMgr.TCSwitch} tcs - * @param {string} chatId - */ -async function fetchhtmltext_setup(tcs, chatId) { - return proxyserver_tc_setup('FetchHtmlText', chatId, 'htmltext', 'fetch_html_text', { - "handler": fetchhtmltext_run, - "meta": fetchhtmltext_meta, - "result": "" - }, tcs); } @@ -272,52 +179,6 @@ async function searchwebtext_setup(tcs, chatId) { } -// -// FetchPdfText -// - - -let fetchpdftext_meta = { - "type": "function", - "function": { - "name": "fetch_pdf_as_text", - "description": "Fetch pdf from requested local file path / web url through a proxy server and return its text content after converting pdf to text, in few seconds. One is allowed to get a part of the pdf by specifying the starting and ending page numbers", - "parameters": { - "type": "object", - "properties": { - "url":{ - "type":"string", - "description":"local file path (file://) / web (http/https) based url of the pdf that will be got and inturn converted to text" - }, - "startPageNumber":{ - "type":"integer", - "description":"Specify the starting page number within the pdf, this is optional. If not specified set to first page." - }, - "endPageNumber":{ - "type":"integer", - "description":"Specify the ending page number within the pdf, this is optional. If not specified set to the last page." - }, - }, - "required": ["url"] - } - } - } - - -/** - * Implementation of the fetch pdf as text logic. - * Expects a simple minded proxy server to be running locally - * * listening on a configured port - * * expecting http requests - * * with a query token named url wrt pdftext path, - * which gives the actual url to fetch - * * gets the requested pdf and converts to text, before returning same. - * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful - * @param {string} chatid - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ function fetchpdftext_run(chatid, toolcallid, toolname, obj) { return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'pdftext'); } @@ -338,86 +199,14 @@ async function fetchpdftext_setup(tcs, chatId) { } -// -// Fetch XML Filtered -// - - -let gRSSTagDropsDefault = [ - "^rss:channel:item:guid:.*", - "^rss:channel:item:link:.*", - "^rss:channel:item:description:.*", - ".*:image:.*", - ".*:enclosure:.*" -]; - -let fetchxmlfiltered_meta = { - "type": "function", - "function": { - "name": "fetch_xml_filtered", - "description": "Fetch requested xml url through a proxy server that can optionally filter out unwanted tags and their contents. Will take few seconds", - "parameters": { - "type": "object", - "properties": { - "url":{ - "type":"string", - "description":"url of the xml file that will be fetched" - }, - "tagDropREs":{ - "type":"string", - "description":`Optionally specify a json stringified list of xml tag heirarchies to drop. - For each tag that needs to be dropped, one needs to specify regular expression of the heirarchy of tags involved, - where the tag names are always mentioned in lower case along with a : as suffix. - For example for rss feeds one could use ${JSON.stringify(gRSSTagDropsDefault)} and so...` - } - }, - "required": ["url"] - } - } - } - - -/** - * Implementation of the fetch xml filtered logic. - * Expects simpleproxy to be running at specified url and providing xmltext service - * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful - * @param {string} chatid - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function fetchxmlfiltered_run(chatid, toolcallid, toolname, obj) { - let tagDropREs = obj.tagDropREs - if (tagDropREs == undefined) { - tagDropREs = JSON.stringify([]) // JSON.stringify(gRSSTagDropsDefault) - } - let headers = { 'xmlfiltered-tagdrop-res': tagDropREs } - return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'xmlfiltered', headers); -} - - -/** - * Setup fetch_xml_filtered for tool calling - * NOTE: Currently the logic is setup for the bundled simpleproxy.py - * @param {mToolsMgr.TCSwitch} tcs - * @param {string} chatId - */ -async function fetchxmlfiltered_setup(tcs, chatId) { - return proxyserver_tc_setup('FetchXmlFiltered', chatId, 'xmlfiltered', 'fetch_xml_filtered', { - "handler": fetchxmlfiltered_run, - "meta": fetchxmlfiltered_meta, - "result": "" - }, tcs); -} - - // // Entry point // /** - * Used to get hold of the web worker to use for running tool/function call related code. + * Used to get hold of the global Me instance, and through it + * the toolsManager and chat settings ... * @param {mChatMagic.Me} me */ export async function init(me) { @@ -429,7 +218,7 @@ export async function init(me) { * Return the tool call switch with supported / enabled / available tool calls * Allows to verify / setup tool calls, which need to cross check things at runtime * before getting allowed, like maybe bcas they depend on a config wrt specified - * chat session. + * chat session or handshake with mcpish server in this case and so... * @param {string} chatId */ export async function setup(chatId) { @@ -437,10 +226,6 @@ export async function setup(chatId) { * @type {mToolsMgr.TCSwitch} tcs */ let tc_switch = {} - await fetchurlraw_setup(tc_switch, chatId) - await fetchhtmltext_setup(tc_switch, chatId) - await searchwebtext_setup(tc_switch, chatId) - await fetchpdftext_setup(tc_switch, chatId) - await fetchxmlfiltered_setup(tc_switch, chatId) + await mcpserver_toolslist("ToolMCP", chatId, tc_switch) return tc_switch } From 00adebe208dc78496c83758902dec332bc7fc801 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 22:25:33 +0530 Subject: [PATCH 438/446] SimpleSallap:ToolMCP:McpishClient:P2:Initial go wrt tools/call Also fix some minor oversights wrt tools/list --- tools/server/public_simplechat/toolmcp.mjs | 165 +++++++-------------- 1 file changed, 53 insertions(+), 112 deletions(-) diff --git a/tools/server/public_simplechat/toolmcp.mjs b/tools/server/public_simplechat/toolmcp.mjs index b10487a6e1..b034d03c4b 100644 --- a/tools/server/public_simplechat/toolmcp.mjs +++ b/tools/server/public_simplechat/toolmcp.mjs @@ -1,6 +1,6 @@ //@ts-check -// ALERT - Simple Stupid flow - Using from a discardable VM is better -// simple mcpish client to handle tool/function calling provided by bundled simplemcp.py server logic. +// ALERT - Simple minded flow - Using from a discardable VM is better. +// Simple mcpish client to handle tool/function calling provided by bundled simplemcp.py server logic. // Currently it provides tool calls related to local/web access, pdf, etal // by Humans for All // @@ -32,47 +32,66 @@ async function bearer_transform(chat) { return Array.from(new Uint8Array(ab)).map(b => b.toString(16).padStart(2, '0')).join(''); } + /** - * Helper http get logic wrt the bundled SimpleProxy server, - * which helps execute a given proxy dependent tool call. - * Expects the simple minded proxy server to be running locally - * * listening on a configured port - * * expecting http requests - * * with a predefined query token and value wrt a predefined path - * NOTE: Initial go, handles textual data type. - * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful + * Implements tool call execution through a mcpish server. Initial go. + * NOTE: Currently only uses textual contents in the result. + * NOTE: Currently the logic is setup to work with bundled simplemcp.py + * ALERT: Accesses a seperate/external mcpish server, be aware and careful * @param {string} chatid * @param {string} toolcallid * @param {string} toolname - * @param {any} objSearchParams - * @param {string} path - * @param {any} objHeaders + * @param {any} obj */ -async function proxyserver_get_anyargs(chatid, toolcallid, toolname, objSearchParams, path, objHeaders={}) { +async function mcpserver_toolcall(chatid, toolcallid, toolname, obj) { let chat = gMe.multiChat.simpleChats[chatid] - if (gMe.toolsMgr.workers.js.onmessage != null) { - let params = new URLSearchParams(objSearchParams) - let newUrl = `${chat.cfg.tools.proxyUrl}/${path}?${params}` - let headers = new Headers(objHeaders) + if (gMe.toolsMgr.workers.js.onmessage == null) { + return + } + try { + let newUrl = `${chat.cfg.tools.proxyUrl}/mcp` + let headers = new Headers(); let btoken = await bearer_transform(chat) headers.append('Authorization', `Bearer ${btoken}`) - fetch(newUrl, { headers: headers}).then(resp => { - if (!resp.ok) { - throw new Error(`${resp.status}:${resp.statusText}`); + headers.append("Content-Type", "application/json") + let ibody = { + jsonrpc: "2.0", + id: toolcallid, + method: "tools/call", + params: { + name: toolname, + arguments: obj } - return resp.text() - }).then(data => { - gMe.toolsMgr.workers_postmessage_for_main(gMe.toolsMgr.workers.js, chatid, toolcallid, toolname, data); - }).catch((err)=>{ - gMe.toolsMgr.workers_postmessage_for_main(gMe.toolsMgr.workers.js, chatid, toolcallid, toolname, `Error:${err}`); - }) + } + let resp = await fetch(newUrl, { + method: "POST", + headers: headers, + body: JSON.stringify(ibody), + }); + if (!resp.ok) { + throw new Error(`${resp.status}:${resp.statusText}`); + } + let obody = await resp.json() + let textResult = "" + if ((obody.result) && (obody.result.content)) { + for(const tcr of obody.result.content) { + if (!tcr.text) { + continue + } + textResult += `\n\n${tcr.text}` + } + } + gMe.toolsMgr.workers_postmessage_for_main(gMe.toolsMgr.workers.js, chatid, toolcallid, toolname, textResult); + } catch (err) { + gMe.toolsMgr.workers_postmessage_for_main(gMe.toolsMgr.workers.js, chatid, toolcallid, toolname, `Error:${err}`); } } /** - * fetch supported tool calls meta data. - * NOTE: Currently the logic is setup for the bundled simplemcp.py + * Fetch supported tool calls meta data from a mcpish server. + * NOTE: Currently the logic is setup to work with bundled simplemcp.py + * ALERT: Accesses a seperate/external mcpish server, be aware and careful * @param {string} tag * @param {string} chatId * @param {mToolsMgr.TCSwitch} tcs @@ -89,6 +108,8 @@ async function mcpserver_toolslist(tag, chatId, tcs) { method: "tools/list" } let headers = new Headers(); + let btoken = await bearer_transform(chat) + headers.append('Authorization', `Bearer ${btoken}`) headers.append("Content-Type", "application/json") let resp = await fetch(`${chat.cfg.tools.proxyUrl}/mcp`, { method: "POST", @@ -100,8 +121,8 @@ async function mcpserver_toolslist(tag, chatId, tcs) { return } let obody = await resp.json() - if ((obody.results) && (obody.results.tools)) { - for(const tcmeta of obody.results.tools) { + if ((obody.result) && (obody.result.tools)) { + for(const tcmeta of obody.result.tools) { if (!tcmeta.function) { continue } @@ -119,86 +140,6 @@ async function mcpserver_toolslist(tag, chatId, tcs) { } -// -// Search Web Text -// - - -let searchwebtext_meta = { - "type": "function", - "function": { - "name": "search_web_text", - "description": "search web for given words and return the plain text content after stripping the html tags as well as head, script, style, header, footer, nav blocks from got html result page, in few seconds", - "parameters": { - "type": "object", - "properties": { - "words":{ - "type":"string", - "description":"the words to search for on the web" - } - }, - "required": ["words"] - } - } - } - - -/** - * Implementation of the search web text logic. Initial go. - * Builds on htmltext path service of the bundled simpleproxy.py. - * ALERT: Accesses a seperate/external web proxy/caching server, be aware and careful - * @param {string} chatid - * @param {string} toolcallid - * @param {string} toolname - * @param {any} obj - */ -function searchwebtext_run(chatid, toolcallid, toolname, obj) { - let chat = gMe.multiChat.simpleChats[chatid] - /** @type {string} */ - let searchUrl = chat.cfg.tools.searchUrl; - searchUrl = searchUrl.replace("SEARCHWORDS", encodeURIComponent(obj.words)); - delete(obj.words) - obj['url'] = searchUrl - let headers = { 'htmltext-tag-drops': JSON.stringify(chat.cfg.tools.searchDrops) } - return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'htmltext', headers); -} - - -/** - * Setup search_web_text for tool calling - * NOTE: Currently the logic is setup for the bundled simpleproxy.py - * @param {mToolsMgr.TCSwitch} tcs - * @param {string} chatId - */ -async function searchwebtext_setup(tcs, chatId) { - return proxyserver_tc_setup('SearchWebText', chatId, 'htmltext', 'search_web_text', { - "handler": searchwebtext_run, - "meta": searchwebtext_meta, - "result": "" - }, tcs); -} - - -function fetchpdftext_run(chatid, toolcallid, toolname, obj) { - return proxyserver_get_anyargs(chatid, toolcallid, toolname, obj, 'pdftext'); -} - - -/** - * Setup fetchpdftext for tool calling - * NOTE: Currently the logic is setup for the bundled simpleproxy.py - * @param {mToolsMgr.TCSwitch} tcs - * @param {string} chatId - */ -async function fetchpdftext_setup(tcs, chatId) { - return proxyserver_tc_setup('FetchPdfAsText', chatId, 'pdftext', 'fetch_pdf_as_text', { - "handler": fetchpdftext_run, - "meta": fetchpdftext_meta, - "result": "" - }, tcs); -} - - // // Entry point // @@ -223,7 +164,7 @@ export async function init(me) { */ export async function setup(chatId) { /** - * @type {mToolsMgr.TCSwitch} tcs + * @type {mToolsMgr.TCSwitch} */ let tc_switch = {} await mcpserver_toolslist("ToolMCP", chatId, tc_switch) From 631aa7fb36d58e154c59e93c437be285fcc28afb Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Sun, 7 Dec 2025 22:39:21 +0530 Subject: [PATCH 439/446] SimpleSallap:SimpleMCP/ToolMCP: bring in wrt tools manager Setup to test initial go of the mcpish server and client logics --- tools/server/public_simplechat/docs/changelog.md | 13 +++++++++++-- tools/server/public_simplechat/toolmcp.mjs | 4 ++-- tools/server/public_simplechat/tools.mjs | 8 ++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 8cd4e121eb..805c76a21d 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -319,12 +319,21 @@ Chat Session specific settings * switch to a Dicty DataClass based Config with better type validation and usage, instead of literal dict++ * ToolCall, ToolManager and related classes based flow wrt the tool calls. * all existing tool calls duplicated and updated to support and build on this new flow. -* Initial skeleton towards SimpleMCP, a post and json rpcish based handshake flow, so that tool calls supported - through SimpleProxy can be exposed through a MCP standardish mechanism. +* Initial skeleton towards SimpleMCP, a mcpish server, which uses post and json rpcish based handshake flow, + so that tool calls supported through SimpleProxy can be exposed through a MCP standardish mechanism. * can allow others beyond AnveshikaSallap client to use the corresponding tool calls * can allow AnveshikaSallap client to support other MCP servers and their exposed tool calls in future. Mcp command tools/list implemented and verified at a basic level Mcp command tools/call implemented, need to verify and update the initial go version +* Initial skeleton towards ToolMCP, a mcpish client logic + Mcp command tools/list handshake implemented, need to verify and update this initial go + Mcp command tools/call handshake implemented, need to verify and update this initial go +* MCPish and not full fledged MCP currently + * no initialise command handshake + * use seconds since unix epoch or toolcall id, as the case maybe, as the id wrt json-rpc calls + * the tools/list response mirrors the openai rest api convention rather than mcp convention + * uses the additional type: function wrapper wrt tool call meta + * uses the keyword parameters instead of inputschema or so ## ToDo diff --git a/tools/server/public_simplechat/toolmcp.mjs b/tools/server/public_simplechat/toolmcp.mjs index b034d03c4b..689bab457f 100644 --- a/tools/server/public_simplechat/toolmcp.mjs +++ b/tools/server/public_simplechat/toolmcp.mjs @@ -117,7 +117,7 @@ async function mcpserver_toolslist(tag, chatId, tcs) { body: JSON.stringify(ibody), }); if (resp.status != 200) { - console.log`WARN:${tag}:ToolsList:MCP server says:${resp.status}:${resp.statusText}` + console.log(`WARN:${tag}:ToolsList:MCP server says:${resp.status}:${resp.statusText}`) return } let obody = await resp.json() @@ -126,7 +126,7 @@ async function mcpserver_toolslist(tag, chatId, tcs) { if (!tcmeta.function) { continue } - console.log`INFO:${tag}:ToolsList:${tcmeta.function.name}` + console.log(`INFO:${tag}:ToolsList:${tcmeta.function.name}`) tcs[tcmeta.function.name] = { "handler": mcpserver_toolcall, "meta": tcmeta, diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index bee4561e2c..2786807194 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -7,6 +7,7 @@ import * as tjs from './tooljs.mjs' import * as tweb from './toolweb.mjs' +import * as tmcp from './toolmcp.mjs' import * as tdb from './tooldb.mjs' import * as tai from './toolai.mjs' import * as mChatMagic from './simplechat.js' @@ -56,6 +57,7 @@ export class ToolsManager { tcM.push(tdb.init(me)) tcM.push(tai.init(me)) tcM.push(tweb.init(me)) + tcM.push(tmcp.init(me)) return Promise.all(tcM) } @@ -98,6 +100,12 @@ export class ToolsManager { chat.cfg.tools.toolNames.push(key) } }) + await tmcp.setup(chatId).then((tcs)=>{ + for (const key in tcs) { + this.tc_switchs[chatId][key] = tcs[key] + chat.cfg.tools.toolNames.push(key) + } + }) } /** From 728202ba4f404724ce079191b5419286858970a0 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 8 Dec 2025 03:44:54 +0530 Subject: [PATCH 440/446] SimpleSallap:SimpleMCP:TCWeb:SearchWeb tool call Move the search web tool call also from previous js client + python simpleproxy based logic to the new simplemcp based logic, while following the same overall logic of reusing the HtmlText's equiv logic with now predefined and user non-replacable (at runtime) tagDrops and template urls --- .../local.tools/simplemcp.py | 1 + .../public_simplechat/local.tools/tcweb.py | 75 ++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 0233c1f2ce..5a23c83f4c 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -207,6 +207,7 @@ def setup_toolmanager(): gMe.op.toolManager.tc_add("fetch_url_raw", mTCWeb.TCUrlRaw("fetch_url_raw")) gMe.op.toolManager.tc_add("fetch_html_text", mTCWeb.TCHtmlText("fetch_html_text")) gMe.op.toolManager.tc_add("fetch_xml_filtered", mTCWeb.TCXmlFiltered("fetch_xml_filtered")) + gMe.op.toolManager.tc_add("search_web_text", mTCWeb.TCSearchWeb("search_web_text")) if mTCPdf.ok(): gMe.op.toolManager.tc_add("fetch_pdf_text", mTCPdf.TCPdfText("fetch_pdf_text")) diff --git a/tools/server/public_simplechat/local.tools/tcweb.py b/tools/server/public_simplechat/local.tools/tcweb.py index fe4b4239c3..f008d69c5f 100644 --- a/tools/server/public_simplechat/local.tools/tcweb.py +++ b/tools/server/public_simplechat/local.tools/tcweb.py @@ -7,7 +7,9 @@ import debug import filemagic as mFile import json import re -from typing import Any, cast +import urllib.parse +from typing import Any, cast, Optional +from dataclasses import dataclass import toolcalls as mTC @@ -222,6 +224,77 @@ class TCHtmlText(mTC.ToolCall): return mTC.TCOutResponse(False, 502, f"WARN:HtmlText:Failed:{exc}") + +@dataclass(frozen=True) +class SearchEngine: + template: str + drop: Optional[list[dict[str, str]]] = None + +#Few web search engine url template strings. +#The SEARCHWORDS keyword will get replaced by the actual user specified search words at runtime. +gSearchEngines: dict[str, SearchEngine] = { + "duckduckgo": SearchEngine( + "https://duckduckgo.com/html/?q=SEARCHWORDS", + [ { 'tag': 'div', 'id': "header" } ] + ), + "_bing": SearchEngine( + "https://www.bing.com/search?q=SEARCHWORDS" # doesnt seem to like google chrome clients in particular + ), + "brave": SearchEngine( + "https://search.brave.com/search?q=SEARCHWORDS", + ), + "_google": SearchEngine( + "https://www.google.com/search?q=SEARCHWORDS", # doesnt seem to like any client in general + ), +} + +class TCSearchWeb(mTC.ToolCall): + + def tcf_meta(self) -> mTC.TCFunction: + return mTC.TCFunction( + self.name, + "Search web for given words and return plain text content after stripping html tags as well as head, script, style, header, footer, nav blocks from got html result page, in few seconds", + mTC.TCInParameters( + "object", + { + "words": mTC.TCInProperty ( + "string", + "The words to search for on the web" + ), + "searchEngine": mTC.TCInProperty( + "string", + f"Name of the search engine to use. The supported search engines are {list(gSearchEngines.keys())}. The engine names prefixed with _ may not work many a times" + ) + }, + [ "words", "searchEngine" ] + ) + ) + + def tc_handle(self, args: mTC.TCInArgs, inHeaders: mTC.HttpHeaders) -> mTC.TCOutResponse: + try: + words = args['words'] + engineName = args['searchEngine'] + if not engineName: + engineName = list(gSearchEngines.keys())[0] + searchEngine = gSearchEngines[engineName] + searchUrl = searchEngine.template.replace("SEARCHWORDS", urllib.parse.quote(words, safe='')) + # Get requested url + got = handle_urlreq(searchUrl, inHeaders, "HandleTCSearchWeb") + if not got.callOk: + return got + # Extract Text + tagDrops = searchEngine.drop + if not tagDrops: + tagDrops = [] + textHtml = TextHtmlParser(tagDrops) + textHtml.feed(got.contentData.decode('utf-8')) + debug.dump({ 'op': 'MCPWeb.SearchWeb', 'RawText': 'yes', 'StrippedText': 'yes' }, { 'RawText': textHtml.text, 'StrippedText': textHtml.get_stripped_text() }) + return mTC.TCOutResponse(True, got.statusCode, got.statusMsg, got.contentType, textHtml.get_stripped_text().encode('utf-8')) + except Exception as exc: + return mTC.TCOutResponse(False, 502, f"WARN:SearchWeb:Failed:{exc}") + + + class XMLFilterParser(html.parser.HTMLParser): """ A simple minded logic used to strip xml content of From fffa6a808cc6c5111fc3f13af17a82d5b3738551 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 8 Dec 2025 04:17:33 +0530 Subject: [PATCH 441/446] SimpleSallap:SimpleMCP:Cleanup in general --- tools/server/public_simplechat/local.tools/simplemcp.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 5a23c83f4c..75ed5cf5e7 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -56,7 +56,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): Common headers to include in responses from this server """ self.send_header('Access-Control-Allow-Origin', '*') - self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') + self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS') self.send_header('Access-Control-Allow-Headers', '*') self.end_headers() @@ -66,6 +66,8 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): so that the common headers mentioned above can get added to them else CORS failure will be triggered by the browser on fetch from browser. """ + if not message: + message = "" print(f"WARN:PH:SendError:{code}:{message}") self.send_response(code, message) self.send_headers_common() @@ -174,6 +176,9 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): print(f"ERRR:PH:ThePOST:{traceback.format_exception_only(sys.exception())}") self.send_error(500, f"ERRR: handling request") + def do_GET(self): + self.send_error(400, "Bad request") + def do_OPTIONS(self): """ Handle OPTIONS for CORS preflights (just in case from browser) From ff710903a95b41a37e644f3cd114501bd082af18 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 8 Dec 2025 04:33:34 +0530 Subject: [PATCH 442/446] SimpleSallap:SimpleMCP:Move out simpleproxy and its helpers --- .../public_simplechat/{ => MOVED}/local.tools/pdfmagic.py | 0 .../{ => MOVED}/local.tools/simpleproxy.py | 0 .../public_simplechat/{ => MOVED}/local.tools/webmagic.py | 0 tools/server/public_simplechat/{ => MOVED}/toolweb.mjs | 0 .../local.tools/{simpleproxy.json => simplemcp.json} | 0 tools/server/public_simplechat/tools.mjs | 8 -------- 6 files changed, 8 deletions(-) rename tools/server/public_simplechat/{ => MOVED}/local.tools/pdfmagic.py (100%) rename tools/server/public_simplechat/{ => MOVED}/local.tools/simpleproxy.py (100%) rename tools/server/public_simplechat/{ => MOVED}/local.tools/webmagic.py (100%) rename tools/server/public_simplechat/{ => MOVED}/toolweb.mjs (100%) rename tools/server/public_simplechat/local.tools/{simpleproxy.json => simplemcp.json} (100%) diff --git a/tools/server/public_simplechat/local.tools/pdfmagic.py b/tools/server/public_simplechat/MOVED/local.tools/pdfmagic.py similarity index 100% rename from tools/server/public_simplechat/local.tools/pdfmagic.py rename to tools/server/public_simplechat/MOVED/local.tools/pdfmagic.py diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.py b/tools/server/public_simplechat/MOVED/local.tools/simpleproxy.py similarity index 100% rename from tools/server/public_simplechat/local.tools/simpleproxy.py rename to tools/server/public_simplechat/MOVED/local.tools/simpleproxy.py diff --git a/tools/server/public_simplechat/local.tools/webmagic.py b/tools/server/public_simplechat/MOVED/local.tools/webmagic.py similarity index 100% rename from tools/server/public_simplechat/local.tools/webmagic.py rename to tools/server/public_simplechat/MOVED/local.tools/webmagic.py diff --git a/tools/server/public_simplechat/toolweb.mjs b/tools/server/public_simplechat/MOVED/toolweb.mjs similarity index 100% rename from tools/server/public_simplechat/toolweb.mjs rename to tools/server/public_simplechat/MOVED/toolweb.mjs diff --git a/tools/server/public_simplechat/local.tools/simpleproxy.json b/tools/server/public_simplechat/local.tools/simplemcp.json similarity index 100% rename from tools/server/public_simplechat/local.tools/simpleproxy.json rename to tools/server/public_simplechat/local.tools/simplemcp.json diff --git a/tools/server/public_simplechat/tools.mjs b/tools/server/public_simplechat/tools.mjs index 2786807194..5c05806c15 100644 --- a/tools/server/public_simplechat/tools.mjs +++ b/tools/server/public_simplechat/tools.mjs @@ -6,7 +6,6 @@ import * as tjs from './tooljs.mjs' -import * as tweb from './toolweb.mjs' import * as tmcp from './toolmcp.mjs' import * as tdb from './tooldb.mjs' import * as tai from './toolai.mjs' @@ -56,7 +55,6 @@ export class ToolsManager { tcM.push(tjs.init(me)) tcM.push(tdb.init(me)) tcM.push(tai.init(me)) - tcM.push(tweb.init(me)) tcM.push(tmcp.init(me)) return Promise.all(tcM) } @@ -94,12 +92,6 @@ export class ToolsManager { chat.cfg.tools.toolNames.push(key) } }) - await tweb.setup(chatId).then((tcs)=>{ - for (const key in tcs) { - this.tc_switchs[chatId][key] = tcs[key] - chat.cfg.tools.toolNames.push(key) - } - }) await tmcp.setup(chatId).then((tcs)=>{ for (const key in tcs) { this.tc_switchs[chatId][key] = tcs[key] From 9039a917d05f613c7e01b40c9e9d7e24fc52ad49 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 8 Dec 2025 05:04:49 +0530 Subject: [PATCH 443/446] SimpleSallap:SimpleMCP: Cleanup, Readme updates Update the documentation a bit wrt switch from simpleproxy to simplemcp with mcp-ish kind of handshake between the chat client and simplemcp. Rename proxyUrl and related to mcpServerUrl and mcpServerAuth. Now include the path in the url itself, given that in future, we may want to allow the chat client logic to handshake with other mcp servers, which may expose their services through a different path or so. Drop SearchEngine related config entries from chat session settings, given that its now controlled directly in SimpleMCP. --- .../public_simplechat/docs/changelog.md | 2 + .../server/public_simplechat/docs/details.md | 104 +++++++++--------- .../public_simplechat/local.tools/debug.py | 4 +- .../local.tools/filemagic.py | 8 +- .../local.tools/simplemcp.py | 4 +- .../public_simplechat/local.tools/tcweb.py | 5 - .../local.tools/test-gen-self-signed.sh | 2 +- tools/server/public_simplechat/main.js | 2 +- tools/server/public_simplechat/readme.md | 34 +++--- tools/server/public_simplechat/simplechat.js | 28 +---- tools/server/public_simplechat/toolmcp.mjs | 6 +- 11 files changed, 91 insertions(+), 108 deletions(-) diff --git a/tools/server/public_simplechat/docs/changelog.md b/tools/server/public_simplechat/docs/changelog.md index 805c76a21d..f01b89580c 100644 --- a/tools/server/public_simplechat/docs/changelog.md +++ b/tools/server/public_simplechat/docs/changelog.md @@ -328,12 +328,14 @@ Chat Session specific settings * Initial skeleton towards ToolMCP, a mcpish client logic Mcp command tools/list handshake implemented, need to verify and update this initial go Mcp command tools/call handshake implemented, need to verify and update this initial go + Minimal cross check wrt tools/list and tools/call. * MCPish and not full fledged MCP currently * no initialise command handshake * use seconds since unix epoch or toolcall id, as the case maybe, as the id wrt json-rpc calls * the tools/list response mirrors the openai rest api convention rather than mcp convention * uses the additional type: function wrapper wrt tool call meta * uses the keyword parameters instead of inputschema or so +* Retire the previous simpleproxy.py and its related helpers, including ones running in browser env. ## ToDo diff --git a/tools/server/public_simplechat/docs/details.md b/tools/server/public_simplechat/docs/details.md index 7ff53e7860..6ce83fce03 100644 --- a/tools/server/public_simplechat/docs/details.md +++ b/tools/server/public_simplechat/docs/details.md @@ -40,7 +40,7 @@ to have independent chat sessions with different instances of llama-server and o For GenAi/LLM models supporting tool / function calling, allows one to interact with them and explore use of ai driven augmenting of the knowledge used for generating answers as well as for cross checking ai generated answers logically / programatically and by checking with other sources and lot more by making using of the -simple yet useful predefined tools / functions provided by this client web ui. The end user is provided full +simple yet useful predefined tools / functions provided by this chat client. The end user is provided full control over tool calling and response submitting. For GenAi/LLM models which support reasoning, the thinking of the model will be shown to the end user as the @@ -92,28 +92,28 @@ remember to * use a GenAi/LLM model which supports tool calling. * if fetch web page, web search, pdf-to-text, ... tool call is needed remember to run bundled - local.tools/simpleproxy.py + local.tools/simplemcp.py helper along with its config file, before using/loading this client ui through a browser - * cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config simpleproxy.json + * cd tools/server/public_simplechat/local.tools; python3 ./simplemcp.py --config simplemcp.json - * remember that this is a relatively minimal dumb proxy logic which can fetch html or pdf content and - inturn optionally provide plain text version of the content by stripping off non textual/core contents. + * remember that this is a relatively minimal dumb mcp(ish) server logic with few builtin tool calls + related to fetching raw html or stripped plain text equivalent or pdf text content. Be careful when accessing web through this and use it only with known safe sites. - * look into local.tools/simpleproxy.json for specifying + * look into local.tools/simplemcp.json for specifying - * the white list of allowed.schemes + * the white list of acl.schemes * you may want to use this to disable local file access and or disable http access, and inturn retaining only https based urls or so. - * the white list of allowed.domains + * the white list of acl.domains * review and update this to match your needs. - * the shared bearer token between simpleproxy server and client ui + * the shared bearer token between simplemcp server and chat client * the public certificate and private key files to enable https mode - * sec.certfile and sec.keyfile + * sec.certFile and sec.keyFile * other builtin tool / function calls like datetime, calculator, javascript runner, DataStore, - external ai dont require the simpleproxy.py helper. + external ai dont require this bundled simplemcp.py helper. ### for vision models @@ -204,11 +204,11 @@ Once inside * use the clear button to clear the currently active chat session. * just refresh the page, to reset wrt the chat history and system prompts across chat sessions and start afresh. - * This also helps if you had forgotten to start the bundled simpleproxy.py server before hand. - Start the simpleproxy.py server and refresh the client ui page, to get access to web access + * This also helps if you had forgotten to start the bundled simplemcp.py server before hand. + Start the simplemcp.py server and refresh the client ui page, to get access to web access related tool calls. - * starting new chat session, after starting simpleproxy, will also give access to tool calls - exposed by simpleproxy, in that new chat session. + * starting new chat session, after starting simplemcp, will also give access to tool calls + exposed by simplemcp, in that new chat session. * if you refreshed/cleared unknowingly, you can use the Restore feature to try load previous chat session and resume that session. This uses a basic local auto save logic that is in there. @@ -292,17 +292,11 @@ It is attached to the document object. Some of these can also be updated using t * remember to enable this only for GenAi/LLM models which support tool/function calling. - * proxyUrl - specify the address for the running instance of bundled local.tools/simpleproxy.py + * mcpServerUrl - specify the address for the running instance of bundled local.tools/simplemcp.py - * proxyAuthInsecure - shared token between simpleproxy.py server and client ui, for accessing service provided by it. + * mcpServerAuth - shared token between simplemcp.py server and client ui, for accessing service provided by it. - * Shared token is currently hashed with the current year and inturn handshaked over the network. In future if required one could also include a dynamic token provided by simpleproxy server during /aum handshake and running counter or so into hashed token. ALERT: However do remember that currently by default handshake occurs over http and not https, so others can snoop the network and get token. Per client ui running counter and random dynamic token can help mitigate things to some extent, if required in future. Remember to enable https mode by specifying a valid public certificate and private key. - - * searchUrl - specify the search engine's search url template along with the tag SEARCHWORDS in place where the search words should be substituted at runtime. - - * searchDrops - allows one to drop contents of html tags with specified id from the plain text search result. - - * specify a list of dicts, where each dict should contain a 'tag' entry specifying the tag to filter like div or p or ... and also a 'id' entry which specifies the id of interest. + * Shared token is currently hashed with the current year and inturn handshaked over the network. In future if required one could also include a dynamic token provided by simplemcp server during say a special /aum handshake and running counter or so into hashed token. ALERT: However do remember that currently by default handshake occurs over http and not https, so others can snoop the network and get token. Per client ui running counter and random dynamic token can help mitigate things to some extent, if required in future. Remember to enable https mode by specifying a valid public certificate and private key. * iResultMaxDataLength - specify what amount of any tool call result should be sent back to the ai engine server. @@ -316,8 +310,8 @@ It is attached to the document object. Some of these can also be updated using t * setting this value to 0 (default), disables auto logic, so that end user can review the tool calls requested by ai and if needed even modify them, before triggering/executing them as well as review and modify results generated by the tool call, before submitting them back to the ai. - * this is specified in seconds, so that users by default will normally not overload any website through the proxy server. - + * this is specified in seconds, so that users by default will normally not overload any website through the bundled mcp server. + 1. the builtin tools' meta data is sent to the ai model in the requests sent to it. 2. inturn if the ai model requests a tool call to be made, the same will be done and the response sent back to the ai model, under user control, by default. @@ -423,7 +417,7 @@ host / router, tools etal, for basic useful tools/functions like calculator, cod Additionally if users want to work with web content or pdf content as part of their ai chat session, Few functions related to web access as well as pdf access which work with a included -python based simple proxy server have been implemented. +python based simple mcp server (rather mcp-ish) have been implemented. This can allow end users to use some basic yet useful tool calls to enhance their ai chat sessions to some extent. It also provides for a simple minded exploration of tool calling @@ -514,17 +508,25 @@ shared web worker scope isnt isolated. Either way always remember to cross check tool requests and generated responses when using tool calling. -##### using bundled simpleproxy.py (helps bypass browser cors restriction, ...) +##### using bundled simplemcp.py (helps bypass browser cors restriction, ...) -* fetch_url_raw - fetch contents of the requested url through a proxy server +* fetch_url_raw - fetch contents of the requested url through/using mcp server -* fetch_html_text - fetch text parts of the html content from the requested url through a proxy server. +* fetch_html_text - fetch text parts of the html content from the requested url through a mcp server. Related logic tries to strip html response of html tags and also head, script, style, header,footer, - nav, ... blocks. + nav, ... blocks (which are usually not needed). * search_web_text - search for the specified words using the configured search engine and return the plain textual content from the search result page. + From the bundled simplemcp.py one can control the search engine details like + + * template - specify the search engine's search url template along with the tag SEARCHWORDS in place where the search words should be substituted at runtime. + + * drops - allows one to drop contents of html tags with specified id from the final plain text search result. + + * specify a list of dicts, where each dict should contain a 'tag' entry specifying the tag to filter like div or p or ... and also a 'id' entry which specifies the id of interest. + * fetch_pdf_as_text - fetch/read specified pdf file and extract its textual content * this depends on the pypdf python based open source library * create a outline of titles along with numbering if the pdf contains a outline/toc @@ -539,33 +541,29 @@ Either way always remember to cross check tool requests and generated responses * .*:tagname:.* * rather the tool call meta data passed to ai model explains the same and provides a sample. -the above set of web related tool calls work by handshaking with a bundled simple local web proxy -(/caching in future) server logic, this helps bypass the CORS restrictions applied if trying to +the above set of web related tool calls work by handshaking with a bundled simple local mcp (may be +add caching in future) server logic, this helps bypass the CORS restrictions applied if trying to directly fetch from the browser js runtime environment. Local file access is also enabled for web fetch and pdf tool calls, if one uses the file:/// scheme -in the url, so be careful as to where and under which user id the simple proxy will be run. +in the url, so be careful as to where and under which user id the simple mcp will be run. -* one can always disable local file access by removing 'file' from the list of allowed.schemes in -simpleproxy.json config file. +* one can always disable local file access by removing 'file' from the list of acl.schemes in +simplemcp.json config file. -Implementing some of the tool calls through the simpleproxy.py server and not directly in the browser +Implementing some of the tool calls through the simplemcp.py server and not directly in the browser js env, allows one to isolate the core of these logic within a discardable VM or so and also if required -in a different region or so, by running the simpleproxy.py in such a vm. +in a different region or so, by running the simplemcp.py in such a vm. -Depending on the path specified wrt the proxy server, it executes the corresponding logic. Like if -htmltext path is used (and not urlraw), the logic in addition to fetching content from given url, it -tries to convert html content into equivalent plain text content to some extent in a simple minded -manner by dropping head block as well as all scripts/styles/footers/headers/nav blocks and inturn -also dropping the html tags. Similarly for pdftext. +Depending on path and method specified using json-rpc wrt the mcp server, it executes corresponding logic. -The client ui logic does a simple check to see if the bundled simpleproxy is running at specified -proxyUrl before enabling these web and related tool calls. +This chat client logic does a simple check to see if bundled simplemcp is running at specified +mcpServerUrl and in turn the provided tool calls like those related to web / pdf etal. -The bundled simple proxy +The bundled simple mcp * can be found at - * tools/server/public_simplechat/local.tools/simpleproxy.py + * tools/server/public_simplechat/local.tools/simplemcp.py * it provides for a basic white list of allowed domains to access, to be specified by the end user. This should help limit web access to a safe set of sites determined by the end user. There is also @@ -574,14 +572,14 @@ The bundled simple proxy * by default runs in http mode. If valid sec.keyfile and sec.certfile options are specified, logic will run in https mode. - * Remember to also update tools->proxyUrl wrt the chat session settings. + * Remember to also update tools->mcpServerUrl wrt the chat session settings. * the new url will be used for subsequent tool handshakes, however remember that the list of - tool calls supported wont get updated, till the client web ui is refreshed/reloaded. + tool calls supported wont get updated, till this chat client web ui is refreshed/reloaded. * it tries to mimic the client/browser making the request to it by propogating header entries like - user-agent, accept and accept-language from the got request to the generated request during proxying - so that websites will hopefully respect the request rather than blindly rejecting it as coming from - a non-browser entity. + user-agent, accept and accept-language from the got request to the generated request during this + mcp based proxying, so that websites will hopefully respect the request rather than blindly + rejecting it as coming from a non-browser entity. * allows getting specified local or web based pdf files and extract their text content for ai to use @@ -615,7 +613,7 @@ Update the tc_switch to include a object entry for the tool, which inturn includ * the result key (was used previously, may use in future, but for now left as is) Look into tooljs.mjs, toolai.mjs and tooldb.mjs for javascript and inturn browser web worker based -tool calls and toolweb.mjs for the simpleproxy.py based tool calls. +tool calls and toolweb.mjs for the simplemcp.py based tool calls. #### OLD: Mapping tool calls and responses to normal assistant - user chat flow diff --git a/tools/server/public_simplechat/local.tools/debug.py b/tools/server/public_simplechat/local.tools/debug.py index bf42be631c..d8923c110a 100644 --- a/tools/server/public_simplechat/local.tools/debug.py +++ b/tools/server/public_simplechat/local.tools/debug.py @@ -16,9 +16,9 @@ def dump(meta: dict, data: dict): if not gMe['--debug']: return timeTag = f"{time.time():0.12f}" - with open(f"/tmp/simpleproxy.{timeTag}.meta", '+w') as f: + with open(f"/tmp/simplemcp.{timeTag}.meta", '+w') as f: for k in meta: f.write(f"\n\n\n\n{k}:{meta[k]}\n\n\n\n") - with open(f"/tmp/simpleproxy.{timeTag}.data", '+w') as f: + with open(f"/tmp/simplemcp.{timeTag}.data", '+w') as f: for k in data: f.write(f"\n\n\n\n{k}:{data[k]}\n\n\n\n") diff --git a/tools/server/public_simplechat/local.tools/filemagic.py b/tools/server/public_simplechat/local.tools/filemagic.py index 4ed834f8cf..d24deac6bd 100644 --- a/tools/server/public_simplechat/local.tools/filemagic.py +++ b/tools/server/public_simplechat/local.tools/filemagic.py @@ -14,7 +14,13 @@ def get_from_web(url: str, tag: str, inContentType: str, inHeaders: mTC.HttpHead Get the url specified from web. If passed header doesnt contain certain useful http header entries, - some predefined defaults will be used in place. + some predefined defaults will be used in place. This includes User-Agent, + Accept-Language and Accept. + + One should ideally pass the header got in the request being proxied, so as + to help one to try mimic the real client, whose request we are proxying. + In case a header is missing in the got request, fallback to using some + possibly ok enough defaults. """ try: hUA = inHeaders.get('User-Agent', None) diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 75ed5cf5e7..1df6dab828 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -204,8 +204,8 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): def setup_toolmanager(): """ - Handle requests to aum path, which is used in a simple way to - verify that one is communicating with this proxy server + Setup the ToolCall helpers. + Ensure the toolcall module is ok before setting up its tool calls. """ gMe.op.toolManager = mTC.ToolManager() if mTCWeb.ok(): diff --git a/tools/server/public_simplechat/local.tools/tcweb.py b/tools/server/public_simplechat/local.tools/tcweb.py index f008d69c5f..a0263d29c5 100644 --- a/tools/server/public_simplechat/local.tools/tcweb.py +++ b/tools/server/public_simplechat/local.tools/tcweb.py @@ -20,11 +20,6 @@ def handle_urlreq(url: str, inHeaders: mTC.HttpHeaders, tag: str): Verify the url being requested is allowed. - Include User-Agent, Accept-Language and Accept in the generated request using - equivalent values got in the request being proxied, so as to try mimic the - real client, whose request we are proxying. In case a header is missing in the - got request, fallback to using some possibly ok enough defaults. - Fetch the requested url. """ tag=f"UrlReq:{tag}" diff --git a/tools/server/public_simplechat/local.tools/test-gen-self-signed.sh b/tools/server/public_simplechat/local.tools/test-gen-self-signed.sh index 53183fbd61..204b906488 100755 --- a/tools/server/public_simplechat/local.tools/test-gen-self-signed.sh +++ b/tools/server/public_simplechat/local.tools/test-gen-self-signed.sh @@ -1,3 +1,3 @@ -openssl req -new -x509 -days 365 -noenc -out /tmp/test-cert.crt -keyout /tmp/test-priv.key -subj '/C=IN/ST=TEST/O=AnveshikaSallap/OU=SimpleProxyTEST/CN=127.0.0.1' -addext "subjectAltName = DNS:localhost, IP:127.0.0.1" +openssl req -new -x509 -days 365 -noenc -out /tmp/test-cert.crt -keyout /tmp/test-priv.key -subj '/C=IN/ST=TEST/O=AnveshikaSallap/OU=SimpleMCPTEST/CN=127.0.0.1' -addext "subjectAltName = DNS:localhost, IP:127.0.0.1" openssl x509 -in /tmp/test-cert.crt -text -noout #openssl s_client -connect 127.0.0.1:3128 -showcerts diff --git a/tools/server/public_simplechat/main.js b/tools/server/public_simplechat/main.js index b3d6608345..21edb6c336 100644 --- a/tools/server/public_simplechat/main.js +++ b/tools/server/public_simplechat/main.js @@ -2,7 +2,7 @@ // A simple minded GenAi/LLM chat web client implementation. // Handshakes with // * ai server's completions and chat/completions endpoints -// * simpleproxy tool calls provider +// * simplemcp tool calls provider // Helps with basic usage and testing. // by Humans for All diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 4c74221158..2526cfe8fc 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -2,7 +2,7 @@ by Humans for All. -A lightweight simple minded ai chat client with a web front-end that supports multiple chat sessions, vision, reasoning and tool calling. +A lightweight simple minded ai chat client, which runs in a browser environment, with a web front-end that supports multiple chat sessions, vision models, reasoning and tool calling (including bundled tool calls - some browser native and some bundled simplemcp based). ## Quickstart @@ -26,12 +26,13 @@ build/bin/llama-server -m \ If one needs web related access / tool calls dont forget to run ```bash -cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config simpleproxy.json +cd tools/server/public_simplechat/local.tools; python3 ./simplemcp.py --config simplemcp.json ``` - `--debug True` enables debug mode which captures internet handshake data -- port defaults to 3128, can be changed from simpleproxy.json, if needed -- add sec.keyfile and sec.certfile to simpleproxy.json, for https mode +- port defaults to 3128, can be changed from simplemcp.json, if needed +- add sec.keyFile and sec.certFile to simplemcp.json, for https mode; + - also dont forget to change mcpServerUrl to mention https scheme ### Client @@ -41,6 +42,7 @@ cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config 2. Select / Create a chat session - set a suitable system prompt, if needed - modify **settings**, if needed + - modifying mcpServerUrl wont reload supported tool calls list, till next app page refresh - **Restore** loads last autosaved session with same name 3. Enter query/response into user input area at the bottom, press **Enter** @@ -53,7 +55,7 @@ cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config - verify / edit the tool call details before triggering the same - one can even ask ai to rethink on the tool call requested, by sending a appropriate user response instead of a tool call response - - tool call is executed using Browser's web worker or included SimpleProxy.py + - tool call is executed using Browser's web worker or included SimpleMCP.py - tool call response is placed in user input area - the user input area is color coded to distinguish between user and tool responses - verify / edit the tool call response, before submit same back to ai @@ -68,7 +70,7 @@ cd tools/server/public_simplechat/local.tools; python3 ./simpleproxy.py --config ## Overview -A lightweight simple minded ai chat client with a web front-end that supports multiple chat sessions, vision, reasoning and tool calling. +A lightweight simple minded ai chat client, which runs in a browser environment, with a web front-end that supports multiple chat sessions, vision models, reasoning and tool calling (including bundled tool calls - some browser native and some bundled simplemcp based). - Supports multiple independent chat sessions with - One‑shot or Streamed (default) responses @@ -97,16 +99,18 @@ A lightweight simple minded ai chat client with a web front-end that supports mu - except for external_ai, these are run from within a web worker context to isolate main context from them - data_store brings in browser IndexedDB based persistant key/value storage across sessions - - in collaboration with included python based simpleproxy.py, these additional tool calls are supported + - in collaboration with included python based simplemcp.py, these additional tool calls are supported - `search_web_text`, `fetch_url_raw`, `fetch_html_text`, `fetch_pdf_as_text`, `fetch_xml_filtered` - - these built‑in tool calls (via SimpleProxy) help fetch PDFs, HTML, XML or perform web search + - these built‑in tool calls (via SimpleMCP) help fetch PDFs, HTML, XML or perform web search - PDF tool also returns an outline with numbering, if available - result is truncated to `iResultMaxDataLength` (default 128 kB) - helps isolate core of these functionality into a separate vm running locally or otherwise, if needed - - supports whitelisting of `allowed.schemes` and `allowed.domains` through `simpleproxy.json` + - supports whitelisting of `acl.schemes` and `acl.domains` through `simplemcp.json` - supports a bearer token shared between server and client for auth - - needs https support, for better security wrt this flow, avoided now given mostly local use - and need for user to setup corresponding pki key pairs. + - needs https mode to be enabled, for better security wrt this flow + - by default simplemcp.py runs in http mode, + however if sec.keyFile and sec.certFile are specified, the logic switches to https mode + - this handshake is loosely based on MCP standard, doesnt stick to the standard fully - follows a safety first design and lets the user - verify and optionally edit the tool call requests, before executing the same @@ -143,7 +147,7 @@ A lightweight simple minded ai chat client with a web front-end that supports mu - built using plain html + css + javascript and python - no additional dependencies that one needs to worry about and inturn keep track of - except for pypdf, if pdf support needed. automaticaly drops pdf tool call support, if pypdf missing - - fits within ~50KB compressed source or ~284KB in uncompressed source form (both including simpleproxy.py) + - fits within ~50KB compressed source or ~300KB in uncompressed source form (both including simplemcp.py) - easily extend with additional tool calls using either javascript or python, for additional functionality as you see fit @@ -159,7 +163,7 @@ One can modify the session configuration using Settings UI. All the settings and | Group | Purpose | |---------|---------| | `chatProps` | ApiEndpoint, streaming, sliding window, markdown, ... | -| `tools` | `enabled`, `proxyUrl`, `proxyAuthInsecure`, search URL/template & drop rules, max data length, timeouts | +| `tools` | `enabled`, `mcpServerUrl`, `mcpServerAuth`, search URL/template & drop rules, max data length, timeouts | | `apiRequestOptions` | `temperature`, `max_tokens`, `frequency_penalty`, `presence_penalty`, `cache_prompt`, ... | | `headers` | `Content-Type`, `Authorization`, ... | @@ -168,8 +172,8 @@ One can modify the session configuration using Settings UI. All the settings and - **Ai Server** (`baseURL`) - ai server (llama-server) address - default is `http://127.0.0.1:8080` -- **SimpleProxy Server** (`proxyUrl`) - - the simpleproxy.py server address +- **SimpleMCP Server** (`mcpServerUrl`) + - the simplemcp.py server address - default is `http://127.0.0.1:3128` - **Stream** (`stream`) - `true` for live streaming, `false` for oneshot diff --git a/tools/server/public_simplechat/simplechat.js b/tools/server/public_simplechat/simplechat.js index 1c782b60f4..617ec88b0b 100644 --- a/tools/server/public_simplechat/simplechat.js +++ b/tools/server/public_simplechat/simplechat.js @@ -495,7 +495,7 @@ function usage_note(sRecentUserMsgCnt) {
    • if ai assistant requests a tool call, verify same before triggering.
    • submit tool response placed into user query/response text area
    • -
    • for web access inc search/pdf tool calls, run included simpleproxy.py
    • +
    • for web access inc search/pdf tool calls, run included simplemcp.py
  • ContextWindow = [System, ${sRecentUserMsgCnt} User Query/Resp, Cur Query].
    • @@ -2136,26 +2136,6 @@ class MultiChatUI { } -/** - * Few web search engine url template strings. - * The SEARCHWORDS keyword will get replaced by the actual user specified search words at runtime. - */ -const SearchURLS = { - duckduckgo: { - 'template': "https://duckduckgo.com/html/?q=SEARCHWORDS", - 'drop': [ { 'tag': 'div', 'id': "header" } ] - }, - bing: { - 'template': "https://www.bing.com/search?q=SEARCHWORDS", // doesnt seem to like google chrome clients in particular - }, - brave: { - 'template': "https://search.brave.com/search?q=SEARCHWORDS", - }, - google: { - 'template': "https://www.google.com/search?q=SEARCHWORDS", // doesnt seem to like any client in general - }, -} - export class Config { @@ -2163,10 +2143,8 @@ export class Config { this.baseURL = "http://127.0.0.1:8080"; this.tools = { enabled: true, - proxyUrl: "http://127.0.0.1:3128", - proxyAuthInsecure: "NeverSecure", - searchUrl: SearchURLS.duckduckgo.template, - searchDrops: SearchURLS.duckduckgo.drop, + mcpServerUrl: "http://127.0.0.1:3128/mcp", + mcpServerAuth: "NeverSecure", toolNames: /** @type {Array} */([]), /** * Control the length of the tool call result data returned to ai after tool call. diff --git a/tools/server/public_simplechat/toolmcp.mjs b/tools/server/public_simplechat/toolmcp.mjs index 689bab457f..6b8c5ec6b6 100644 --- a/tools/server/public_simplechat/toolmcp.mjs +++ b/tools/server/public_simplechat/toolmcp.mjs @@ -27,7 +27,7 @@ let gMe = /** @type{mChatMagic.Me} */(/** @type {unknown} */(null)); * @param {mChatMagic.SimpleChat} chat */ async function bearer_transform(chat) { - let data = `${new Date().getUTCFullYear()}${chat.cfg.tools.proxyAuthInsecure}` + let data = `${new Date().getUTCFullYear()}${chat.cfg.tools.mcpServerAuth}` const ab = await crypto.subtle.digest('sha-256', new TextEncoder().encode(data)); return Array.from(new Uint8Array(ab)).map(b => b.toString(16).padStart(2, '0')).join(''); } @@ -49,7 +49,7 @@ async function mcpserver_toolcall(chatid, toolcallid, toolname, obj) { return } try { - let newUrl = `${chat.cfg.tools.proxyUrl}/mcp` + let newUrl = `${chat.cfg.tools.mcpServerUrl}` let headers = new Headers(); let btoken = await bearer_transform(chat) headers.append('Authorization', `Bearer ${btoken}`) @@ -111,7 +111,7 @@ async function mcpserver_toolslist(tag, chatId, tcs) { let btoken = await bearer_transform(chat) headers.append('Authorization', `Bearer ${btoken}`) headers.append("Content-Type", "application/json") - let resp = await fetch(`${chat.cfg.tools.proxyUrl}/mcp`, { + let resp = await fetch(`${chat.cfg.tools.mcpServerUrl}`, { method: "POST", headers: headers, body: JSON.stringify(ibody), From 9d895b0ed335492140a8ecc69b2be136376a0faf Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Mon, 8 Dec 2025 16:28:27 +0530 Subject: [PATCH 444/446] SimpleSallap:SimpleMCP:Fix cmdline arg oversight, cleanup space Had forgotten to update docs wrt renamed --op.configFile arg Remove the unneeded space from details.md, which was triggering the editorconfig check at upstream github repo. --- tools/server/public_simplechat/docs/details.md | 4 ++-- tools/server/public_simplechat/local.tools/config.py | 2 +- tools/server/public_simplechat/readme.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/server/public_simplechat/docs/details.md b/tools/server/public_simplechat/docs/details.md index 6ce83fce03..f63c6d53ba 100644 --- a/tools/server/public_simplechat/docs/details.md +++ b/tools/server/public_simplechat/docs/details.md @@ -95,7 +95,7 @@ remember to local.tools/simplemcp.py helper along with its config file, before using/loading this client ui through a browser - * cd tools/server/public_simplechat/local.tools; python3 ./simplemcp.py --config simplemcp.json + * cd tools/server/public_simplechat/local.tools; python3 ./simplemcp.py --op.configFile simplemcp.json * remember that this is a relatively minimal dumb mcp(ish) server logic with few builtin tool calls related to fetching raw html or stripped plain text equivalent or pdf text content. @@ -311,7 +311,7 @@ It is attached to the document object. Some of these can also be updated using t * setting this value to 0 (default), disables auto logic, so that end user can review the tool calls requested by ai and if needed even modify them, before triggering/executing them as well as review and modify results generated by the tool call, before submitting them back to the ai. * this is specified in seconds, so that users by default will normally not overload any website through the bundled mcp server. - + 1. the builtin tools' meta data is sent to the ai model in the requests sent to it. 2. inturn if the ai model requests a tool call to be made, the same will be done and the response sent back to the ai model, under user control, by default. diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index 448ec8bed5..370ebb9f52 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -152,7 +152,7 @@ class Config(DictyDataclassMixin): Helper to process command line arguments. Flow setup below such that - * location of --config in commandline will decide whether command line or config file will get + * location of --op.configFile in commandline will decide whether command line or config file will get priority wrt setting program parameters. * str type values in cmdline are picked up directly, without running them through ast.literal_eval, bcas otherwise one will have to ensure throught the cmdline arg mechanism that string quote is diff --git a/tools/server/public_simplechat/readme.md b/tools/server/public_simplechat/readme.md index 2526cfe8fc..a3987d698a 100644 --- a/tools/server/public_simplechat/readme.md +++ b/tools/server/public_simplechat/readme.md @@ -26,7 +26,7 @@ build/bin/llama-server -m \ If one needs web related access / tool calls dont forget to run ```bash -cd tools/server/public_simplechat/local.tools; python3 ./simplemcp.py --config simplemcp.json +cd tools/server/public_simplechat/local.tools; python3 ./simplemcp.py --op.configFile simplemcp.json ``` - `--debug True` enables debug mode which captures internet handshake data From e04afaa6ffe1324d23638574303690affdbdfd30 Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 9 Dec 2025 21:01:25 +0530 Subject: [PATCH 445/446] SimpleSallap:SimpleMCP: Require auth line only for https Also send 401 error response when appropriate --- tools/server/public_simplechat/local.tools/config.py | 6 +++++- tools/server/public_simplechat/local.tools/simplemcp.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index 370ebb9f52..ae0f124afd 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -43,7 +43,11 @@ class Sec(DictyDataclassMixin): certFile: str = "" keyFile: str = "" bearerAuth: str = "" - bAuthAlways: bool = True + bAuthAlways: bool = False + """ + if true, expects authorization line irrespective of http / https + if false, authorization line needed only for https + """ @dataclass diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 1df6dab828..2e34f941b2 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -80,14 +80,14 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): bearer_transform() authline = self.headers['Authorization'] if authline == None: - return mTC.TCOutResponse(False, 400, "WARN:No auth line") + return mTC.TCOutResponse(False, 401, "WARN:No auth line") authlineA = authline.strip().split(' ') if len(authlineA) != 2: return mTC.TCOutResponse(False, 400, "WARN:Invalid auth line") if authlineA[0] != 'Bearer': return mTC.TCOutResponse(False, 400, "WARN:Invalid auth type") if authlineA[1] != gMe.op.bearerTransformed: - return mTC.TCOutResponse(False, 400, "WARN:Invalid auth") + return mTC.TCOutResponse(False, 401, "WARN:Invalid auth") return mTC.TCOutResponse(True, 200, "Auth Ok") def send_mcp(self, statusCode: int, statusMessage: str, body: Any): @@ -130,6 +130,7 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): def mcp_run(self, body: bytes): oRPC = json.loads(body) + print(f"DBUG:PH:MCP:Method:{oRPC['method']}") if oRPC["method"] == "tools/call": self.mcp_toolscall(oRPC) elif oRPC["method"] == "tools/list": From e67734fa229b72ff17701b7b9e5576fb4802e4ae Mon Sep 17 00:00:00 2001 From: hanishkvc Date: Tue, 9 Dec 2025 21:25:53 +0530 Subject: [PATCH 446/446] SimpleSallap:SimpleMCP:Try identify https in http mode early Else python default http code will try interpret it has a malformed http request line and give failure lines like * bad version number * bad http/0.9 request etal along with a long bytes string following it --- tools/server/public_simplechat/local.tools/config.py | 4 ++++ tools/server/public_simplechat/local.tools/simplemcp.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/tools/server/public_simplechat/local.tools/config.py b/tools/server/public_simplechat/local.tools/config.py index ae0f124afd..2a6c16b109 100644 --- a/tools/server/public_simplechat/local.tools/config.py +++ b/tools/server/public_simplechat/local.tools/config.py @@ -76,6 +76,10 @@ class Network(DictyDataclassMixin): class Op(DictyDataclassMixin): """ Used to store runtime operation related config entries and states + + Attributes: + sslContext: stores ssl context to use, + indirectly indicate if using https mode or not """ configFile: str = "/dev/null" debug: bool = False diff --git a/tools/server/public_simplechat/local.tools/simplemcp.py b/tools/server/public_simplechat/local.tools/simplemcp.py index 2e34f941b2..c916a1d621 100644 --- a/tools/server/public_simplechat/local.tools/simplemcp.py +++ b/tools/server/public_simplechat/local.tools/simplemcp.py @@ -18,6 +18,8 @@ import time import ssl import traceback import json +import select +import socket from typing import Any from dataclasses import asdict import tcpdf as mTCPdf @@ -197,6 +199,13 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler): if (gMe.op.sslContext): self.request = gMe.op.sslContext.wrap_socket(self.request, server_side=True) self.setup() + else: + conn: socket.socket = self.connection + readReady, _, _ = select.select([conn], [], [], 1.0) + if readReady: + peek = conn.recv(3, socket.MSG_PEEK) + if peek.startswith(b'\x16\x03'): + raise ConnectionError("Https in http mode???") except: print(f"ERRR:ProxyHandler:SSLHS:{traceback.format_exception_only(sys.exception())}") return