refactor: use Nuxt 4 directory structure
This commit is contained in:
46
app/composables/useAiProvider.ts
Normal file
46
app/composables/useAiProvider.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { createDeepSeek } from '@ai-sdk/deepseek'
|
||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
||||
import { createOpenAI } from '@ai-sdk/openai'
|
||||
import {
|
||||
extractReasoningMiddleware,
|
||||
wrapLanguageModel,
|
||||
type LanguageModelV1,
|
||||
} from 'ai'
|
||||
|
||||
export const useAiModel = () => {
|
||||
const { config, aiApiBase } = useConfigStore()
|
||||
let model: LanguageModelV1
|
||||
|
||||
if (config.ai.provider === 'openrouter') {
|
||||
const openRouter = createOpenRouter({
|
||||
apiKey: config.ai.apiKey,
|
||||
baseURL: aiApiBase,
|
||||
})
|
||||
model = openRouter(config.ai.model, {
|
||||
includeReasoning: true,
|
||||
})
|
||||
} else if (
|
||||
config.ai.provider === 'deepseek' ||
|
||||
config.ai.provider === 'siliconflow' ||
|
||||
// Special case if model name includes 'deepseek'
|
||||
// This ensures compatibilty with providers like Siliconflow
|
||||
config.ai.model?.toLowerCase().includes('deepseek')
|
||||
) {
|
||||
const deepSeek = createDeepSeek({
|
||||
apiKey: config.ai.apiKey,
|
||||
baseURL: aiApiBase,
|
||||
})
|
||||
model = deepSeek(config.ai.model)
|
||||
} else {
|
||||
const openai = createOpenAI({
|
||||
apiKey: config.ai.apiKey,
|
||||
baseURL: aiApiBase,
|
||||
})
|
||||
model = openai(config.ai.model)
|
||||
}
|
||||
|
||||
return wrapLanguageModel({
|
||||
model,
|
||||
middleware: extractReasoningMiddleware({ tagName: 'think' }),
|
||||
})
|
||||
}
|
62
app/composables/useNodeLayout.ts
Normal file
62
app/composables/useNodeLayout.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import dagre from '@dagrejs/dagre'
|
||||
import { Position, useVueFlow, type Edge, type Node } from '@vue-flow/core'
|
||||
|
||||
// Picked from https://vueflow.dev/examples/layout/animated.html
|
||||
export function useNodeLayout() {
|
||||
const { findNode } = useVueFlow()
|
||||
|
||||
function layout(nodes: Node[], edges: Edge[]) {
|
||||
// we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
|
||||
const isHorizontal = true
|
||||
dagreGraph.setGraph({
|
||||
rankdir: 'LR',
|
||||
// distance between nodes at the same level
|
||||
nodesep: 25,
|
||||
// distance between levels
|
||||
ranksep: 30,
|
||||
})
|
||||
|
||||
for (const node of nodes) {
|
||||
// if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
|
||||
const graphNode = findNode(node.id)
|
||||
|
||||
if (!graphNode) {
|
||||
console.error(`Node with id ${node.id} not found in the graph`)
|
||||
continue
|
||||
}
|
||||
|
||||
dagreGraph.setNode(node.id, {
|
||||
width: graphNode.dimensions.width || 100,
|
||||
height: graphNode.dimensions.height || 50,
|
||||
})
|
||||
}
|
||||
|
||||
for (const edge of edges) {
|
||||
dagreGraph.setEdge(edge.source, edge.target)
|
||||
}
|
||||
|
||||
dagre.layout(dagreGraph, {
|
||||
rankdir: 'LR',
|
||||
nodesep: 25,
|
||||
ranksep: 30,
|
||||
})
|
||||
|
||||
// set nodes with updated positions
|
||||
return nodes.map((node) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id)
|
||||
|
||||
return {
|
||||
...node,
|
||||
targetPosition: isHorizontal ? Position.Left : Position.Top,
|
||||
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
|
||||
position: { x: nodeWithPosition.x, y: nodeWithPosition.y },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { layout }
|
||||
}
|
30
app/composables/usePLimit.ts
Normal file
30
app/composables/usePLimit.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import pLimit from 'p-limit'
|
||||
|
||||
/**
|
||||
* The concurrency value used by the global limit.
|
||||
* This represents the *actual* limit value.
|
||||
* The value in `globalLimit` should not be used, because `deepResearch` uses recursive calls,
|
||||
* and `globalLimit.concurrency` can be much higher than the actual one.
|
||||
*/
|
||||
let globalLimitConcurrency = 2
|
||||
const globalLimit = pLimit(globalLimitConcurrency)
|
||||
|
||||
export function usePLimit() {
|
||||
const { config } = useConfigStore()
|
||||
|
||||
if (
|
||||
config.webSearch.concurrencyLimit &&
|
||||
config.webSearch.concurrencyLimit >= 1 &&
|
||||
globalLimitConcurrency !== config.webSearch.concurrencyLimit
|
||||
) {
|
||||
console.log(
|
||||
`[usePLimit] Updating concurrency from ${globalLimitConcurrency} to ${config.webSearch.concurrencyLimit}. Current concurrency: ${globalLimit.concurrency}`,
|
||||
)
|
||||
let newLimit = config.webSearch.concurrencyLimit
|
||||
let diff = newLimit - globalLimitConcurrency
|
||||
globalLimitConcurrency = newLimit
|
||||
globalLimit.concurrency += diff
|
||||
}
|
||||
|
||||
return globalLimit
|
||||
}
|
72
app/composables/useWebSearch.ts
Normal file
72
app/composables/useWebSearch.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { tavily } from '@tavily/core'
|
||||
import Firecrawl from '@mendable/firecrawl-js'
|
||||
|
||||
type WebSearchOptions = {
|
||||
maxResults?: number
|
||||
/** The search language, e.g. `en`. Only works for Firecrawl. */
|
||||
lang?: string
|
||||
}
|
||||
|
||||
export type WebSearchResult = {
|
||||
content: string
|
||||
url: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
type WebSearchFunction = (
|
||||
query: string,
|
||||
options: WebSearchOptions,
|
||||
) => Promise<WebSearchResult[]>
|
||||
|
||||
export const useWebSearch = (): WebSearchFunction => {
|
||||
const { config, webSearchApiBase } = useConfigStore()
|
||||
|
||||
switch (config.webSearch.provider) {
|
||||
case 'firecrawl': {
|
||||
const fc = new Firecrawl({
|
||||
apiKey: config.webSearch.apiKey,
|
||||
apiUrl: webSearchApiBase,
|
||||
})
|
||||
return async (q: string, o: WebSearchOptions) => {
|
||||
const results = await fc.search(q, {
|
||||
...o,
|
||||
scrapeOptions: {
|
||||
formats: ['markdown']
|
||||
}
|
||||
})
|
||||
if (results.error) {
|
||||
throw new Error(results.error)
|
||||
}
|
||||
return results.data
|
||||
.filter((x) => !!x?.markdown && !!x.url)
|
||||
.map((r) => ({
|
||||
content: r.markdown!,
|
||||
url: r.url!,
|
||||
title: r.title,
|
||||
}))
|
||||
}
|
||||
}
|
||||
case 'tavily':
|
||||
default: {
|
||||
const tvly = tavily({
|
||||
apiKey: config.webSearch.apiKey,
|
||||
})
|
||||
return async (q: string, o: WebSearchOptions) => {
|
||||
const results = await tvly.search(q, {
|
||||
...o,
|
||||
searchDepth: config.webSearch.tavilyAdvancedSearch
|
||||
? 'advanced'
|
||||
: 'basic',
|
||||
topic: config.webSearch.tavilySearchTopic,
|
||||
})
|
||||
return results.results
|
||||
.filter((x) => !!x?.content && !!x.url)
|
||||
.map((r) => ({
|
||||
content: r.content,
|
||||
url: r.url,
|
||||
title: r.title,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user