@effect-x/ultimate-search 0.1.2 → 0.1.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.
- package/dist/cli.js +67199 -584
- package/dist/cli.js.map +1 -1
- package/package.json +3 -3
- package/src/cli.ts +21 -0
- package/src/commands/fetch.ts +95 -0
- package/src/commands/map.ts +97 -0
- package/src/commands/mcp/stdio.ts +27 -0
- package/src/commands/mcp.ts +7 -0
- package/src/commands/root.ts +10 -0
- package/src/commands/search/dual.ts +88 -0
- package/src/commands/search/grok.ts +70 -0
- package/src/commands/search/tavily.ts +99 -0
- package/src/commands/search.ts +9 -0
- package/src/config/settings.ts +261 -0
- package/src/providers/firecrawl/client.ts +75 -0
- package/src/providers/firecrawl/schema.ts +31 -0
- package/src/providers/grok/client.ts +77 -0
- package/src/providers/grok/schema.ts +107 -0
- package/src/providers/tavily/client.ts +143 -0
- package/src/providers/tavily/schema.ts +207 -0
- package/src/services/dual-search.ts +150 -0
- package/src/services/firecrawl-fetch.ts +101 -0
- package/src/services/grok-search.ts +68 -0
- package/src/services/read-only-mcp.ts +275 -0
- package/src/services/tavily-extract.ts +66 -0
- package/src/services/tavily-map.ts +38 -0
- package/src/services/tavily-search.ts +40 -0
- package/src/services/web-fetch-schema.ts +72 -0
- package/src/services/web-fetch.ts +100 -0
- package/src/shared/cli-flags.ts +25 -0
- package/src/shared/command-output.ts +21 -0
- package/src/shared/effect.ts +52 -0
- package/src/shared/errors.ts +56 -0
- package/src/shared/output.ts +210 -0
- package/src/shared/provider-http-client.ts +73 -0
- package/src/shared/render-error.ts +101 -0
- package/src/shared/schema.ts +42 -0
- package/src/shared/tracing.ts +53 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
import { absoluteUrlStringSchema, trimmedNonEmptyStringSchema } from "../../shared/schema";
|
|
3
|
+
|
|
4
|
+
export const TavilySearchDepthSchema = Schema.Literals(["basic", "advanced"] as const);
|
|
5
|
+
|
|
6
|
+
export type TavilySearchDepth = typeof TavilySearchDepthSchema.Type;
|
|
7
|
+
|
|
8
|
+
export const TavilyExtractDepthSchema = TavilySearchDepthSchema;
|
|
9
|
+
|
|
10
|
+
export type TavilyExtractDepth = typeof TavilyExtractDepthSchema.Type;
|
|
11
|
+
|
|
12
|
+
export const TavilySearchTopicSchema = Schema.Literals(["general", "news", "finance"] as const);
|
|
13
|
+
|
|
14
|
+
export type TavilySearchTopic = typeof TavilySearchTopicSchema.Type;
|
|
15
|
+
|
|
16
|
+
export const FetchContentFormatSchema = Schema.Literals(["markdown", "text"] as const);
|
|
17
|
+
|
|
18
|
+
export type FetchContentFormat = typeof FetchContentFormatSchema.Type;
|
|
19
|
+
|
|
20
|
+
export const TavilyTimeRangeSchema = Schema.Literals(["day", "week", "month", "year"] as const);
|
|
21
|
+
|
|
22
|
+
export type TavilyTimeRange = typeof TavilyTimeRangeSchema.Type;
|
|
23
|
+
|
|
24
|
+
const maxResultsSchema = Schema.Int.pipe(
|
|
25
|
+
Schema.check(Schema.isBetween({ minimum: 1, maximum: 20 })),
|
|
26
|
+
Schema.annotate({
|
|
27
|
+
message: "max-results must be an integer between 1 and 20",
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const TavilyMapDepthSchema = Schema.Int.pipe(
|
|
32
|
+
Schema.check(Schema.isBetween({ minimum: 1, maximum: 5 })),
|
|
33
|
+
Schema.annotate({
|
|
34
|
+
message: "depth must be an integer between 1 and 5",
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export type TavilyMapDepth = typeof TavilyMapDepthSchema.Type;
|
|
39
|
+
|
|
40
|
+
export const TavilyMapBreadthSchema = Schema.Int.pipe(
|
|
41
|
+
Schema.check(Schema.isBetween({ minimum: 1, maximum: 500 })),
|
|
42
|
+
Schema.annotate({
|
|
43
|
+
message: "breadth must be an integer between 1 and 500",
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export type TavilyMapBreadth = typeof TavilyMapBreadthSchema.Type;
|
|
48
|
+
|
|
49
|
+
export const TavilyMapLimitSchema = Schema.Int.pipe(
|
|
50
|
+
Schema.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
51
|
+
Schema.annotate({
|
|
52
|
+
message: "limit must be an integer greater than or equal to 1",
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export type TavilyMapLimit = typeof TavilyMapLimitSchema.Type;
|
|
57
|
+
|
|
58
|
+
const responseTimeSchema = Schema.Union([Schema.Number, Schema.NumberFromString]);
|
|
59
|
+
|
|
60
|
+
export const TavilySearchRequestSchema = Schema.Struct({
|
|
61
|
+
query: Schema.NonEmptyString,
|
|
62
|
+
search_depth: Schema.optional(TavilySearchDepthSchema),
|
|
63
|
+
topic: Schema.optional(TavilySearchTopicSchema),
|
|
64
|
+
time_range: Schema.optional(TavilyTimeRangeSchema),
|
|
65
|
+
max_results: Schema.optional(maxResultsSchema),
|
|
66
|
+
include_answer: Schema.optional(Schema.Boolean),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export type TavilySearchRequest = typeof TavilySearchRequestSchema.Type;
|
|
70
|
+
|
|
71
|
+
export const TavilySearchResultItemSchema = Schema.Struct({
|
|
72
|
+
title: Schema.String,
|
|
73
|
+
url: Schema.String,
|
|
74
|
+
content: Schema.String,
|
|
75
|
+
score: Schema.Number,
|
|
76
|
+
raw_content: Schema.optional(Schema.NullOr(Schema.String)),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export type TavilySearchResultItem = typeof TavilySearchResultItemSchema.Type;
|
|
80
|
+
|
|
81
|
+
export const TavilySearchResponseSchema = Schema.Struct({
|
|
82
|
+
query: Schema.String,
|
|
83
|
+
answer: Schema.optional(Schema.NullOr(Schema.String)),
|
|
84
|
+
images: Schema.optional(Schema.Array(Schema.String)),
|
|
85
|
+
response_time: Schema.optional(responseTimeSchema),
|
|
86
|
+
results: Schema.Array(TavilySearchResultItemSchema),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type TavilySearchResponse = typeof TavilySearchResponseSchema.Type;
|
|
90
|
+
|
|
91
|
+
export const TavilyExtractRequestSchema = Schema.Struct({
|
|
92
|
+
urls: Schema.NonEmptyArray(Schema.String),
|
|
93
|
+
extract_depth: Schema.optional(TavilyExtractDepthSchema),
|
|
94
|
+
format: Schema.optional(FetchContentFormatSchema),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export type TavilyExtractRequest = typeof TavilyExtractRequestSchema.Type;
|
|
98
|
+
|
|
99
|
+
export const TavilyExtractResultItemSchema = Schema.Struct({
|
|
100
|
+
url: Schema.String,
|
|
101
|
+
title: Schema.optional(Schema.NullOr(Schema.String)),
|
|
102
|
+
raw_content: Schema.optional(Schema.NullOr(Schema.String)),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export type TavilyExtractResultItem = typeof TavilyExtractResultItemSchema.Type;
|
|
106
|
+
|
|
107
|
+
export const TavilyExtractResponseSchema = Schema.Struct({
|
|
108
|
+
results: Schema.Array(TavilyExtractResultItemSchema),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export type TavilyExtractResponse = typeof TavilyExtractResponseSchema.Type;
|
|
112
|
+
|
|
113
|
+
export class TavilySearchInput extends Schema.Class<TavilySearchInput>("TavilySearchInput")({
|
|
114
|
+
query: trimmedNonEmptyStringSchema("query must be a non-empty string"),
|
|
115
|
+
searchDepth: Schema.Option(TavilySearchDepthSchema),
|
|
116
|
+
topic: Schema.Option(TavilySearchTopicSchema),
|
|
117
|
+
timeRange: Schema.Option(TavilyTimeRangeSchema),
|
|
118
|
+
maxResults: Schema.Option(maxResultsSchema),
|
|
119
|
+
includeAnswer: Schema.Boolean,
|
|
120
|
+
}) {
|
|
121
|
+
static decodeEffect = Schema.decodeUnknownEffect(TavilySearchInput);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const buildTavilySearchRequest = (input: TavilySearchInput): TavilySearchRequest => ({
|
|
125
|
+
query: input.query,
|
|
126
|
+
...(Option.isSome(input.searchDepth) && {
|
|
127
|
+
search_depth: input.searchDepth.value,
|
|
128
|
+
}),
|
|
129
|
+
...(Option.isSome(input.topic) && {
|
|
130
|
+
topic: input.topic.value,
|
|
131
|
+
}),
|
|
132
|
+
...(Option.isSome(input.timeRange) && {
|
|
133
|
+
time_range: input.timeRange.value,
|
|
134
|
+
}),
|
|
135
|
+
...(Option.isSome(input.maxResults) && {
|
|
136
|
+
max_results: input.maxResults.value,
|
|
137
|
+
}),
|
|
138
|
+
...(input.includeAnswer ? { include_answer: true } : {}),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export const TavilyMapRequestSchema = Schema.Struct({
|
|
142
|
+
url: Schema.String,
|
|
143
|
+
max_depth: Schema.optional(TavilyMapDepthSchema),
|
|
144
|
+
max_breadth: Schema.optional(TavilyMapBreadthSchema),
|
|
145
|
+
limit: Schema.optional(TavilyMapLimitSchema),
|
|
146
|
+
instructions: Schema.optional(Schema.NonEmptyString),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
export type TavilyMapRequest = typeof TavilyMapRequestSchema.Type;
|
|
150
|
+
|
|
151
|
+
export const TavilyMapUsageSchema = Schema.Struct({
|
|
152
|
+
credits_used: Schema.optional(Schema.Number),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export type TavilyMapUsage = typeof TavilyMapUsageSchema.Type;
|
|
156
|
+
|
|
157
|
+
export const TavilyMapResponseSchema = Schema.Struct({
|
|
158
|
+
base_url: Schema.String,
|
|
159
|
+
results: Schema.Array(Schema.String),
|
|
160
|
+
response_time: Schema.optional(responseTimeSchema),
|
|
161
|
+
request_id: Schema.optional(Schema.String),
|
|
162
|
+
usage: Schema.optional(TavilyMapUsageSchema),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export type TavilyMapResponse = typeof TavilyMapResponseSchema.Type;
|
|
166
|
+
|
|
167
|
+
export class TavilyMapInput extends Schema.Class<TavilyMapInput>("TavilyMapInput")({
|
|
168
|
+
url: absoluteUrlStringSchema("url must be an absolute URL"),
|
|
169
|
+
depth: Schema.Option(TavilyMapDepthSchema),
|
|
170
|
+
breadth: Schema.Option(TavilyMapBreadthSchema),
|
|
171
|
+
limit: Schema.Option(TavilyMapLimitSchema),
|
|
172
|
+
instructions: Schema.Option(
|
|
173
|
+
trimmedNonEmptyStringSchema("instructions must be a non-empty string"),
|
|
174
|
+
),
|
|
175
|
+
}) {
|
|
176
|
+
static decodeEffect = Schema.decodeUnknownEffect(TavilyMapInput);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const buildTavilyMapRequest = (input: TavilyMapInput): TavilyMapRequest => ({
|
|
180
|
+
url: input.url,
|
|
181
|
+
...(Option.isSome(input.depth) && {
|
|
182
|
+
max_depth: input.depth.value,
|
|
183
|
+
}),
|
|
184
|
+
...(Option.isSome(input.breadth) && {
|
|
185
|
+
max_breadth: input.breadth.value,
|
|
186
|
+
}),
|
|
187
|
+
...(Option.isSome(input.limit) && {
|
|
188
|
+
limit: input.limit.value,
|
|
189
|
+
}),
|
|
190
|
+
...(Option.isSome(input.instructions) && {
|
|
191
|
+
instructions: input.instructions.value,
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export const buildTavilyExtractRequest = (input: {
|
|
196
|
+
readonly urls: readonly [string, ...Array<string>];
|
|
197
|
+
readonly depth: TavilyExtractDepth;
|
|
198
|
+
readonly format: FetchContentFormat;
|
|
199
|
+
}): TavilyExtractRequest => {
|
|
200
|
+
const [firstUrl, ...remainingUrls] = input.urls;
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
urls: [firstUrl, ...remainingUrls],
|
|
204
|
+
extract_depth: input.depth,
|
|
205
|
+
format: input.format,
|
|
206
|
+
};
|
|
207
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Effect, Layer, Result, Schema, ServiceMap } from "effect";
|
|
2
|
+
import {
|
|
3
|
+
GrokSearchInput,
|
|
4
|
+
GrokSearchResultSchema,
|
|
5
|
+
type GrokSearchResult,
|
|
6
|
+
} from "../providers/grok/schema";
|
|
7
|
+
import {
|
|
8
|
+
TavilySearchDepthSchema,
|
|
9
|
+
TavilySearchInput,
|
|
10
|
+
TavilySearchResponseSchema,
|
|
11
|
+
type TavilySearchResponse,
|
|
12
|
+
TavilySearchTopicSchema,
|
|
13
|
+
TavilyTimeRangeSchema,
|
|
14
|
+
} from "../providers/tavily/schema";
|
|
15
|
+
import type { ServicesReturns } from "../shared/effect";
|
|
16
|
+
import type { RenderedError } from "../shared/render-error";
|
|
17
|
+
import { RenderedErrorSchema, renderStructuredError } from "../shared/render-error";
|
|
18
|
+
import { trimmedNonEmptyStringSchema } from "../shared/schema";
|
|
19
|
+
import { GrokSearch } from "./grok-search";
|
|
20
|
+
import { TavilySearch } from "./tavily-search";
|
|
21
|
+
|
|
22
|
+
export class DualSearchInput extends Schema.Class<DualSearchInput>("DualSearchInput")({
|
|
23
|
+
query: trimmedNonEmptyStringSchema("query must be a non-empty string"),
|
|
24
|
+
platform: Schema.Option(Schema.NonEmptyString),
|
|
25
|
+
model: Schema.Option(Schema.NonEmptyString),
|
|
26
|
+
searchDepth: Schema.Option(TavilySearchDepthSchema),
|
|
27
|
+
topic: Schema.Option(TavilySearchTopicSchema),
|
|
28
|
+
timeRange: Schema.Option(TavilyTimeRangeSchema),
|
|
29
|
+
maxResults: Schema.Option(
|
|
30
|
+
Schema.Int.pipe(
|
|
31
|
+
Schema.check(Schema.isBetween({ minimum: 1, maximum: 20 })),
|
|
32
|
+
Schema.annotate({
|
|
33
|
+
message: "max-results must be an integer between 1 and 20",
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
includeAnswer: Schema.Boolean,
|
|
38
|
+
}) {
|
|
39
|
+
static decodeEffect = Schema.decodeUnknownEffect(DualSearchInput);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DualSearchProviderSuccess<A> {
|
|
43
|
+
readonly status: "success";
|
|
44
|
+
readonly result: A;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DualSearchProviderFailure {
|
|
48
|
+
readonly status: "error";
|
|
49
|
+
readonly error: RenderedError;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type DualSearchProviderResult<A> = DualSearchProviderSuccess<A> | DualSearchProviderFailure;
|
|
53
|
+
|
|
54
|
+
export interface DualSearchResult {
|
|
55
|
+
readonly grok: DualSearchProviderResult<GrokSearchResult>;
|
|
56
|
+
readonly tavily: DualSearchProviderResult<TavilySearchResponse>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const dualSearchProviderSuccessSchema = <A extends Schema.Top>(resultSchema: A) =>
|
|
60
|
+
Schema.Struct({
|
|
61
|
+
status: Schema.Literal("success"),
|
|
62
|
+
result: resultSchema,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const DualSearchProviderFailureSchema = Schema.Struct({
|
|
66
|
+
status: Schema.Literal("error"),
|
|
67
|
+
error: RenderedErrorSchema,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const DualSearchResultSchema = Schema.Struct({
|
|
71
|
+
grok: Schema.Union([
|
|
72
|
+
dualSearchProviderSuccessSchema(GrokSearchResultSchema),
|
|
73
|
+
DualSearchProviderFailureSchema,
|
|
74
|
+
]),
|
|
75
|
+
tavily: Schema.Union([
|
|
76
|
+
dualSearchProviderSuccessSchema(TavilySearchResponseSchema),
|
|
77
|
+
DualSearchProviderFailureSchema,
|
|
78
|
+
]),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const toProviderResult = <A, E>(result: Result.Result<A, E>): DualSearchProviderResult<A> =>
|
|
82
|
+
Result.match(result, {
|
|
83
|
+
onSuccess: (value) => ({
|
|
84
|
+
status: "success",
|
|
85
|
+
result: value,
|
|
86
|
+
}),
|
|
87
|
+
onFailure: (error) => ({
|
|
88
|
+
status: "error",
|
|
89
|
+
error: renderStructuredError(error),
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export class DualSearch extends ServiceMap.Service<
|
|
94
|
+
DualSearch,
|
|
95
|
+
{
|
|
96
|
+
readonly search: (input: DualSearchInput) => Effect.Effect<DualSearchResult>;
|
|
97
|
+
}
|
|
98
|
+
>()("DualSearch") {
|
|
99
|
+
static readonly layer = Layer.effect(
|
|
100
|
+
DualSearch,
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
const grokSearch = yield* GrokSearch;
|
|
103
|
+
const tavilySearch = yield* TavilySearch;
|
|
104
|
+
|
|
105
|
+
const search: DualSearch.Methods["search"] = Effect.fn("DualSearch.search")(
|
|
106
|
+
function* (input): DualSearch.Returns<"search"> {
|
|
107
|
+
const results = yield* Effect.all(
|
|
108
|
+
{
|
|
109
|
+
grok: grokSearch.search(
|
|
110
|
+
yield* GrokSearchInput.decodeEffect({
|
|
111
|
+
query: input.query,
|
|
112
|
+
platform: input.platform,
|
|
113
|
+
model: input.model,
|
|
114
|
+
}).pipe(Effect.orDie),
|
|
115
|
+
),
|
|
116
|
+
tavily: tavilySearch.search(
|
|
117
|
+
yield* TavilySearchInput.decodeEffect({
|
|
118
|
+
query: input.query,
|
|
119
|
+
searchDepth: input.searchDepth,
|
|
120
|
+
topic: input.topic,
|
|
121
|
+
timeRange: input.timeRange,
|
|
122
|
+
maxResults: input.maxResults,
|
|
123
|
+
includeAnswer: input.includeAnswer,
|
|
124
|
+
}).pipe(Effect.orDie),
|
|
125
|
+
),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
concurrency: "unbounded",
|
|
129
|
+
mode: "result",
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
grok: toProviderResult(results.grok),
|
|
135
|
+
tavily: toProviderResult(results.tavily),
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return DualSearch.of({
|
|
141
|
+
search,
|
|
142
|
+
});
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export declare namespace DualSearch {
|
|
148
|
+
export type Methods = ServiceMap.Service.Shape<typeof DualSearch>;
|
|
149
|
+
export type Returns<key extends keyof Methods, R = never> = ServicesReturns<Methods[key], R>;
|
|
150
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Effect, Layer, Result, ServiceMap } from "effect";
|
|
2
|
+
import { FirecrawlProviderClient } from "../providers/firecrawl/client";
|
|
3
|
+
import type { FirecrawlScrapeResponse } from "../providers/firecrawl/schema";
|
|
4
|
+
import { ProviderContentError, type UltimateSearchError } from "../shared/errors";
|
|
5
|
+
import type { ServicesReturns } from "../shared/effect";
|
|
6
|
+
import type { FetchedPage, WebFetchInput } from "./web-fetch-schema";
|
|
7
|
+
|
|
8
|
+
const normalizeContent = (response: FirecrawlScrapeResponse, format: WebFetchInput["format"]) => {
|
|
9
|
+
const data = response.data;
|
|
10
|
+
|
|
11
|
+
if (data == null) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const preferred = format === "text" ? data.content : data.markdown;
|
|
16
|
+
const fallback = format === "text" ? data.markdown : data.content;
|
|
17
|
+
const content = (preferred ?? fallback ?? "").trim();
|
|
18
|
+
|
|
19
|
+
return content.length > 0 ? content : null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class FirecrawlFetch extends ServiceMap.Service<
|
|
23
|
+
FirecrawlFetch,
|
|
24
|
+
{
|
|
25
|
+
readonly fetch: (
|
|
26
|
+
input: WebFetchInput,
|
|
27
|
+
) => Effect.Effect<ReadonlyArray<FetchedPage>, UltimateSearchError, never>;
|
|
28
|
+
}
|
|
29
|
+
>()("FirecrawlFetch") {
|
|
30
|
+
static readonly layer = Layer.effect(
|
|
31
|
+
FirecrawlFetch,
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
const provider = yield* FirecrawlProviderClient;
|
|
34
|
+
|
|
35
|
+
const fetch: FirecrawlFetch.Methods["fetch"] = Effect.fn("FirecrawlFetch.fetch")(
|
|
36
|
+
function* (input): FirecrawlFetch.Returns<"fetch"> {
|
|
37
|
+
const attempts = yield* Effect.forEach(
|
|
38
|
+
input.urls,
|
|
39
|
+
(url) =>
|
|
40
|
+
Effect.result(
|
|
41
|
+
provider.scrape({
|
|
42
|
+
url,
|
|
43
|
+
formats: ["markdown"],
|
|
44
|
+
}),
|
|
45
|
+
).pipe(
|
|
46
|
+
Effect.map((result) => ({
|
|
47
|
+
url,
|
|
48
|
+
result,
|
|
49
|
+
})),
|
|
50
|
+
),
|
|
51
|
+
{ concurrency: "unbounded" },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const successes = attempts.flatMap(({ url, result }) => {
|
|
55
|
+
if (Result.isFailure(result)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rawContent = normalizeContent(result.success, input.format);
|
|
60
|
+
|
|
61
|
+
if (rawContent === null) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
url,
|
|
68
|
+
title: result.success.data?.metadata?.title,
|
|
69
|
+
raw_content: rawContent,
|
|
70
|
+
} satisfies FetchedPage,
|
|
71
|
+
];
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (successes.length > 0) {
|
|
75
|
+
return successes;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const firstFailure = attempts.find(({ result }) => Result.isFailure(result));
|
|
79
|
+
|
|
80
|
+
if (firstFailure != null && Result.isFailure(firstFailure.result)) {
|
|
81
|
+
return yield* firstFailure.result.failure;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return yield* new ProviderContentError({
|
|
85
|
+
provider: "firecrawl",
|
|
86
|
+
message: "FireCrawl returned no extractable content.",
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return FirecrawlFetch.of({
|
|
92
|
+
fetch,
|
|
93
|
+
});
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export declare namespace FirecrawlFetch {
|
|
99
|
+
export type Methods = ServiceMap.Service.Shape<typeof FirecrawlFetch>;
|
|
100
|
+
export type Returns<key extends keyof Methods, R = never> = ServicesReturns<Methods[key], R>;
|
|
101
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Effect, Layer, Option, ServiceMap } from "effect";
|
|
2
|
+
import { UltimateSearchConfig } from "../config/settings";
|
|
3
|
+
import { GrokProviderClient } from "../providers/grok/client";
|
|
4
|
+
import {
|
|
5
|
+
buildGrokUserMessage,
|
|
6
|
+
type GrokChatCompletionRequest,
|
|
7
|
+
GrokSearchInput,
|
|
8
|
+
type GrokSearchResult,
|
|
9
|
+
grokSystemPrompt,
|
|
10
|
+
} from "../providers/grok/schema";
|
|
11
|
+
import type { UltimateSearchError } from "../shared/errors";
|
|
12
|
+
import type { ServicesReturns } from "../shared/effect";
|
|
13
|
+
|
|
14
|
+
export class GrokSearch extends ServiceMap.Service<
|
|
15
|
+
GrokSearch,
|
|
16
|
+
{
|
|
17
|
+
readonly search: (
|
|
18
|
+
input: GrokSearchInput,
|
|
19
|
+
) => Effect.Effect<GrokSearchResult, UltimateSearchError, never>;
|
|
20
|
+
}
|
|
21
|
+
>()("GrokSearch") {
|
|
22
|
+
// 也可以单独定义成变量或者class上的静态方法
|
|
23
|
+
static readonly layer = Layer.effect(
|
|
24
|
+
GrokSearch,
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const config = yield* UltimateSearchConfig;
|
|
27
|
+
const provider = yield* GrokProviderClient;
|
|
28
|
+
|
|
29
|
+
const search: GrokSearch.Methods["search"] = Effect.fn("search")(
|
|
30
|
+
function* (input): GrokSearch.Returns<"search"> {
|
|
31
|
+
const grok = yield* config.getGrokConfig();
|
|
32
|
+
const payload: GrokChatCompletionRequest = {
|
|
33
|
+
model: Option.getOrElse(input.model, () => grok.model),
|
|
34
|
+
stream: false,
|
|
35
|
+
messages: [
|
|
36
|
+
{
|
|
37
|
+
role: "system",
|
|
38
|
+
content: grokSystemPrompt,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
role: "user",
|
|
42
|
+
content: buildGrokUserMessage(input),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
const response = yield* provider.createChatCompletion(payload);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
content: response.choices[0].message.content,
|
|
50
|
+
model: response.model,
|
|
51
|
+
usage: response.usage,
|
|
52
|
+
} satisfies GrokSearchResult;
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return GrokSearch.of({
|
|
57
|
+
search,
|
|
58
|
+
});
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// 也可以再定义 live = Layer.provide(xxx.layer, deps) // 无依赖的 Layer<A, E, never>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export declare namespace GrokSearch {
|
|
66
|
+
export type Methods = ServiceMap.Service.Shape<typeof GrokSearch>;
|
|
67
|
+
export type Returns<key extends keyof Methods, R = never> = ServicesReturns<Methods[key], R>;
|
|
68
|
+
}
|