diff --git a/package.json b/package.json index e462802..1e64931 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc3c3ac..02268cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/components/chat-view/Chat.tsx b/src/components/chat-view/Chat.tsx index 4369624..b7628b5 100644 --- a/src/components/chat-view/Chat.tsx +++ b/src/components/chat-view/Chat.tsx @@ -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((props, ref) => { chatModel, { model: chatModel.modelId, - temperature: 0.5, + temperature: 0, messages: requestMessages, stream: true, }, @@ -532,6 +533,38 @@ const Chat = forwardRef((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) diff --git a/src/components/chat-view/MarkdownFetchUrlsContentBlock.tsx b/src/components/chat-view/MarkdownFetchUrlsContentBlock.tsx new file mode 100644 index 0000000..b20ca02 --- /dev/null +++ b/src/components/chat-view/MarkdownFetchUrlsContentBlock.tsx @@ -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(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 && ( +
+
+
+ + Fetch URLs Content +
+ +
+
+ + {urlsMarkdownContent} + +
+
+ ) + ) +} diff --git a/src/components/chat-view/MarkdownSearchWebBlock.tsx b/src/components/chat-view/MarkdownSearchWebBlock.tsx new file mode 100644 index 0000000..63bb8f4 --- /dev/null +++ b/src/components/chat-view/MarkdownSearchWebBlock.tsx @@ -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 ( +
+
+
+ + Web search: {query} +
+
+
+ ) +} diff --git a/src/components/chat-view/ReactMarkdown.tsx b/src/components/chat-view/ReactMarkdown.tsx index aa103c9..857fc67 100644 --- a/src/components/chat-view/ReactMarkdown.tsx +++ b/src/components/chat-view/ReactMarkdown.tsx @@ -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={ ` ${block.question && block.question.trimStart()}`} /> + ) : block.type === 'search_web' ? ( + + ) : block.type === 'fetch_urls_content' ? ( + ) : ( {block.content} diff --git a/src/types/apply.ts b/src/types/apply.ts index dd1bef7..cd54429 100644 --- a/src/types/apply.ts +++ b/src/types/apply.ts @@ -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; diff --git a/src/utils/modes.ts b/src/utils/modes.ts index 20cc6af..bd01adc 100644 --- a/src/utils/modes.ts +++ b/src/utils/modes.ts @@ -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", diff --git a/src/utils/parse-infio-block.ts b/src/utils/parse-infio-block.ts index 0fb062c..a24e79f 100644 --- a/src/utils/parse-infio-block.ts +++ b/src/utils/parse-infio-block.ts @@ -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 diff --git a/src/utils/prompt-generator.ts b/src/utils/prompt-generator.ts index ea7e7e3..4c02fcd 100644 --- a/src/utils/prompt-generator.ts +++ b/src/utils/prompt-generator.ts @@ -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' diff --git a/src/utils/tool-groups.ts b/src/utils/tool-groups.ts index d8a4d6c..bf84420 100644 --- a/src/utils/tool-groups.ts +++ b/src/utils/tool-groups.ts @@ -30,6 +30,9 @@ export const TOOL_GROUPS: Record = { 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 = { read: "Read Files", edit: "Edit Files", + research: "Research", browser: "Use Browser", command: "Run Commands", mcp: "Use MCP", diff --git a/src/utils/web-search.ts b/src/utils/web-search.ts new file mode 100644 index 0000000..4a31700 --- /dev/null +++ b/src/utils/web-search.ts @@ -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 { + 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 { + 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 { + return new Promise((resolve) => { + const results = urls.map(async (url) => { + try { + const content = USE_JINA ? await fetchJina(url) : await getWebsiteContent(url); + return `\n${content}\n`; + } catch (error) { + console.error(`获取URL内容失败: ${url}`, error); + return `\n获取内容失败: ${error}\n`; + } + }); + + 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 { + 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(); + }); +} \ No newline at end of file