You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/09/24 14:03:39 UTC

(skywalking-ai-sessionizer) branch horizon-langchain-prompts created (now 3965c6f)

This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a change to branch horizon-langchain-prompts
in repository https://gitbox.apache.org/repos/asf/skywalking-ai-sessionizer.git


      at 3965c6f  Show what a LangChain model call was sent, in LangChain's words

This branch includes the following new commits:

     new 3965c6f  Show what a LangChain model call was sent, in LangChain's words

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(skywalking-ai-sessionizer) 01/01: Show what a LangChain model call was sent, in LangChain's words

Posted by wu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch horizon-langchain-prompts
in repository https://gitbox.apache.org/repos/asf/skywalking-ai-sessionizer.git

commit 3965c6f015851f2a194aaac2b26e675914c81195
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Thu Sep 24 22:03:30 2026 +0800

    Show what a LangChain model call was sent, in LangChain's words
    
    The page's Prompt tab read a LangChain request as Claude Code's shape and
    drew it as one empty message. Horizon's renderer now reads LangChain's
    serialized messages: each by its own type, system, human, ai or tool, with
    the tool calls an ai message made and each tool result under the call it
    answers, and a response with its answer, model and token counts.
    
    The embedded renderer moves to Horizon 297f020, which carries that fix
    (apache/skywalking-horizon-ui#172). The theme it brings also draws a light
    theme's scrollbars and other native controls light, and lets the top bar's
    actions give way on a narrow window.
---
 docs/en/changes/changes.md                         |   7 ++
 internal/view/conversation-view/HORIZON_COMMIT     |   2 +-
 .../view/conversation-view/conversation-view.js    | 104 ++++++++++++++++++++-
 .../conversation-view/host-shell/horizon-theme.css |  16 +++-
 4 files changed, 124 insertions(+), 5 deletions(-)

diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 3265eaf..a536f6c 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -15,6 +15,13 @@
 - A link to a plain model call inside a tool landed with its `auxiliary` flag twice. A request is
   placed once to check it can land and again to land it, and the second placement built on the
   first. Placement now leaves the request as it arrived.
+- The page shows what a LangChain model call was sent. The Prompt tab read a LangChain request as
+  Claude Code's shape and drew it as one empty message. It now shows each message in LangChain's
+  own words, `system`, `human`, `ai` and `tool`, with the tool calls an `ai` message made and each
+  tool result under the call it answers. A response shows the answer, the model and the token
+  counts. What it added stays unavailable for a LangChain call, because no request names the one
+  before it. The renderer is Horizon's, now pinned at `297f020`, which also draws a light theme's
+  scrollbars and other native controls light.
 
 ## Development
 
diff --git a/internal/view/conversation-view/HORIZON_COMMIT b/internal/view/conversation-view/HORIZON_COMMIT
index 59fd670..a4e85ad 100644
--- a/internal/view/conversation-view/HORIZON_COMMIT
+++ b/internal/view/conversation-view/HORIZON_COMMIT
@@ -1,3 +1,3 @@
-commit 53c1f63b635bc8efbaec0e4bc28ec9539e526328
+commit 297f0205e502a3bc795d236356a31364dec65b5c
 package @skywalking-horizon-ui/conversation-view 1.1.0-dev
 source https://github.com/apache/skywalking-horizon-ui.git
diff --git a/internal/view/conversation-view/conversation-view.js b/internal/view/conversation-view/conversation-view.js
index c91d01a..29f81a9 100644
--- a/internal/view/conversation-view/conversation-view.js
+++ b/internal/view/conversation-view/conversation-view.js
@@ -1745,10 +1745,24 @@ function readBody(bytes, role) {
   }
   if (!value || typeof value !== "object" || Array.isArray(value)) return { kind: "other", raw: value };
   const body = value;
-  if (role === "request" && Array.isArray(body["messages"])) return readRequest(body);
-  if (role === "response" && Array.isArray(body["content"])) return readResponse(body);
+  if (role === "request") {
+    if (isObjectList(body["messages"])) return readRequest(body);
+    const prompt = langChainPrompt(body["messages"]);
+    if (prompt) return readLangChainRequest(body, prompt);
+  }
+  if (role === "response") {
+    if (Array.isArray(body["content"])) return readResponse(body);
+    const generation = langChainGeneration(body["generations"]);
+    if (generation) return readLangChainResponse(body, generation);
+  }
   return { kind: "other", raw: value };
 }
+function isObjectList(value) {
+  return Array.isArray(value) && value.every(isObject);
+}
+function isObject(value) {
+  return !!value && typeof value === "object" && !Array.isArray(value);
+}
 function readRequest(body) {
   const rest = /* @__PURE__ */ Object.create(null);
   for (const [k, v] of Object.entries(body)) {
@@ -1830,6 +1844,90 @@ function readBlock(value) {
   }
   return { kind, json: value };
 }
+function langChainPrompt(messages) {
+  if (!Array.isArray(messages) || messages.length !== 1) return null;
+  const prompt = messages[0];
+  return isObjectList(prompt) ? prompt : null;
+}
+function langChainGeneration(generations) {
+  if (!Array.isArray(generations) || generations.length !== 1) return null;
+  const prompt = generations[0];
+  if (!Array.isArray(prompt) || prompt.length !== 1) return null;
+  const only = prompt[0];
+  return isObject(only) ? only : null;
+}
+function readLangChainRequest(body, prompt) {
+  const rest = /* @__PURE__ */ Object.create(null);
+  for (const [k, v] of Object.entries(body)) if (k !== "messages") rest[k] = v;
+  return { kind: "request", system: [], tools: [], messages: prompt.map(readLangChainMessage), rest, raw: body };
+}
+function readLangChainResponse(body, generation) {
+  const message2 = generation["message"];
+  const fields = isObject(message2) ? langChainFields(message2) : null;
+  const rest = /* @__PURE__ */ Object.create(null);
+  for (const [k, v] of Object.entries(fields ?? {})) {
+    if (!["content", "tool_calls", "invalid_tool_calls", "usage_metadata", "id", "type"].includes(k)) rest[k] = v;
+  }
+  if (generation["generation_info"] != null) rest["generation_info"] = generation["generation_info"];
+  if (body["llm_output"] != null) rest["llm_output"] = body["llm_output"];
+  const metadata = fields == null ? void 0 : fields["response_metadata"];
+  const output = body["llm_output"];
+  const model = (isObject(metadata) ? metadata["model_name"] : void 0) ?? (isObject(output) ? output["model_name"] : void 0);
+  const usage = fields == null ? void 0 : fields["usage_metadata"];
+  const text = generation["text"];
+  return {
+    kind: "response",
+    model: typeof model === "string" ? model : void 0,
+    id: typeof (fields == null ? void 0 : fields["id"]) === "string" ? fields["id"] : void 0,
+    blocks: fields ? langChainBlocks(fields, message2) : typeof text === "string" ? [{ kind: "text", text }] : [],
+    usage: isObject(usage) ? usage : void 0,
+    rest,
+    raw: body
+  };
+}
+function readLangChainMessage(value) {
+  const fields = langChainFields(value);
+  const type = typeof fields["type"] === "string" ? fields["type"] : "";
+  const role = typeof fields["role"] === "string" ? fields["role"] : "";
+  return { role: type && type !== "chat" ? type : role, blocks: langChainBlocks(fields, value), raw: value };
+}
+function langChainContentBlock(value) {
+  return isObject(value) && value["type"] === "tool_call" ? langChainToolCall(value) : readBlock(value);
+}
+function langChainToolCall(c) {
+  const id = typeof c["id"] === "string" ? c["id"] : void 0;
+  return { kind: "tool_use", name: typeof c["name"] === "string" ? c["name"] : "", ...id ? { id } : {}, json: c["args"] };
+}
+function langChainFields(m) {
+  const kwargs = m["kwargs"];
+  return m["lc"] === 1 && m["type"] === "constructor" && isObject(kwargs) ? kwargs : m;
+}
+function langChainBlocks(fields, raw) {
+  const content = fields["content"];
+  if (fields["type"] === "tool" || fields["role"] === "tool") {
+    const about = {
+      ...typeof fields["tool_call_id"] === "string" ? { id: fields["tool_call_id"] } : {},
+      ...fields["status"] === "error" ? { failed: true } : {}
+    };
+    return [typeof content === "string" ? { kind: "tool_result", text: content, json: raw, ...about } : { kind: "tool_result", json: content ?? raw, ...about }];
+  }
+  const blocks = typeof content === "string" ? content ? [{ kind: "text", text: content, reminder: content.includes(REMINDER) }] : [] : Array.isArray(content) ? content.map(langChainContentBlock) : [];
+  const at = /* @__PURE__ */ new Map();
+  blocks.forEach((b, i) => {
+    if (b.kind === "tool_use" && b.id) at.set(b.id, i);
+  });
+  for (const call of Array.isArray(fields["tool_calls"]) ? fields["tool_calls"] : []) {
+    const use = langChainToolCall(isObject(call) ? call : {});
+    const i = use.id ? at.get(use.id) : void 0;
+    if (i === void 0) blocks.push(use);
+    else blocks[i] = { ...use, name: use.name || (blocks[i].name ?? "") };
+  }
+  for (const call of Array.isArray(fields["invalid_tool_calls"]) ? fields["invalid_tool_calls"] : []) {
+    blocks.push({ kind: "invalid_tool_call", json: call });
+  }
+  if (blocks.length) return blocks;
+  return typeof content === "string" ? [{ kind: "text", text: "" }] : [{ kind: "unknown", json: raw }];
+}
 function deltaOf(previous, current) {
   let shared = 0;
   let rewritten = false;
@@ -2308,7 +2406,7 @@ function drawRequest(ctx, e, read2, sides, store) {
     read2.messages.map((m, i) => message(ctx, m, i + 1, read2.messages.length, `${e.id}|req|msg|${i}`)).join(""),
     true
   );
-  const settings2 = section(ctx, e, "settings", s.promptSettings, Object.keys(read2.rest).join(", "), json(ctx, read2.rest, `${e.id}|req|set`));
+  const settings2 = Object.keys(read2.rest).length ? section(ctx, e, "settings", s.promptSettings, Object.keys(read2.rest).join(", "), json(ctx, read2.rest, `${e.id}|req|set`)) : "";
   const whole = section(ctx, e, "request-raw", s.promptWholeBody, "", json(ctx, read2.raw, `${e.id}|req|raw`));
   return `${modes}${system}${tools}${messages}${settings2}${whole}`;
 }
diff --git a/internal/view/conversation-view/host-shell/horizon-theme.css b/internal/view/conversation-view/host-shell/horizon-theme.css
index 1dfb921..868400f 100644
--- a/internal/view/conversation-view/host-shell/horizon-theme.css
+++ b/internal/view/conversation-view/host-shell/horizon-theme.css
@@ -298,7 +298,10 @@
   background: var(--sw-bg-3); border-radius: 3px;
   padding: 1px 5px; border: 1px solid var(--sw-line-2);
 }
-.sw-top-actions { margin-left: auto; display: flex; align-items: center; gap: 6px; }
+/* `min-width: 0` so the row can give way on a narrow window: without it the
+   widest chip's text sets a floor the topbar cannot go below, and the whole
+   bar overflows the viewport instead of the label ellipsizing. */
+.sw-top-actions { margin-left: auto; display: flex; align-items: center; gap: 6px; min-width: 0; }
 
 /* main */
 .sw-main {
@@ -462,6 +465,17 @@ textarea:disabled {
  * theme store sets `<html data-theme>` and CSS does the rest.
  */
 
+/* Native form controls — date inputs and their calendar popups, spinners,
+ * scrollbars, the default focus ring — read `color-scheme`, not our tokens.
+ * `tokens.css` states `dark` on `:root` for the dark palette it defines, and
+ * nothing turned it back: on a LIGHT theme the browser went on drawing the
+ * calendar icon white on a white field, and opened a dark popup over a light
+ * page. Keyed on the appearance rather than on one theme id, so a light theme
+ * added later is covered by having declared what it is. */
+[data-appearance="light"] {
+  color-scheme: light;
+}
+
 /* ── horizon (default — declared explicitly for parity with the others) ── */
 [data-theme="horizon"] {
   --sw-bg-0: #0a0d12;