chat-agent-toolkit 1.2.48 → 1.2.50
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.
- package/dist/index.d.ts +1 -0
- package/dist/mastra/agents.d.ts +72 -0
- package/dist/mastra/evals.d.ts +63 -0
- package/dist/mastra/index.d.ts +15 -0
- package/dist/mastra/model-routing.d.ts +65 -0
- package/dist/mastra/rag.d.ts +83 -0
- package/dist/mastra/telemetry.d.ts +35 -0
- package/dist/mastra/workflows.d.ts +79 -0
- package/dist/research-agent.cjs.js +1 -1
- package/dist/research-agent.cjs.js.map +1 -1
- package/dist/research-agent.es.js +1 -1
- package/dist/research-agent.es.js.map +1 -1
- package/dist/utils/markdown-to-html.d.ts +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/mastra/agents.ts +109 -0
- package/src/mastra/evals.ts +222 -0
- package/src/mastra/index.ts +45 -0
- package/src/mastra/model-routing.ts +166 -0
- package/src/mastra/rag.ts +188 -0
- package/src/mastra/telemetry.ts +71 -0
- package/src/mastra/workflows.ts +155 -0
- package/src/tools/search/search-handlers.ts +1 -2
- package/src/utils/markdown-to-html.ts +10 -7
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Mastra RAG (Retrieval-Augmented Generation)
|
|
3
|
+
*
|
|
4
|
+
* Document processing, chunking, embedding, and vector-store retrieval.
|
|
5
|
+
* Uses Vercel AI SDK embeddings with pluggable vector backends.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { embed, embedMany } from "ai";
|
|
9
|
+
|
|
10
|
+
export interface RAGConfig {
|
|
11
|
+
embeddingModel: any;
|
|
12
|
+
chunkSize?: number;
|
|
13
|
+
chunkOverlap?: number;
|
|
14
|
+
topK?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RAGDocument {
|
|
18
|
+
id: string;
|
|
19
|
+
content: string;
|
|
20
|
+
metadata?: Record<string, any>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RAGChunk {
|
|
24
|
+
id: string;
|
|
25
|
+
documentId: string;
|
|
26
|
+
text: string;
|
|
27
|
+
embedding: number[];
|
|
28
|
+
metadata?: Record<string, any>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RAGRetrievalResult {
|
|
32
|
+
chunk: RAGChunk;
|
|
33
|
+
score: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* In-memory RAG pipeline with document chunking, embedding, and retrieval.
|
|
38
|
+
* For production, swap the vector store with Pinecone, Qdrant, or pgvector.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { MastraRAG } from "chat-agent-toolkit/mastra";
|
|
43
|
+
* import { openai } from "@ai-sdk/openai";
|
|
44
|
+
*
|
|
45
|
+
* const rag = new MastraRAG({
|
|
46
|
+
* embeddingModel: openai.embedding("text-embedding-3-small"),
|
|
47
|
+
* chunkSize: 512,
|
|
48
|
+
* topK: 5,
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* await rag.ingest([
|
|
52
|
+
* { id: "doc1", content: "Long document text..." },
|
|
53
|
+
* ]);
|
|
54
|
+
*
|
|
55
|
+
* const results = await rag.retrieve("What is the main topic?");
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export class MastraRAG {
|
|
59
|
+
private config: Required<RAGConfig>;
|
|
60
|
+
private chunks: RAGChunk[] = [];
|
|
61
|
+
|
|
62
|
+
constructor(config: RAGConfig) {
|
|
63
|
+
this.config = {
|
|
64
|
+
embeddingModel: config.embeddingModel,
|
|
65
|
+
chunkSize: config.chunkSize ?? 512,
|
|
66
|
+
chunkOverlap: config.chunkOverlap ?? 64,
|
|
67
|
+
topK: config.topK ?? 5,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Split text into overlapping chunks of configured size.
|
|
73
|
+
*/
|
|
74
|
+
private splitIntoChunks(text: string): string[] {
|
|
75
|
+
const chunks: string[] = [];
|
|
76
|
+
const words = text.split(/\s+/);
|
|
77
|
+
const { chunkSize, chunkOverlap } = this.config;
|
|
78
|
+
|
|
79
|
+
let start = 0;
|
|
80
|
+
while (start < words.length) {
|
|
81
|
+
const end = Math.min(start + chunkSize, words.length);
|
|
82
|
+
chunks.push(words.slice(start, end).join(" "));
|
|
83
|
+
start += chunkSize - chunkOverlap;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return chunks;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Ingest documents: chunk, embed, and store.
|
|
91
|
+
*/
|
|
92
|
+
async ingest(documents: RAGDocument[]): Promise<number> {
|
|
93
|
+
let totalChunks = 0;
|
|
94
|
+
|
|
95
|
+
for (const doc of documents) {
|
|
96
|
+
const textChunks = this.splitIntoChunks(doc.content);
|
|
97
|
+
|
|
98
|
+
const { embeddings } = await embedMany({
|
|
99
|
+
model: this.config.embeddingModel,
|
|
100
|
+
values: textChunks,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < textChunks.length; i++) {
|
|
104
|
+
this.chunks.push({
|
|
105
|
+
id: `${doc.id}_chunk_${i}`,
|
|
106
|
+
documentId: doc.id,
|
|
107
|
+
text: textChunks[i],
|
|
108
|
+
embedding: embeddings[i],
|
|
109
|
+
metadata: doc.metadata,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
totalChunks += textChunks.length;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return totalChunks;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Retrieve relevant chunks for a query using cosine similarity.
|
|
121
|
+
*/
|
|
122
|
+
async retrieve(query: string): Promise<RAGRetrievalResult[]> {
|
|
123
|
+
if (this.chunks.length === 0) return [];
|
|
124
|
+
|
|
125
|
+
const { embedding } = await embed({
|
|
126
|
+
model: this.config.embeddingModel,
|
|
127
|
+
value: query,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const scored = this.chunks.map((chunk) => ({
|
|
131
|
+
chunk,
|
|
132
|
+
score: cosineSimilarity(embedding, chunk.embedding),
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
scored.sort((a, b) => b.score - a.score);
|
|
136
|
+
|
|
137
|
+
return scored.slice(0, this.config.topK);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Retrieve and format context string for LLM consumption.
|
|
142
|
+
*/
|
|
143
|
+
async getContext(query: string): Promise<string> {
|
|
144
|
+
const results = await this.retrieve(query);
|
|
145
|
+
if (results.length === 0) return "";
|
|
146
|
+
|
|
147
|
+
return results
|
|
148
|
+
.map((r, i) => `[${i + 1}] (score: ${r.score.toFixed(3)}) ${r.chunk.text}`)
|
|
149
|
+
.join("\n\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get total chunk count in the store.
|
|
154
|
+
*/
|
|
155
|
+
get size(): number {
|
|
156
|
+
return this.chunks.length;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Clear all stored chunks.
|
|
161
|
+
*/
|
|
162
|
+
clear(): void {
|
|
163
|
+
this.chunks = [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Factory for creating a configured RAG pipeline.
|
|
169
|
+
*/
|
|
170
|
+
export function createRAGPipeline(config: RAGConfig): MastraRAG {
|
|
171
|
+
return new MastraRAG(config);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function cosineSimilarity(a: number[], b: number[]): number {
|
|
175
|
+
let dotProduct = 0;
|
|
176
|
+
let normA = 0;
|
|
177
|
+
let normB = 0;
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < a.length; i++) {
|
|
180
|
+
dotProduct += a[i] * b[i];
|
|
181
|
+
normA += a[i] * a[i];
|
|
182
|
+
normB += b[i] * b[i];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
186
|
+
if (denominator === 0) return 0;
|
|
187
|
+
return dotProduct / denominator;
|
|
188
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Mastra Telemetry & Instance Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates configured Mastra instances with OpenTelemetry-compatible
|
|
5
|
+
* tracing, logging, and agent registration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Mastra } from "@mastra/core";
|
|
9
|
+
|
|
10
|
+
export interface TelemetryConfig {
|
|
11
|
+
serviceName: string;
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
sampling?: number;
|
|
14
|
+
exporterUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MastraInstanceConfig {
|
|
18
|
+
agents?: Record<string, any>;
|
|
19
|
+
workflows?: Record<string, any>;
|
|
20
|
+
telemetry?: TelemetryConfig;
|
|
21
|
+
storage?: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a fully configured Mastra instance with agents, workflows,
|
|
26
|
+
* and telemetry. Central entry point for Mastra framework setup.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { createMastraInstance } from "chat-agent-toolkit/mastra";
|
|
31
|
+
*
|
|
32
|
+
* const mastra = createMastraInstance({
|
|
33
|
+
* agents: { assistant, researcher },
|
|
34
|
+
* workflows: { researchWorkflow },
|
|
35
|
+
* telemetry: {
|
|
36
|
+
* serviceName: "my-app",
|
|
37
|
+
* enabled: true,
|
|
38
|
+
* },
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* const agent = mastra.getAgent("assistant");
|
|
42
|
+
* const result = await agent.generate("Hello");
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function createMastraInstance(config: MastraInstanceConfig): Mastra {
|
|
46
|
+
const mastraConfig: any = {};
|
|
47
|
+
|
|
48
|
+
if (config.agents) {
|
|
49
|
+
mastraConfig.agents = config.agents;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (config.workflows) {
|
|
53
|
+
mastraConfig.workflows = config.workflows;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (config.telemetry) {
|
|
57
|
+
mastraConfig.telemetry = {
|
|
58
|
+
serviceName: config.telemetry.serviceName,
|
|
59
|
+
enabled: config.telemetry.enabled ?? true,
|
|
60
|
+
sampling: {
|
|
61
|
+
default: config.telemetry.sampling ?? 1.0,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (config.storage) {
|
|
67
|
+
mastraConfig.storage = config.storage;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return new Mastra(mastraConfig);
|
|
71
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Mastra Workflow Builder
|
|
3
|
+
*
|
|
4
|
+
* Graph-based deterministic workflows with typed steps, chaining,
|
|
5
|
+
* branching, and parallel execution support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createWorkflow, createStep } from "@mastra/core/workflows";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
export interface WorkflowStepConfig<TInput = any, TOutput = any> {
|
|
12
|
+
id: string;
|
|
13
|
+
inputSchema: z.ZodType<TInput>;
|
|
14
|
+
outputSchema: z.ZodType<TOutput>;
|
|
15
|
+
execute: (params: { inputData: TInput }) => Promise<TOutput>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a typed workflow step.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const fetchStep = createWorkflowStep({
|
|
24
|
+
* id: "fetch-sources",
|
|
25
|
+
* inputSchema: z.object({ query: z.string() }),
|
|
26
|
+
* outputSchema: z.object({ sources: z.array(z.string()) }),
|
|
27
|
+
* execute: async ({ inputData }) => {
|
|
28
|
+
* const sources = await searchWeb(inputData.query);
|
|
29
|
+
* return { sources };
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function createWorkflowStep<TInput, TOutput>(
|
|
35
|
+
config: WorkflowStepConfig<TInput, TOutput>
|
|
36
|
+
) {
|
|
37
|
+
return createStep({
|
|
38
|
+
id: config.id,
|
|
39
|
+
inputSchema: config.inputSchema,
|
|
40
|
+
outputSchema: config.outputSchema,
|
|
41
|
+
execute: config.execute,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a research workflow: fetch → analyze → summarize.
|
|
47
|
+
* Pre-built pipeline for common research tasks.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* const workflow = createResearchWorkflow({
|
|
52
|
+
* fetchFn: async (topic) => fetchSources(topic),
|
|
53
|
+
* analyzeFn: async (sources) => analyzeContent(sources),
|
|
54
|
+
* summarizeFn: async (analysis) => generateSummary(analysis),
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* const run = await workflow.createRunAsync();
|
|
58
|
+
* const result = await run.start({ inputData: { topic: "quantum computing" } });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function createResearchWorkflow(handlers: {
|
|
62
|
+
fetchFn: (topic: string) => Promise<string[]>;
|
|
63
|
+
analyzeFn: (sources: string[]) => Promise<string>;
|
|
64
|
+
summarizeFn: (analysis: string) => Promise<string>;
|
|
65
|
+
}) {
|
|
66
|
+
const fetchStep = createStep({
|
|
67
|
+
id: "fetch-sources",
|
|
68
|
+
inputSchema: z.object({ topic: z.string() }),
|
|
69
|
+
outputSchema: z.object({ sources: z.array(z.string()) }),
|
|
70
|
+
execute: async ({ inputData }) => {
|
|
71
|
+
const sources = await handlers.fetchFn(inputData.topic);
|
|
72
|
+
return { sources };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const analyzeStep = createStep({
|
|
77
|
+
id: "analyze-content",
|
|
78
|
+
inputSchema: z.object({ sources: z.array(z.string()) }),
|
|
79
|
+
outputSchema: z.object({ analysis: z.string() }),
|
|
80
|
+
execute: async ({ inputData }) => {
|
|
81
|
+
const analysis = await handlers.analyzeFn(inputData.sources);
|
|
82
|
+
return { analysis };
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const summarizeStep = createStep({
|
|
87
|
+
id: "summarize",
|
|
88
|
+
inputSchema: z.object({ analysis: z.string() }),
|
|
89
|
+
outputSchema: z.object({ summary: z.string() }),
|
|
90
|
+
execute: async ({ inputData }) => {
|
|
91
|
+
const summary = await handlers.summarizeFn(inputData.analysis);
|
|
92
|
+
return { summary };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return createWorkflow({
|
|
97
|
+
id: "research-workflow",
|
|
98
|
+
inputSchema: z.object({ topic: z.string() }),
|
|
99
|
+
outputSchema: z.object({ summary: z.string() }),
|
|
100
|
+
})
|
|
101
|
+
.then(fetchStep)
|
|
102
|
+
.then(analyzeStep)
|
|
103
|
+
.then(summarizeStep)
|
|
104
|
+
.commit();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a RAG workflow: chunk → embed → retrieve → generate.
|
|
109
|
+
* End-to-end retrieval-augmented generation pipeline.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* const workflow = createRAGWorkflow({
|
|
114
|
+
* chunkFn: async (doc) => splitIntoChunks(doc),
|
|
115
|
+
* embedFn: async (chunks) => embedChunks(chunks),
|
|
116
|
+
* retrieveFn: async (query) => findRelevant(query),
|
|
117
|
+
* generateFn: async (context, query) => llmGenerate(context, query),
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function createRAGWorkflow(handlers: {
|
|
122
|
+
chunkFn: (document: string) => Promise<string[]>;
|
|
123
|
+
embedFn: (chunks: string[]) => Promise<number[][]>;
|
|
124
|
+
retrieveFn: (query: string) => Promise<string[]>;
|
|
125
|
+
generateFn: (context: string[], query: string) => Promise<string>;
|
|
126
|
+
}) {
|
|
127
|
+
const retrieveStep = createStep({
|
|
128
|
+
id: "retrieve",
|
|
129
|
+
inputSchema: z.object({ query: z.string() }),
|
|
130
|
+
outputSchema: z.object({ context: z.array(z.string()), query: z.string() }),
|
|
131
|
+
execute: async ({ inputData }) => {
|
|
132
|
+
const context = await handlers.retrieveFn(inputData.query);
|
|
133
|
+
return { context, query: inputData.query };
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const generateStep = createStep({
|
|
138
|
+
id: "generate",
|
|
139
|
+
inputSchema: z.object({ context: z.array(z.string()), query: z.string() }),
|
|
140
|
+
outputSchema: z.object({ answer: z.string() }),
|
|
141
|
+
execute: async ({ inputData }) => {
|
|
142
|
+
const answer = await handlers.generateFn(inputData.context, inputData.query);
|
|
143
|
+
return { answer };
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return createWorkflow({
|
|
148
|
+
id: "rag-workflow",
|
|
149
|
+
inputSchema: z.object({ query: z.string() }),
|
|
150
|
+
outputSchema: z.object({ answer: z.string() }),
|
|
151
|
+
})
|
|
152
|
+
.then(retrieveStep)
|
|
153
|
+
.then(generateStep)
|
|
154
|
+
.commit();
|
|
155
|
+
}
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
* @description Research library module.
|
|
4
4
|
*
|
|
5
5
|
* Note: To use these search handlers, you need to provide search functions
|
|
6
|
-
* (searchSearxng, searchTavily, etc.) from
|
|
7
|
-
* See extract-webpage/src/search/index.ts for an example.
|
|
6
|
+
* (searchSearxng, searchTavily, etc.) from search-web-api package.
|
|
8
7
|
*/
|
|
9
8
|
import MetaSearchAgent from "./metaSearchAgent";
|
|
10
9
|
import {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module research/agents/markdown-to-html
|
|
3
|
-
* @description Converts Markdown to HTML with
|
|
3
|
+
* @description Converts Markdown to HTML with Prism.js syntax highlighting.
|
|
4
4
|
*/
|
|
5
5
|
import { marked } from "marked";
|
|
6
|
-
import
|
|
6
|
+
import Prism from "prismjs";
|
|
7
|
+
import loadLanguages from "prismjs/components/index.js";
|
|
7
8
|
import { encode, decode } from "html-entities";
|
|
8
9
|
|
|
10
|
+
loadLanguages();
|
|
11
|
+
|
|
9
12
|
// Inline onclick: self-contained per button, works in dangerouslySetInnerHTML contexts
|
|
10
13
|
const COPY_ONCLICK = [
|
|
11
14
|
"(function(b){",
|
|
@@ -28,22 +31,22 @@ marked.use({
|
|
|
28
31
|
let highlighted: string;
|
|
29
32
|
|
|
30
33
|
try {
|
|
31
|
-
if (language &&
|
|
32
|
-
highlighted =
|
|
34
|
+
if (language && Prism.languages[language]) {
|
|
35
|
+
highlighted = Prism.highlight(text, Prism.languages[language], language);
|
|
33
36
|
} else {
|
|
34
|
-
highlighted =
|
|
37
|
+
highlighted = encode(text);
|
|
35
38
|
}
|
|
36
39
|
} catch (e) {
|
|
37
40
|
highlighted = encode(text);
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
return `<figure class="code-block"><button class="code-copy-btn" type="button" onclick="${COPY_ONCLICK}">Copy</button><pre><code class="
|
|
43
|
+
return `<figure class="code-block"><button class="code-copy-btn" type="button" onclick="${COPY_ONCLICK}">Copy</button><pre><code class="language-${language}">${highlighted}</code></pre></figure>`;
|
|
41
44
|
},
|
|
42
45
|
},
|
|
43
46
|
});
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
|
-
* Convert markdown text to HTML with
|
|
49
|
+
* Convert markdown text to HTML with Prism.js syntax highlighting.
|
|
47
50
|
* Unescapes HTML entities like `&` \u2192 `&`.
|
|
48
51
|
*/
|
|
49
52
|
export async function convertMarkdownToHTMLEscaped(
|