@hef2024/llmasaservice-ui 0.24.2 → 0.24.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import materialLight from "react-syntax-highlighter/dist/esm/styles/prism/materi
4
4
  import ChatPanel from "./ChatPanel";
5
5
  import { LLMAsAServiceCustomer } from "llmasaservice-client";
6
6
  import PrismStyle from "react-syntax-highlighter";
7
+ import { MCPAuthHeaderResolver } from "./mcpAuth";
7
8
 
8
9
  export interface AgentPanelProps {
9
10
  //project_id: string;
@@ -61,6 +62,7 @@ export interface AgentPanelProps {
61
62
  createConversationOnFirstChat?: boolean;
62
63
  customerEmailCaptureMode?: "HIDE" | "OPTIONAL" | "REQUIRED";
63
64
  customerEmailCapturePlaceholder?: string;
65
+ resolveMcpAuthHeaders?: MCPAuthHeaderResolver;
64
66
  }
65
67
  interface ExtraProps extends React.HTMLAttributes<HTMLElement> {
66
68
  inline?: boolean;
@@ -107,6 +109,7 @@ const AgentPanel: React.FC<AgentPanelProps & ExtraProps> = ({
107
109
  //ragRankLimit = 5,
108
110
  initialHistory = {},
109
111
  hideRagContextInPrompt = true,
112
+ resolveMcpAuthHeaders,
110
113
  }) => {
111
114
  const searchParams = new URLSearchParams(location.search);
112
115
  const customer_id = searchParams.get("customer_id") || null;
@@ -314,6 +317,7 @@ const AgentPanel: React.FC<AgentPanelProps & ExtraProps> = ({
314
317
  "Please enter your email..."
315
318
  }
316
319
  mcpServers={mcpData}
320
+ resolveMcpAuthHeaders={resolveMcpAuthHeaders}
317
321
  />
318
322
  )}
319
323
  </>
package/src/ChatPanel.tsx CHANGED
@@ -19,6 +19,12 @@ import materialLight from "react-syntax-highlighter/dist/esm/styles/prism/materi
19
19
  import EmailModal from "./EmailModal";
20
20
  import ToolInfoModal from "./ToolInfoModal";
21
21
  import { ThinkingBlock as ThinkingBlockComponent } from './components/ui';
22
+ import {
23
+ MCPAuthHeaderResolver,
24
+ MCPAuthPhase,
25
+ normalizeMcpHeaders,
26
+ } from "./mcpAuth";
27
+ import { parseToolArguments } from "./toolArgsParser";
22
28
 
23
29
  export interface ChatPanelProps {
24
30
  project_id: string;
@@ -80,6 +86,7 @@ export interface ChatPanelProps {
80
86
  customerEmailCapturePlaceholder?: string;
81
87
  mcpServers?: [];
82
88
  progressiveActions?: boolean;
89
+ resolveMcpAuthHeaders?: MCPAuthHeaderResolver;
83
90
  }
84
91
 
85
92
  interface HistoryEntry {
@@ -138,6 +145,7 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
138
145
  customerEmailCapturePlaceholder = "Please enter your email...",
139
146
  mcpServers,
140
147
  progressiveActions = true,
148
+ resolveMcpAuthHeaders,
141
149
  }) => {
142
150
  const isEmailAddress = (email: string): boolean => {
143
151
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -624,6 +632,59 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
624
632
  const [toolsLoading, setToolsLoading] = useState(false);
625
633
  const [toolsFetchError, setToolsFetchError] = useState(false);
626
634
 
635
+ const buildMcpRequestHeaders = useCallback(
636
+ async ({
637
+ phase,
638
+ mcpServer,
639
+ toolName,
640
+ toolArgs,
641
+ }: {
642
+ phase: MCPAuthPhase;
643
+ mcpServer: Record<string, unknown>;
644
+ toolName?: string;
645
+ toolArgs?: unknown;
646
+ }): Promise<Record<string, string>> => {
647
+ const merged: Record<string, string> = {};
648
+
649
+ const baseAccessToken =
650
+ typeof mcpServer.accessToken === "string"
651
+ ? mcpServer.accessToken.trim()
652
+ : "";
653
+ if (baseAccessToken) {
654
+ merged["x-mcp-access-token"] = baseAccessToken;
655
+ }
656
+ if (project_id) {
657
+ merged["x-project-id"] = project_id;
658
+ }
659
+
660
+ if (!resolveMcpAuthHeaders) return merged;
661
+
662
+ try {
663
+ const resolved = await resolveMcpAuthHeaders({
664
+ phase,
665
+ mcpServer,
666
+ projectId: project_id,
667
+ customer: currentCustomer,
668
+ toolName,
669
+ toolArgs,
670
+ });
671
+ return {
672
+ ...merged,
673
+ ...normalizeMcpHeaders(
674
+ resolved as Record<string, unknown> | null | undefined
675
+ ),
676
+ };
677
+ } catch (error) {
678
+ console.error(
679
+ `Failed to resolve MCP auth headers for ${phase} request:`,
680
+ error
681
+ );
682
+ return merged;
683
+ }
684
+ },
685
+ [project_id, currentCustomer, resolveMcpAuthHeaders]
686
+ );
687
+
627
688
  // mcp servers are passed in in the mcpServers prop. Fetch tools for each one.
628
689
  useEffect(() => {
629
690
  //console.log("MCP servers", mcpServers);
@@ -647,7 +708,13 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
647
708
  )}`;
648
709
 
649
710
  try {
650
- const response = await fetch(urlToFetch);
711
+ const requestHeaders = await buildMcpRequestHeaders({
712
+ phase: "list",
713
+ mcpServer: m,
714
+ });
715
+ const response = await fetch(urlToFetch, {
716
+ headers: requestHeaders,
717
+ });
651
718
  if (!response.ok) {
652
719
  console.error(
653
720
  `Error fetching tools from ${m.url}: ${response.status} ${response.statusText}`
@@ -664,7 +731,7 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
664
731
  ...tool,
665
732
  url: m.url,
666
733
  accessToken: m.accessToken || "",
667
- headers: {},
734
+ headers: requestHeaders,
668
735
  }));
669
736
  } else {
670
737
  return [];
@@ -699,7 +766,7 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
699
766
  };
700
767
 
701
768
  fetchAndSetTools();
702
- }, [mcpServers, publicAPIUrl]);
769
+ }, [mcpServers, publicAPIUrl, buildMcpRequestHeaders]);
703
770
 
704
771
  const llmResult = useLLM({
705
772
  project_id: project_id,
@@ -1203,7 +1270,7 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
1203
1270
 
1204
1271
  const anthropic_toolAction = {
1205
1272
  pattern:
1206
- '\\{"type":"tool_use","id":"([^"]+)","name":"([^"]+)","input":(\\{[\\s\\S]+?\\}),"service":"([^"]+)"\\}',
1273
+ '\\{"type":"tool_use","id":"([^"]+)","name":"([^"]+)","input":(\\{[\\s\\S]*?\\}),"service":"([^"]+)"\\}',
1207
1274
  type: "markdown",
1208
1275
  markdown: "<br />*Tool use requested: $2*",
1209
1276
  actionType: "tool",
@@ -1211,7 +1278,7 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
1211
1278
 
1212
1279
  const openAI_toolAction = {
1213
1280
  pattern:
1214
- '\\{"id":"([^"]+)","type":"function","function":\\{"name":"([^"]+)","arguments":"((?:\\\\.|[^"\\\\])*)"\\},"service":"([^"]+)"\\}',
1281
+ '\\{"id":"([^"]+)","type":"function","function":\\{"name":"([^"]+)","arguments":"([\\s\\S]*?)"\\},"service":"([^"]+)"\\}',
1215
1282
  type: "markdown",
1216
1283
  markdown: "<br />*Tool use requested: $2*",
1217
1284
  actionType: "tool",
@@ -1220,7 +1287,7 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
1220
1287
  // google doesn't return an id, so we just grab functioCall
1221
1288
  const google_toolAction = {
1222
1289
  pattern:
1223
- '^\\{\\s*"(functionCall)"\\s*:\\s*\\{\\s*"name"\\s*:\\s*"([^"]+)"\\s*,\\s*"args"\\s*:\\s*(\\{[\\s\\S]+?\\})\\s*\\}(?:\\s*,\\s*"thoughtSignature"\\s*:\\s*"[^"]*")?\\s*,\\s*"service"\\s*:\\s*"([^"]+)"\\s*\\}$',
1290
+ '^\\{\\s*"(functionCall)"\\s*:\\s*\\{\\s*"name"\\s*:\\s*"([^"]+)"\\s*,\\s*"args"\\s*:\\s*(\\{[\\s\\S]*?\\})\\s*\\}(?:\\s*,\\s*"thoughtSignature"\\s*:\\s*"[^"]*")?\\s*,\\s*"service"\\s*:\\s*"([^"]+)"\\s*\\}$',
1224
1291
  type: "markdown",
1225
1292
  markdown: "<br />*Tool use requested: $2*",
1226
1293
  actionType: "tool",
@@ -1400,7 +1467,7 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
1400
1467
  setPendingToolRequests([]);
1401
1468
  try {
1402
1469
  // Start with base messages including the user's original question
1403
- const newMessages = [
1470
+ const newMessages: any[] = [
1404
1471
  {
1405
1472
  role: "user",
1406
1473
  content: [
@@ -1412,72 +1479,108 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
1412
1479
  },
1413
1480
  ];
1414
1481
 
1415
- // Add a single assistant message with ALL tool calls
1416
- const toolCallsMessage = {
1417
- role: "assistant",
1418
- content: [],
1419
- tool_calls: [],
1420
- };
1482
+ const parsedToolCalls = await Promise.all(
1483
+ toolsToProcess.map(async (req, index) => {
1484
+ if (!req) return null;
1421
1485
 
1422
- // Parse all tool calls first
1423
- const toolCallsPromises = toolsToProcess.map(async (req) => {
1424
- if (!req) return null;
1486
+ let parsedToolCall: any = null;
1487
+ try {
1488
+ parsedToolCall = JSON.parse(req.match);
1489
+ } catch (error) {
1490
+ console.error("Failed to parse tool call:", error);
1491
+ }
1492
+
1493
+ const toolName =
1494
+ req.groups[1] ||
1495
+ req.toolName ||
1496
+ (typeof parsedToolCall?.name === "string" ? parsedToolCall.name : "") ||
1497
+ (typeof parsedToolCall?.function?.name === "string"
1498
+ ? parsedToolCall.function.name
1499
+ : "");
1500
+ if (!toolName) return null;
1501
+
1502
+ const rawCallId =
1503
+ req.groups[0] ||
1504
+ parsedToolCall?.id ||
1505
+ parsedToolCall?.tool_call_id ||
1506
+ `${toolName}-${index + 1}`;
1507
+ const callId =
1508
+ typeof rawCallId === "string" &&
1509
+ rawCallId.trim().length > 0 &&
1510
+ rawCallId !== "functionCall"
1511
+ ? rawCallId
1512
+ : `${toolName}-${index + 1}`;
1513
+
1514
+ let args: Record<string, unknown> = {};
1515
+ const rawArgs =
1516
+ req.groups[2] ??
1517
+ parsedToolCall?.input ??
1518
+ parsedToolCall?.args ??
1519
+ parsedToolCall?.function?.arguments ??
1520
+ "{}";
1521
+
1522
+ const parsedArgs = parseToolArguments(rawArgs);
1523
+ if (!parsedArgs) {
1524
+ console.error("Failed to parse tool arguments", {
1525
+ toolName,
1526
+ callId,
1527
+ rawArgsPreview:
1528
+ typeof rawArgs === "string"
1529
+ ? rawArgs.slice(0, 500)
1530
+ : JSON.stringify(rawArgs).slice(0, 500),
1531
+ });
1532
+ return null;
1533
+ }
1534
+ args = parsedArgs;
1535
+
1536
+ const serviceTag =
1537
+ (typeof req.groups[3] === "string" && req.groups[3]) ||
1538
+ (typeof parsedToolCall?.service === "string" && parsedToolCall.service) ||
1539
+ "";
1425
1540
 
1426
- try {
1427
1541
  return {
1428
1542
  req,
1429
- parsedToolCall: JSON.parse(req.match),
1543
+ toolName,
1544
+ callId,
1545
+ args,
1546
+ serviceTag,
1430
1547
  };
1431
- } catch (e) {
1432
- console.error("Failed to parse tool call:", e);
1433
- return null;
1434
- }
1435
- });
1436
-
1437
- // Wait for all tool calls to be parsed
1438
- const parsedToolCalls = await Promise.all(toolCallsPromises);
1439
-
1440
- // Add all tool calls to the assistant message
1441
- parsedToolCalls.forEach((item) => {
1442
- if (item && item.parsedToolCall) {
1443
- (toolCallsMessage.tool_calls as any[]).push(item.parsedToolCall);
1444
- }
1445
- });
1446
-
1447
- // Add the assistant message with all tool calls
1448
- newMessages.push(toolCallsMessage);
1449
-
1450
- const finalToolCalls = toolCallsMessage.tool_calls;
1548
+ })
1549
+ );
1451
1550
 
1452
- const toolResponsePromises = parsedToolCalls.map(async (item) => {
1453
- if (!item || !item.req) return null;
1551
+ const toolCallBatch = parsedToolCalls.filter(Boolean) as Array<{
1552
+ req: any;
1553
+ toolName: string;
1554
+ callId: string;
1555
+ args: Record<string, unknown>;
1556
+ serviceTag: string;
1557
+ }>;
1454
1558
 
1455
- const req = item.req;
1456
- //console.log(`Processing tool ${req.toolName}`);
1559
+ const finalToolCalls = toolCallBatch.map((toolCall) => ({
1560
+ id: toolCall.callId,
1561
+ type: "tool_use",
1562
+ name: toolCall.toolName,
1563
+ input: toolCall.args,
1564
+ service: toolCall.serviceTag,
1565
+ }));
1457
1566
 
1458
- const mcpTool = toolList.find((tool) => tool.name === req.toolName);
1567
+ const toolResponsePromises = toolCallBatch.map(async (toolCall) => {
1568
+ const mcpTool = toolList.find((tool) => tool.name === toolCall.toolName);
1459
1569
 
1460
1570
  if (!mcpTool) {
1461
- console.error(`Tool ${req.toolName} not found in tool list`);
1462
- return null;
1571
+ console.error(`Tool ${toolCall.toolName} not found in tool list`);
1572
+ return {
1573
+ tool_call_id: toolCall.callId,
1574
+ tool_name: toolCall.toolName,
1575
+ result: `Tool ${toolCall.toolName} not found in current tool list.`,
1576
+ isError: true,
1577
+ };
1463
1578
  }
1464
1579
 
1465
1580
  try {
1466
- let args;
1467
- try {
1468
- args = JSON.parse(req.groups[2]);
1469
- } catch (e) {
1470
- try {
1471
- args = JSON.parse(req.groups[2].replace(/\\"/g, '"'));
1472
- } catch (err) {
1473
- console.error("Failed to parse tool arguments:", err);
1474
- return null;
1475
- }
1476
- }
1477
-
1478
1581
  const body = {
1479
- tool: req.groups[1],
1480
- args: args,
1582
+ tool: toolCall.toolName,
1583
+ args: toolCall.args,
1481
1584
  };
1482
1585
 
1483
1586
  const result = await fetch(
@@ -1486,11 +1589,12 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
1486
1589
  method: "POST",
1487
1590
  headers: {
1488
1591
  "Content-Type": "application/json",
1489
- "x-mcp-access-token":
1490
- mcpTool.accessToken && mcpTool.accessToken !== ""
1491
- ? mcpTool.accessToken
1492
- : "",
1493
- "x-project-id": project_id,
1592
+ ...(await buildMcpRequestHeaders({
1593
+ phase: "call",
1594
+ mcpServer: mcpTool as Record<string, unknown>,
1595
+ toolName: toolCall.toolName,
1596
+ toolArgs: toolCall.args,
1597
+ })),
1494
1598
  },
1495
1599
  body: JSON.stringify(body),
1496
1600
  }
@@ -1498,11 +1602,18 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
1498
1602
 
1499
1603
  if (!result.ok) {
1500
1604
  console.error(
1501
- `Error calling tool ${req.toolName}: ${result.status} ${result.statusText}`
1605
+ `Error calling tool ${toolCall.toolName}: ${result.status} ${result.statusText}`
1502
1606
  );
1503
1607
  const errorBody = await result.text();
1504
1608
  console.error(`Error body: ${errorBody}`);
1505
- return null;
1609
+ return {
1610
+ tool_call_id: toolCall.callId,
1611
+ tool_name: toolCall.toolName,
1612
+ result: `HTTP ${result.status} ${result.statusText}: ${
1613
+ errorBody || "Tool call failed"
1614
+ }`,
1615
+ isError: true,
1616
+ };
1506
1617
  }
1507
1618
 
1508
1619
  let resultData;
@@ -1510,45 +1621,40 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
1510
1621
  resultData = await result.json();
1511
1622
  } catch (jsonError) {
1512
1623
  console.error(
1513
- `Error parsing JSON response for tool ${req.toolName}:`,
1624
+ `Error parsing JSON response for tool ${toolCall.toolName}:`,
1514
1625
  jsonError
1515
1626
  );
1516
- // Attempt to read as text for debugging if JSON fails
1517
- try {
1518
- const textBody = await result.text(); // Note: This consumes the body if json() failed early
1519
- console.error("Response body (text):", textBody);
1520
- } catch (textError) {
1521
- console.error(
1522
- "Failed to read response body as text either:",
1523
- textError
1524
- );
1525
- }
1526
- return null; // Exit if JSON parsing failed
1527
- }
1528
-
1529
- if (
1530
- resultData &&
1531
- resultData.content &&
1532
- resultData.content.length > 0
1533
- ) {
1534
- const textResult = resultData.content[0]?.text;
1535
1627
  return {
1536
- role: "tool",
1537
- content: [
1538
- {
1539
- type: "text",
1540
- text: textResult,
1541
- },
1542
- ],
1543
- tool_call_id: req.groups[0],
1628
+ tool_call_id: toolCall.callId,
1629
+ tool_name: toolCall.toolName,
1630
+ result: "Tool returned a non-JSON response.",
1631
+ isError: true,
1544
1632
  };
1545
- } else {
1546
- console.error(`No content returned from tool ${req.toolName}`);
1547
- return null;
1548
1633
  }
1634
+
1635
+ const textResult =
1636
+ resultData?.content?.[0]?.text ??
1637
+ (resultData?.result
1638
+ ? JSON.stringify(resultData.result)
1639
+ : JSON.stringify(resultData));
1640
+
1641
+ return {
1642
+ tool_call_id: toolCall.callId,
1643
+ tool_name: toolCall.toolName,
1644
+ result: textResult || "",
1645
+ isError: resultData?.isError === true,
1646
+ };
1549
1647
  } catch (error) {
1550
- console.error(`Error processing tool ${req.toolName}:`, error);
1551
- return null;
1648
+ console.error(`Error processing tool ${toolCall.toolName}:`, error);
1649
+ return {
1650
+ tool_call_id: toolCall.callId,
1651
+ tool_name: toolCall.toolName,
1652
+ result:
1653
+ error instanceof Error
1654
+ ? error.message
1655
+ : `Unhandled error calling ${toolCall.toolName}`,
1656
+ isError: true,
1657
+ };
1552
1658
  }
1553
1659
  });
1554
1660
 
@@ -1577,11 +1683,55 @@ const ChatPanel: React.FC<ChatPanelProps & ExtraProps> = ({
1577
1683
  });
1578
1684
  }
1579
1685
 
1580
- finalToolResponses.forEach((response) => {
1581
- if (response) {
1582
- newMessages.push(response);
1583
- }
1584
- });
1686
+ const toReplayText = (value: unknown, maxLength = 2000): string => {
1687
+ const raw =
1688
+ typeof value === "string"
1689
+ ? value
1690
+ : (() => {
1691
+ try {
1692
+ return JSON.stringify(value);
1693
+ } catch (_error) {
1694
+ return String(value ?? "");
1695
+ }
1696
+ })();
1697
+ const normalized = String(raw ?? "").replace(/\s+/g, " ").trim();
1698
+ if (normalized.length <= maxLength) return normalized;
1699
+ return `${normalized.slice(0, maxLength - 3)}...`;
1700
+ };
1701
+
1702
+ if (toolCallBatch.length > 0) {
1703
+ const replayLines = toolCallBatch.map((toolCall, index) => {
1704
+ const matchedResponse =
1705
+ finalToolResponses.find(
1706
+ (response: any) => response?.tool_call_id === toolCall.callId
1707
+ ) || finalToolResponses[index];
1708
+ const status = matchedResponse?.isError ? "error" : "ok";
1709
+ const resultText = toReplayText(matchedResponse?.result ?? "No result returned");
1710
+
1711
+ return [
1712
+ `Tool: ${toolCall.toolName}`,
1713
+ `Call ID: ${toolCall.callId}`,
1714
+ `Status: ${status}`,
1715
+ `Args: ${toReplayText(toolCall.args, 600)}`,
1716
+ `Result: ${resultText}`,
1717
+ ].join("\n");
1718
+ });
1719
+
1720
+ newMessages.push({
1721
+ role: "user",
1722
+ content: [
1723
+ {
1724
+ type: "text",
1725
+ text: [
1726
+ "Tool execution summary for the previous request:",
1727
+ ...replayLines,
1728
+ "Continue the same assistant response from exactly where you paused using these tool results.",
1729
+ "If this response is using meta tags, keep the same format (<thinking>, <reasoning>, <searching>) in the continuation.",
1730
+ ].join("\n\n"),
1731
+ },
1732
+ ],
1733
+ });
1734
+ }
1585
1735
 
1586
1736
  send(
1587
1737
  "",
@@ -39,6 +39,8 @@ export interface MCPServer {
39
39
  status: 'active' | 'inactive';
40
40
  executionMode: 'CLIENT' | 'SERVER';
41
41
  url?: string;
42
+ accessToken?: string;
43
+ headers?: Record<string, string>;
42
44
  }
43
45
 
44
46
  export interface AgentProfile {
@@ -343,4 +345,3 @@ function extractAgentNameFromMessage(message: string): string | null {
343
345
  }
344
346
 
345
347
  export default useAgentRegistry;
346
-
package/src/mcpAuth.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { LLMAsAServiceCustomer } from "llmasaservice-client";
2
+
3
+ export type MCPAuthPhase = "list" | "call";
4
+
5
+ export interface MCPAuthHeaderResolverInput {
6
+ phase: MCPAuthPhase;
7
+ mcpServer: Record<string, unknown>;
8
+ projectId?: string;
9
+ customer?: LLMAsAServiceCustomer;
10
+ toolName?: string;
11
+ toolArgs?: unknown;
12
+ }
13
+
14
+ export type MCPAuthHeaderResolver = (
15
+ input: MCPAuthHeaderResolverInput
16
+ ) =>
17
+ | Record<string, string>
18
+ | null
19
+ | undefined
20
+ | Promise<Record<string, string> | null | undefined>;
21
+
22
+ export function normalizeMcpHeaders(
23
+ value: Record<string, unknown> | null | undefined
24
+ ): Record<string, string> {
25
+ const normalized: Record<string, string> = {};
26
+ if (!value) return normalized;
27
+
28
+ for (const [key, raw] of Object.entries(value)) {
29
+ if (typeof raw !== "string") continue;
30
+ const trimmedValue = raw.trim();
31
+ if (!trimmedValue) continue;
32
+ normalized[key] = trimmedValue;
33
+ }
34
+
35
+ return normalized;
36
+ }