update research mode, add search web and fetch url content

This commit is contained in:
duanfuxiang 2025-03-15 08:17:10 +08:00
parent 05b1302a6c
commit 9a5e5f3880
12 changed files with 553 additions and 7 deletions

View File

@ -62,6 +62,7 @@
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-tooltip": "^1.1.3",
"@tanstack/react-query": "^5.56.2",
"axios": "^1.8.3",
"clsx": "^2.1.1",
"diff": "^7.0.0",
"drizzle-orm": "^0.35.2",

134
pnpm-lock.yaml generated
View File

@ -44,6 +44,9 @@ importers:
'@tanstack/react-query':
specifier: ^5.56.2
version: 5.66.0(react@18.3.1)
axios:
specifier: ^1.8.3
version: 1.8.3
clsx:
specifier: ^2.1.1
version: 2.1.1
@ -76,7 +79,7 @@ importers:
version: 2.2.3
langchain:
specifier: ^0.3.2
version: 0.3.15(@langchain/core@0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2)))(handlebars@4.7.8)(openai@4.85.1(ws@8.18.0)(zod@3.24.2))(ws@8.18.0)
version: 0.3.15(@langchain/core@0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2)))(axios@1.8.3)(handlebars@4.7.8)(openai@4.85.1(ws@8.18.0)(zod@3.24.2))(ws@8.18.0)
lexical:
specifier: ^0.17.1
version: 0.17.1
@ -128,6 +131,9 @@ importers:
react-syntax-highlighter:
specifier: ^15.5.0
version: 15.6.1(react@18.3.1)
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
remark-gfm:
specifier: ^4.0.0
version: 4.0.1
@ -1788,6 +1794,9 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
axios@1.8.3:
resolution: {integrity: sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==}
babel-jest@29.7.0:
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -2526,6 +2535,15 @@ packages:
flatted@3.3.2:
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
@ -2694,18 +2712,33 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hast-util-from-parse5@8.0.3:
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
hast-util-parse-selector@2.2.5:
resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==}
hast-util-parse-selector@4.0.0:
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
hast-util-raw@9.1.0:
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
hast-util-to-jsx-runtime@2.3.2:
resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==}
hast-util-to-parse5@8.0.0:
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
hastscript@6.0.0:
resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==}
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
highlight.js@10.7.3:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
@ -2729,6 +2762,9 @@ packages:
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
http-proxy-agent@5.0.0:
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
engines: {node: '>= 6'}
@ -3811,6 +3847,12 @@ packages:
property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
property-information@7.0.0:
resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
psl@1.15.0:
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
@ -3903,6 +3945,9 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
@ -4365,6 +4410,9 @@ packages:
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
engines: {node: '>=10.12.0'}
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
vfile-message@4.0.2:
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
@ -4381,6 +4429,9 @@ packages:
walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
@ -6174,6 +6225,14 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
axios@1.8.3:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.2
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
babel-jest@29.7.0(@babel/core@7.26.9):
dependencies:
'@babel/core': 7.26.9
@ -7020,6 +7079,8 @@ snapshots:
flatted@3.3.2: {}
follow-redirects@1.15.9: {}
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
@ -7202,8 +7263,39 @@ snapshots:
dependencies:
function-bind: 1.1.2
hast-util-from-parse5@8.0.3:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
devlop: 1.1.0
hastscript: 9.0.1
property-information: 7.0.0
vfile: 6.0.3
vfile-location: 5.0.3
web-namespaces: 2.0.1
hast-util-parse-selector@2.2.5: {}
hast-util-parse-selector@4.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-raw@9.1.0:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
'@ungap/structured-clone': 1.3.0
hast-util-from-parse5: 8.0.3
hast-util-to-parse5: 8.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.0
parse5: 7.2.1
unist-util-position: 5.0.0
unist-util-visit: 5.0.0
vfile: 6.0.3
web-namespaces: 2.0.1
zwitch: 2.0.4
hast-util-to-jsx-runtime@2.3.2:
dependencies:
'@types/estree': 1.0.6
@ -7224,6 +7316,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
hast-util-to-parse5@8.0.0:
dependencies:
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
devlop: 1.1.0
property-information: 6.5.0
space-separated-tokens: 2.0.2
web-namespaces: 2.0.1
zwitch: 2.0.4
hast-util-whitespace@3.0.0:
dependencies:
'@types/hast': 3.0.4
@ -7236,6 +7338,14 @@ snapshots:
property-information: 5.6.0
space-separated-tokens: 1.1.5
hastscript@9.0.1:
dependencies:
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
hast-util-parse-selector: 4.0.0
property-information: 7.0.0
space-separated-tokens: 2.0.2
highlight.js@10.7.3: {}
highlightjs-vue@1.0.0: {}
@ -7252,6 +7362,8 @@ snapshots:
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
http-proxy-agent@5.0.0:
dependencies:
'@tootallnate/once': 2.0.0
@ -7934,7 +8046,7 @@ snapshots:
known-css-properties@0.35.0: {}
langchain@0.3.15(@langchain/core@0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2)))(handlebars@4.7.8)(openai@4.85.1(ws@8.18.0)(zod@3.24.2))(ws@8.18.0):
langchain@0.3.15(@langchain/core@0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2)))(axios@1.8.3)(handlebars@4.7.8)(openai@4.85.1(ws@8.18.0)(zod@3.24.2))(ws@8.18.0):
dependencies:
'@langchain/core': 0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2))
'@langchain/openai': 0.4.4(@langchain/core@0.3.40(openai@4.85.1(ws@8.18.0)(zod@3.24.2)))(ws@8.18.0)
@ -7950,6 +8062,7 @@ snapshots:
zod: 3.24.2
zod-to-json-schema: 3.24.1(zod@3.24.2)
optionalDependencies:
axios: 1.8.3
handlebars: 4.7.8
transitivePeerDependencies:
- encoding
@ -8696,6 +8809,10 @@ snapshots:
property-information@6.5.0: {}
property-information@7.0.0: {}
proxy-from-env@1.1.0: {}
psl@1.15.0:
dependencies:
punycode: 2.3.1
@ -8809,6 +8926,12 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
rehype-raw@7.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-raw: 9.1.0
vfile: 6.0.3
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
@ -9370,6 +9493,11 @@ snapshots:
'@types/istanbul-lib-coverage': 2.0.6
convert-source-map: 2.0.0
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3
vfile: 6.0.3
vfile-message@4.0.2:
dependencies:
'@types/unist': 3.0.3
@ -9390,6 +9518,8 @@ snapshots:
dependencies:
makeerror: 1.0.12
web-namespaces@2.0.1: {}
web-streams-polyfill@4.0.0-beta.3: {}
webidl-conversions@3.0.1: {}

View File

@ -44,6 +44,7 @@ import {
import { readTFileContent } from '../../utils/obsidian'
import { openSettingsModalWithError } from '../../utils/open-settings-modal'
import { PromptGenerator, addLineNumbers } from '../../utils/prompt-generator'
import { fetchUrlsContent, webSearch } from '../../utils/web-search'
// Simple file reading function that returns a placeholder content for testing
const readFileContent = (filePath: string): string => {
@ -283,7 +284,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
chatModel,
{
model: chatModel.modelId,
temperature: 0.5,
temperature: 0,
messages: requestMessages,
stream: true,
},
@ -532,6 +533,38 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
mentionables: [],
}
}
} else if (toolArgs.type === 'search_web') {
const results = await webSearch(toolArgs.query)
const formattedContent = `[search_web for '${toolArgs.query}'] Result:\n${results}\n`;
return {
type: 'search_web',
applyMsgId,
applyStatus: ApplyStatus.Applied,
returnMsg: {
role: 'user',
applyStatus: ApplyStatus.Idle,
content: null,
promptContent: formattedContent,
id: uuidv4(),
mentionables: [],
}
}
} else if (toolArgs.type === 'fetch_urls_content') {
const results = await fetchUrlsContent(toolArgs.urls)
const formattedContent = `[ fetch_urls_content ] Result:\n${results}\n`;
return {
type: 'fetch_urls_content',
applyMsgId,
applyStatus: ApplyStatus.Applied,
returnMsg: {
role: 'user',
applyStatus: ApplyStatus.Idle,
content: null,
promptContent: formattedContent,
id: uuidv4(),
mentionables: [],
}
}
}
} catch (error) {
console.error('Failed to apply changes', error)

View File

@ -0,0 +1,81 @@
import { ChevronDown, ChevronRight, Globe } from 'lucide-react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useDarkModeContext } from '../../contexts/DarkModeContext'
import { ApplyStatus, FetchUrlsContentToolArgs } from '../../types/apply'
import { MemoizedSyntaxHighlighterWrapper } from './SyntaxHighlighterWrapper'
export default function MarkdownFetchUrlsContentBlock({
applyStatus,
onApply,
urls,
finish
}: {
applyStatus: ApplyStatus
onApply: (args: FetchUrlsContentToolArgs) => void
urls: string[],
finish: boolean
}) {
const { isDarkMode } = useDarkModeContext()
const containerRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(true)
React.useEffect(() => {
console.log('finish', finish, applyStatus)
if (finish && applyStatus === ApplyStatus.Idle) {
console.log('finish auto fetch urls content', urls)
onApply({
type: 'fetch_urls_content',
urls: urls
})
}
}, [finish])
const urlsMarkdownContent = useMemo(() => {
return urls.map(url => {
return `${url}`
}).join('\n\n')
}, [urls])
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight
}
}, [urlsMarkdownContent])
return (
urlsMarkdownContent && (
<div
className={`infio-chat-code-block has-filename infio-reasoning-block`}
>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<Globe size={10} className="infio-chat-code-block-header-icon" />
Fetch URLs Content
</div>
<button
className="clickable-icon infio-chat-list-dropdown"
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
</div>
<div
ref={containerRef}
className="infio-reasoning-content-wrapper"
>
<MemoizedSyntaxHighlighterWrapper
isDarkMode={isDarkMode}
language="markdown"
hasFilename={true}
wrapLines={true}
isOpen={isOpen}
>
{urlsMarkdownContent}
</MemoizedSyntaxHighlighterWrapper>
</div>
</div>
)
)
}

View File

@ -0,0 +1,41 @@
import { Search } from 'lucide-react'
import React from 'react'
import { ApplyStatus, SearchWebToolArgs } from '../../types/apply'
export default function MarkdownWebSearchBlock({
applyStatus,
onApply,
query,
finish
}: {
applyStatus: ApplyStatus
onApply: (args: SearchWebToolArgs) => void
query: string,
finish: boolean
}) {
React.useEffect(() => {
console.log('finish', finish, applyStatus)
if (finish && applyStatus === ApplyStatus.Idle) {
console.log('finish auto web search', query)
onApply({
type: 'search_web',
query: query,
})
}
}, [finish])
return (
<div
className={`infio-chat-code-block has-filename`}
>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<Search size={14} className="infio-chat-code-block-header-icon" />
Web search: {query}
</div>
</div>
</div>
)
}

View File

@ -8,14 +8,15 @@ import {
} from '../../utils/parse-infio-block'
import MarkdownEditFileBlock from './MarkdownEditFileBlock'
import MarkdownFetchUrlsContentBlock from './MarkdownFetchUrlsContentBlock'
import MarkdownListFilesBlock from './MarkdownListFilesBlock'
import MarkdownReadFileBlock from './MarkdownReadFileBlock'
import MarkdownReasoningBlock from './MarkdownReasoningBlock'
import MarkdownRegexSearchFilesBlock from './MarkdownRegexSearchFilesBlock'
import MarkdownSearchAndReplace from './MarkdownSearchAndReplace'
import MarkdownSearchWebBlock from './MarkdownSearchWebBlock'
import MarkdownSemanticSearchFilesBlock from './MarkdownSemanticSearchFilesBlock'
import MarkdownWithIcons from './MarkdownWithIcon'
function ReactMarkdown({
applyStatus,
onApply,
@ -131,6 +132,22 @@ function ReactMarkdown({
markdownContent={
`<icon name='ask_followup_question' size={14} className="infio-markdown-icon" />
${block.question && block.question.trimStart()}`} />
) : block.type === 'search_web' ? (
<MarkdownSearchWebBlock
key={"search-web-" + index}
applyStatus={applyStatus}
onApply={onApply}
query={block.query}
finish={block.finish}
/>
) : block.type === 'fetch_urls_content' ? (
<MarkdownFetchUrlsContentBlock
key={"fetch-urls-content-" + index}
applyStatus={applyStatus}
onApply={onApply}
urls={block.urls}
finish={block.finish}
/>
) : (
<Markdown key={"markdown-" + index} className="infio-markdown">
{block.content}

View File

@ -63,4 +63,17 @@ export type SearchAndReplaceToolArgs = {
regexFlags?: string;
}[];
}
export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs;
export type SearchWebToolArgs = {
type: 'search_web';
query: string;
finish?: boolean;
}
export type FetchUrlsContentToolArgs = {
type: 'fetch_urls_content';
urls: string[];
finish?: boolean;
}
export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs;

View File

@ -78,6 +78,15 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
// Main modes configuration as an ordered array
export const modes: readonly ModeConfig[] = [
{
slug: "research",
name: "Research",
roleDefinition:
"You are Infio, an advanced research assistant specialized in comprehensive investigation and analytical thinking. You excel at breaking down complex questions, exploring multiple perspectives, and synthesizing information to provide well-reasoned conclusions.",
groups: ["research"],
customInstructions:
"You can conduct thorough research by analyzing available information, connecting related concepts, and applying structured reasoning methods. Help users explore topics in depth by considering multiple angles, identifying relevant evidence, and evaluating the reliability of sources. Use step-by-step analysis when tackling complex problems, explaining your thought process clearly. Create visual representations like Mermaid diagrams when they help clarify relationships between ideas. Use Markdown tables to present statistical data or comparative information when appropriate. Present balanced viewpoints while highlighting the strength of evidence behind different conclusions.",
},
{
slug: "write",
name: "Write",

View File

@ -60,6 +60,14 @@ export type ParsedMsgBlock =
path: string
query: string
finish: boolean
} | {
type: 'search_web'
query: string
finish: boolean
} | {
type: 'fetch_urls_content'
urls: string[]
finish: boolean
}
export function parseMsgBlocks(
@ -416,6 +424,66 @@ export function parseMsgBlocks(
})
lastEndOffset = endOffset
}
else if (node.nodeName === 'search_web') {
if (!node.sourceCodeLocation) {
throw new Error('sourceCodeLocation is undefined')
}
const startOffset = node.sourceCodeLocation.startOffset
const endOffset = node.sourceCodeLocation.endOffset
if (startOffset > lastEndOffset) {
parsedResult.push({
type: 'string',
content: input.slice(lastEndOffset, startOffset),
})
}
let query: string | undefined
for (const childNode of node.childNodes) {
if (childNode.nodeName === 'query' && childNode.childNodes.length > 0) {
query = childNode.childNodes[0].value
}
}
parsedResult.push({
type: 'search_web',
query: query || '',
finish: node.sourceCodeLocation.endTag !== undefined
})
lastEndOffset = endOffset
} else if (node.nodeName === 'fetch_urls_content') {
if (!node.sourceCodeLocation) {
throw new Error('sourceCodeLocation is undefined')
}
const startOffset = node.sourceCodeLocation.startOffset
const endOffset = node.sourceCodeLocation.endOffset
if (startOffset > lastEndOffset) {
parsedResult.push({
type: 'string',
content: input.slice(lastEndOffset, startOffset),
})
}
let urls: string[] = []
for (const childNode of node.childNodes) {
if (childNode.nodeName === 'urls' && childNode.childNodes.length > 0) {
try {
const urlsJson = childNode.childNodes[0].value
const parsedUrls = JSON5.parse(urlsJson)
if (Array.isArray(parsedUrls)) {
urls = parsedUrls
}
} catch (error) {
console.error('Failed to parse URLs JSON', error)
}
}
}
parsedResult.push({
type: 'fetch_urls_content',
urls,
finish: node.sourceCodeLocation.endTag !== undefined
})
lastEndOffset = endOffset
}
}
// handle the last part of the input

View File

@ -18,7 +18,6 @@ import {
import { InfioSettings } from '../types/settings'
import { defaultModeSlug, getFullModeDetails } from "../utils/modes"
import { listFilesAndFolders } from './glob-utils'
import {
readTFileContent
} from './obsidian'

View File

@ -30,6 +30,9 @@ export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
edit: {
tools: ["apply_diff", "write_to_file", "insert_content", "search_and_replace"],
},
research: {
tools: ["search_web", "fetch_urls_content"],
},
// browser: {
// tools: ["browser_action"],
// },
@ -52,7 +55,6 @@ export const ALWAYS_AVAILABLE_TOOLS = [
"ask_followup_question",
"attempt_completion",
"switch_mode",
"new_task",
] as const
// Tool name types for type safety
@ -71,6 +73,7 @@ export function getToolOptions(toolConfig: string | readonly [ToolName, ...any[]
export const GROUP_DISPLAY_NAMES: Record<ToolGroup, string> = {
read: "Read Files",
edit: "Edit Files",
research: "Research",
browser: "Use Browser",
command: "Run Commands",
mcp: "Use MCP",

151
src/utils/web-search.ts Normal file
View File

@ -0,0 +1,151 @@
import https from 'https';
import { htmlToMarkdown, requestUrl } from 'obsidian';
import { YoutubeTranscript, isYoutubeUrl } from './youtube-transcript';
const SERPER_API_KEY = 'a6fd4dc4b79f10b1e5008b688c81bacef0d24b4d5cd4e52071afa8329a67497c'
interface SearchResult {
title: string;
link: string;
snippet: string;
}
interface SearchResponse {
organic_results?: SearchResult[];
}
export async function webSearch(query: string): Promise<string> {
return new Promise((resolve, reject) => {
const url = `https://serpapi.com/search?q=${encodeURIComponent(query)}&engine=google&api_key=${SERPER_API_KEY}&num=20`;
console.log(url)
https.get(url, (res: any) => {
let data = '';
res.on('data', (chunk: Buffer) => {
data += chunk.toString();
});
res.on('end', () => {
try {
console.log(data)
let parsedData: SearchResponse;
try {
parsedData = JSON.parse(data);
} catch {
parsedData = { organic_results: undefined };
}
const results = parsedData?.organic_results;
if (!results) {
resolve('');
return;
}
const formattedResults = results.map((item: SearchResult) => {
return `title: ${item.title}\nurl: ${item.link}\nsnippet: ${item.snippet}\n`;
}).join('\n\n');
resolve(formattedResults);
} catch (error) {
reject(error);
}
});
}).on('error', (error: Error) => {
reject(error);
});
});
}
async function getWebsiteContent(url: string): Promise<string> {
if (isYoutubeUrl(url)) {
// TODO: pass language based on user preferences
const { title, transcript } =
await YoutubeTranscript.fetchTranscriptAndMetadata(url)
return `Title: ${title}
Video Transcript:
${transcript.map((t) => `${t.offset}: ${t.text}`).join('\n')}`
}
const response = await requestUrl({ url })
return htmlToMarkdown(response.text)
}
const USE_JINA = true
export async function fetchUrlsContent(urls: string[]): Promise<string> {
return new Promise((resolve) => {
const results = urls.map(async (url) => {
try {
const content = USE_JINA ? await fetchJina(url) : await getWebsiteContent(url);
return `<url_content url="${url}">\n${content}\n</url_content>`;
} catch (error) {
console.error(`获取URL内容失败: ${url}`, error);
return `<url_content url="${url}">\n获取内容失败: ${error}\n</url_content>`;
}
});
console.log('fetchUrlsContent', results);
Promise.all(results).then((texts) => {
resolve(texts.join('\n\n'));
}).catch((error) => {
console.error('获取URLs内容时出错', error);
resolve('fetch urls content error'); // 即使出错也返回一些内容
});
});
}
function fetchJina(url: string): Promise<string> {
return new Promise((resolve) => {
const jinaUrl = `https://r.jina.ai/${url}`;
const jinaHeaders = {
'Authorization': 'Bearer jina_1d721eb8c4814a938b4351ae0c3a0f117FlTTAz1GOmpOsIDN7HvIyLbiOCe',
'X-No-Cache': 'true',
};
const jinaOptions: https.RequestOptions = {
method: 'GET',
headers: jinaHeaders,
};
const req = https.request(jinaUrl, jinaOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log(data);
try {
// 检查是否有错误响应
const response = JSON.parse(data);
if (response.code && response.message) {
console.error(`JINA API 错误: ${response.message}`);
resolve(`无法获取内容: ${response.message}`);
return;
}
resolve(data);
} catch (e) {
// 如果不是JSON格式可能是正常的内容
resolve(data);
}
});
});
req.on('error', (e) => {
console.error(`Error: ${e.message}`);
resolve(`fetch jina error: ${e.message}`);
});
req.end();
});
}