@llm4ts/core 0.1.0
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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/AgentSession.d.ts +66 -0
- package/dist/AgentSession.d.ts.map +1 -0
- package/dist/AgentSession.js +53 -0
- package/dist/AgentSession.js.map +1 -0
- package/dist/Capability.d.ts +101 -0
- package/dist/Capability.d.ts.map +1 -0
- package/dist/Capability.js +210 -0
- package/dist/Capability.js.map +1 -0
- package/dist/Connector.d.ts +69 -0
- package/dist/Connector.d.ts.map +1 -0
- package/dist/Connector.js +86 -0
- package/dist/Connector.js.map +1 -0
- package/dist/ConnectorConfig.d.ts +70 -0
- package/dist/ConnectorConfig.d.ts.map +1 -0
- package/dist/ConnectorConfig.js +110 -0
- package/dist/ConnectorConfig.js.map +1 -0
- package/dist/ConnectorRegistry.d.ts +28 -0
- package/dist/ConnectorRegistry.d.ts.map +1 -0
- package/dist/ConnectorRegistry.js +87 -0
- package/dist/ConnectorRegistry.js.map +1 -0
- package/dist/ContextManagement.d.ts +78 -0
- package/dist/ContextManagement.d.ts.map +1 -0
- package/dist/ContextManagement.js +221 -0
- package/dist/ContextManagement.js.map +1 -0
- package/dist/Conversation.d.ts +18 -0
- package/dist/Conversation.d.ts.map +1 -0
- package/dist/Conversation.js +17 -0
- package/dist/Conversation.js.map +1 -0
- package/dist/Errors.d.ts +64 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +76 -0
- package/dist/Errors.js.map +1 -0
- package/dist/HttpClient.d.ts +43 -0
- package/dist/HttpClient.d.ts.map +1 -0
- package/dist/HttpClient.js +99 -0
- package/dist/HttpClient.js.map +1 -0
- package/dist/LlmService.d.ts +24 -0
- package/dist/LlmService.d.ts.map +1 -0
- package/dist/LlmService.js +4 -0
- package/dist/LlmService.js.map +1 -0
- package/dist/Models.d.ts +138 -0
- package/dist/Models.d.ts.map +1 -0
- package/dist/Models.js +209 -0
- package/dist/Models.js.map +1 -0
- package/dist/ProcessExecutor.d.ts +57 -0
- package/dist/ProcessExecutor.d.ts.map +1 -0
- package/dist/ProcessExecutor.js +71 -0
- package/dist/ProcessExecutor.js.map +1 -0
- package/dist/RateLimiter.d.ts +44 -0
- package/dist/RateLimiter.d.ts.map +1 -0
- package/dist/RateLimiter.js +155 -0
- package/dist/RateLimiter.js.map +1 -0
- package/dist/Streaming.d.ts +17 -0
- package/dist/Streaming.d.ts.map +1 -0
- package/dist/Streaming.js +65 -0
- package/dist/Streaming.js.map +1 -0
- package/dist/StructuredOutput.d.ts +10 -0
- package/dist/StructuredOutput.d.ts.map +1 -0
- package/dist/StructuredOutput.js +97 -0
- package/dist/StructuredOutput.js.map +1 -0
- package/dist/TemporaryFiles.d.ts +20 -0
- package/dist/TemporaryFiles.d.ts.map +1 -0
- package/dist/TemporaryFiles.js +18 -0
- package/dist/TemporaryFiles.js.map +1 -0
- package/dist/UsageLimits.d.ts +5 -0
- package/dist/UsageLimits.d.ts.map +1 -0
- package/dist/UsageLimits.js +187 -0
- package/dist/UsageLimits.js.map +1 -0
- package/dist/eval/Checks.d.ts +6 -0
- package/dist/eval/Checks.d.ts.map +1 -0
- package/dist/eval/Checks.js +39 -0
- package/dist/eval/Checks.js.map +1 -0
- package/dist/eval/Eval.d.ts +57 -0
- package/dist/eval/Eval.d.ts.map +1 -0
- package/dist/eval/Eval.js +91 -0
- package/dist/eval/Eval.js.map +1 -0
- package/dist/eval/EvalSuite.d.ts +21 -0
- package/dist/eval/EvalSuite.d.ts.map +1 -0
- package/dist/eval/EvalSuite.js +30 -0
- package/dist/eval/EvalSuite.js.map +1 -0
- package/dist/eval/Evaluator.d.ts +10 -0
- package/dist/eval/Evaluator.d.ts.map +1 -0
- package/dist/eval/Evaluator.js +14 -0
- package/dist/eval/Evaluator.js.map +1 -0
- package/dist/eval/Judge.d.ts +15 -0
- package/dist/eval/Judge.d.ts.map +1 -0
- package/dist/eval/Judge.js +73 -0
- package/dist/eval/Judge.js.map +1 -0
- package/dist/observability/MeteredLlmService.d.ts +16 -0
- package/dist/observability/MeteredLlmService.d.ts.map +1 -0
- package/dist/observability/MeteredLlmService.js +66 -0
- package/dist/observability/MeteredLlmService.js.map +1 -0
- package/dist/observability/Metrics.d.ts +71 -0
- package/dist/observability/Metrics.d.ts.map +1 -0
- package/dist/observability/Metrics.js +147 -0
- package/dist/observability/Metrics.js.map +1 -0
- package/dist/observability/Redaction.d.ts +8 -0
- package/dist/observability/Redaction.d.ts.map +1 -0
- package/dist/observability/Redaction.js +38 -0
- package/dist/observability/Redaction.js.map +1 -0
- package/dist/observability/StreamRecorder.d.ts +9 -0
- package/dist/observability/StreamRecorder.d.ts.map +1 -0
- package/dist/observability/StreamRecorder.js +10 -0
- package/dist/observability/StreamRecorder.js.map +1 -0
- package/dist/observability/StructuredLogger.d.ts +31 -0
- package/dist/observability/StructuredLogger.d.ts.map +1 -0
- package/dist/observability/StructuredLogger.js +34 -0
- package/dist/observability/StructuredLogger.js.map +1 -0
- package/dist/observability/Tracing.d.ts +38 -0
- package/dist/observability/Tracing.d.ts.map +1 -0
- package/dist/observability/Tracing.js +75 -0
- package/dist/observability/Tracing.js.map +1 -0
- package/dist/providers/AnthropicModels.d.ts +95 -0
- package/dist/providers/AnthropicModels.d.ts.map +1 -0
- package/dist/providers/AnthropicModels.js +83 -0
- package/dist/providers/AnthropicModels.js.map +1 -0
- package/dist/providers/AnthropicProvider.d.ts +13 -0
- package/dist/providers/AnthropicProvider.d.ts.map +1 -0
- package/dist/providers/AnthropicProvider.js +177 -0
- package/dist/providers/AnthropicProvider.js.map +1 -0
- package/dist/providers/AntigravityConnector.d.ts +7 -0
- package/dist/providers/AntigravityConnector.d.ts.map +1 -0
- package/dist/providers/AntigravityConnector.js +52 -0
- package/dist/providers/AntigravityConnector.js.map +1 -0
- package/dist/providers/ClaudeAgentSession.d.ts +11 -0
- package/dist/providers/ClaudeAgentSession.d.ts.map +1 -0
- package/dist/providers/ClaudeAgentSession.js +158 -0
- package/dist/providers/ClaudeAgentSession.js.map +1 -0
- package/dist/providers/ClaudeCliConnector.d.ts +9 -0
- package/dist/providers/ClaudeCliConnector.d.ts.map +1 -0
- package/dist/providers/ClaudeCliConnector.js +123 -0
- package/dist/providers/ClaudeCliConnector.js.map +1 -0
- package/dist/providers/CliSupport.d.ts +21 -0
- package/dist/providers/CliSupport.d.ts.map +1 -0
- package/dist/providers/CliSupport.js +62 -0
- package/dist/providers/CliSupport.js.map +1 -0
- package/dist/providers/CodexConnector.d.ts +12 -0
- package/dist/providers/CodexConnector.d.ts.map +1 -0
- package/dist/providers/CodexConnector.js +175 -0
- package/dist/providers/CodexConnector.js.map +1 -0
- package/dist/providers/ConnectorFactories.d.ts +15 -0
- package/dist/providers/ConnectorFactories.d.ts.map +1 -0
- package/dist/providers/ConnectorFactories.js +74 -0
- package/dist/providers/ConnectorFactories.js.map +1 -0
- package/dist/providers/CopilotConnector.d.ts +6 -0
- package/dist/providers/CopilotConnector.d.ts.map +1 -0
- package/dist/providers/CopilotConnector.js +35 -0
- package/dist/providers/CopilotConnector.js.map +1 -0
- package/dist/providers/CursorConnector.d.ts +8 -0
- package/dist/providers/CursorConnector.d.ts.map +1 -0
- package/dist/providers/CursorConnector.js +82 -0
- package/dist/providers/CursorConnector.js.map +1 -0
- package/dist/providers/GeminiApiProvider.d.ts +9 -0
- package/dist/providers/GeminiApiProvider.d.ts.map +1 -0
- package/dist/providers/GeminiApiProvider.js +181 -0
- package/dist/providers/GeminiApiProvider.js.map +1 -0
- package/dist/providers/GeminiCliProvider.d.ts +106 -0
- package/dist/providers/GeminiCliProvider.d.ts.map +1 -0
- package/dist/providers/GeminiCliProvider.js +596 -0
- package/dist/providers/GeminiCliProvider.js.map +1 -0
- package/dist/providers/GeminiModels.d.ts +92 -0
- package/dist/providers/GeminiModels.d.ts.map +1 -0
- package/dist/providers/GeminiModels.js +80 -0
- package/dist/providers/GeminiModels.js.map +1 -0
- package/dist/providers/GrokCliConnector.d.ts +8 -0
- package/dist/providers/GrokCliConnector.d.ts.map +1 -0
- package/dist/providers/GrokCliConnector.js +82 -0
- package/dist/providers/GrokCliConnector.js.map +1 -0
- package/dist/providers/LmStudioModels.d.ts +114 -0
- package/dist/providers/LmStudioModels.d.ts.map +1 -0
- package/dist/providers/LmStudioModels.js +102 -0
- package/dist/providers/LmStudioModels.js.map +1 -0
- package/dist/providers/LmStudioProvider.d.ts +13 -0
- package/dist/providers/LmStudioProvider.d.ts.map +1 -0
- package/dist/providers/LmStudioProvider.js +158 -0
- package/dist/providers/LmStudioProvider.js.map +1 -0
- package/dist/providers/MockProvider.d.ts +6 -0
- package/dist/providers/MockProvider.d.ts.map +1 -0
- package/dist/providers/MockProvider.js +271 -0
- package/dist/providers/MockProvider.js.map +1 -0
- package/dist/providers/OllamaModels.d.ts +68 -0
- package/dist/providers/OllamaModels.d.ts.map +1 -0
- package/dist/providers/OllamaModels.js +59 -0
- package/dist/providers/OllamaModels.js.map +1 -0
- package/dist/providers/OllamaProvider.d.ts +7 -0
- package/dist/providers/OllamaProvider.d.ts.map +1 -0
- package/dist/providers/OllamaProvider.js +134 -0
- package/dist/providers/OllamaProvider.js.map +1 -0
- package/dist/providers/OpenAIModels.d.ts +139 -0
- package/dist/providers/OpenAIModels.d.ts.map +1 -0
- package/dist/providers/OpenAIModels.js +123 -0
- package/dist/providers/OpenAIModels.js.map +1 -0
- package/dist/providers/OpenAIProvider.d.ts +9 -0
- package/dist/providers/OpenAIProvider.d.ts.map +1 -0
- package/dist/providers/OpenAIProvider.js +185 -0
- package/dist/providers/OpenAIProvider.js.map +1 -0
- package/dist/providers/OpenCodeCliConnector.d.ts +8 -0
- package/dist/providers/OpenCodeCliConnector.d.ts.map +1 -0
- package/dist/providers/OpenCodeCliConnector.js +93 -0
- package/dist/providers/OpenCodeCliConnector.js.map +1 -0
- package/dist/providers/OpenCodeModels.d.ts +61 -0
- package/dist/providers/OpenCodeModels.d.ts.map +1 -0
- package/dist/providers/OpenCodeModels.js +53 -0
- package/dist/providers/OpenCodeModels.js.map +1 -0
- package/dist/providers/OpenCodeProvider.d.ts +10 -0
- package/dist/providers/OpenCodeProvider.d.ts.map +1 -0
- package/dist/providers/OpenCodeProvider.js +154 -0
- package/dist/providers/OpenCodeProvider.js.map +1 -0
- package/dist/providers/PiConnector.d.ts +8 -0
- package/dist/providers/PiConnector.d.ts.map +1 -0
- package/dist/providers/PiConnector.js +99 -0
- package/dist/providers/PiConnector.js.map +1 -0
- package/dist/tools/Tool.d.ts +83 -0
- package/dist/tools/Tool.d.ts.map +1 -0
- package/dist/tools/Tool.js +278 -0
- package/dist/tools/Tool.js.map +1 -0
- package/dist/tools/ToolCallingExecutor.d.ts +18 -0
- package/dist/tools/ToolCallingExecutor.d.ts.map +1 -0
- package/dist/tools/ToolCallingExecutor.js +64 -0
- package/dist/tools/ToolCallingExecutor.js.map +1 -0
- package/dist/tools/ToolRegistry.d.ts +45 -0
- package/dist/tools/ToolRegistry.d.ts.map +1 -0
- package/dist/tools/ToolRegistry.js +133 -0
- package/dist/tools/ToolRegistry.js.map +1 -0
- package/package.json +105 -0
- package/src/.gitkeep +1 -0
- package/src/AgentSession.ts +69 -0
- package/src/Capability.ts +240 -0
- package/src/Connector.ts +220 -0
- package/src/ConnectorConfig.ts +146 -0
- package/src/ConnectorRegistry.ts +184 -0
- package/src/ContextManagement.ts +372 -0
- package/src/Conversation.ts +21 -0
- package/src/Errors.ts +87 -0
- package/src/HttpClient.ts +202 -0
- package/src/LlmService.ts +45 -0
- package/src/Models.ts +250 -0
- package/src/ProcessExecutor.ts +157 -0
- package/src/RateLimiter.ts +238 -0
- package/src/Streaming.ts +164 -0
- package/src/StructuredOutput.ts +138 -0
- package/src/TemporaryFiles.ts +51 -0
- package/src/UsageLimits.ts +242 -0
- package/src/eval/Checks.ts +68 -0
- package/src/eval/Eval.ts +112 -0
- package/src/eval/EvalSuite.ts +63 -0
- package/src/eval/Evaluator.ts +31 -0
- package/src/eval/Judge.ts +92 -0
- package/src/observability/MeteredLlmService.ts +156 -0
- package/src/observability/Metrics.ts +195 -0
- package/src/observability/Redaction.ts +55 -0
- package/src/observability/StreamRecorder.ts +32 -0
- package/src/observability/StructuredLogger.ts +62 -0
- package/src/observability/Tracing.ts +141 -0
- package/src/providers/AnthropicModels.ts +96 -0
- package/src/providers/AnthropicProvider.ts +320 -0
- package/src/providers/AntigravityConnector.ts +77 -0
- package/src/providers/ClaudeAgentSession.ts +226 -0
- package/src/providers/ClaudeCliConnector.ts +201 -0
- package/src/providers/CliSupport.ts +100 -0
- package/src/providers/CodexConnector.ts +251 -0
- package/src/providers/ConnectorFactories.ts +142 -0
- package/src/providers/CopilotConnector.ts +47 -0
- package/src/providers/CursorConnector.ts +120 -0
- package/src/providers/GeminiApiProvider.ts +337 -0
- package/src/providers/GeminiCliProvider.ts +793 -0
- package/src/providers/GeminiModels.ts +97 -0
- package/src/providers/GrokCliConnector.ts +117 -0
- package/src/providers/LmStudioModels.ts +117 -0
- package/src/providers/LmStudioProvider.ts +260 -0
- package/src/providers/MockProvider.ts +322 -0
- package/src/providers/OllamaModels.ts +64 -0
- package/src/providers/OllamaProvider.ts +236 -0
- package/src/providers/OpenAIModels.ts +152 -0
- package/src/providers/OpenAIProvider.ts +331 -0
- package/src/providers/OpenCodeCliConnector.ts +135 -0
- package/src/providers/OpenCodeModels.ts +60 -0
- package/src/providers/OpenCodeProvider.ts +278 -0
- package/src/providers/PiConnector.ts +142 -0
- package/src/tools/Tool.ts +379 -0
- package/src/tools/ToolCallingExecutor.ts +106 -0
- package/src/tools/ToolRegistry.ts +224 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as Context from "effect/Context"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Ref from "effect/Ref"
|
|
4
|
+
import type * as Scope from "effect/Scope"
|
|
5
|
+
import type { LlmError } from "./Errors.ts"
|
|
6
|
+
|
|
7
|
+
export interface TemporaryFilesShape {
|
|
8
|
+
readonly write: (
|
|
9
|
+
prefix: string,
|
|
10
|
+
suffix: string,
|
|
11
|
+
contents: string
|
|
12
|
+
) => Effect.Effect<string, LlmError, Scope.Scope>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class TemporaryFiles extends Context.Service<TemporaryFiles, TemporaryFilesShape>()(
|
|
16
|
+
"@llm4ts/core/TemporaryFiles"
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
export interface FakeTemporaryFiles {
|
|
20
|
+
readonly files: Effect.Effect<
|
|
21
|
+
ReadonlyArray<{
|
|
22
|
+
readonly path: string
|
|
23
|
+
readonly contents: string
|
|
24
|
+
}>
|
|
25
|
+
>
|
|
26
|
+
readonly temporaryFiles: TemporaryFilesShape
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const makeFakeTemporaryFiles = (
|
|
30
|
+
path = "/tmp/llm4ts-schema.json"
|
|
31
|
+
): Effect.Effect<FakeTemporaryFiles> =>
|
|
32
|
+
Ref.make<
|
|
33
|
+
ReadonlyArray<{
|
|
34
|
+
readonly path: string
|
|
35
|
+
readonly contents: string
|
|
36
|
+
}>
|
|
37
|
+
>([]).pipe(
|
|
38
|
+
Effect.map((files) => ({
|
|
39
|
+
files: Ref.get(files),
|
|
40
|
+
temporaryFiles: {
|
|
41
|
+
write: (_prefix, _suffix, contents) =>
|
|
42
|
+
Ref.update(files, (current) => [
|
|
43
|
+
...current,
|
|
44
|
+
{
|
|
45
|
+
path,
|
|
46
|
+
contents
|
|
47
|
+
}
|
|
48
|
+
]).pipe(Effect.as(path))
|
|
49
|
+
}
|
|
50
|
+
}))
|
|
51
|
+
)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import * as DateTime from "effect/DateTime"
|
|
2
|
+
import * as Duration from "effect/Duration"
|
|
3
|
+
import { RateLimitError, UsageLimitError, type LlmError } from "./Errors.ts"
|
|
4
|
+
|
|
5
|
+
const codexAt = /try again at\s+(\d{1,2}:\d{2}\s*[AP]M)/i
|
|
6
|
+
const claudeAt = /usage limit.*?(?:resets?|try again) at\s+(\d{1,2}(?::\d{2})?\s*[ap]m)/i
|
|
7
|
+
const geminiReset = /reset after\s+((?:\d+(?:ms|[dhms]))+)/i
|
|
8
|
+
const resetComponent = /(\d+)(ms|[dhms])/gi
|
|
9
|
+
const shortResetLimit = Duration.seconds(120)
|
|
10
|
+
|
|
11
|
+
const geminiQuotaSignals: ReadonlyArray<string> = [
|
|
12
|
+
"exhausted your capacity",
|
|
13
|
+
"quota",
|
|
14
|
+
"resource_exhausted",
|
|
15
|
+
"rate_limit",
|
|
16
|
+
"rate limit",
|
|
17
|
+
"429"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const genericQuotaSignals: ReadonlyArray<string> = [
|
|
21
|
+
...geminiQuotaSignals,
|
|
22
|
+
"usage limit",
|
|
23
|
+
"out of credits",
|
|
24
|
+
"too many requests"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const includesSignal = (text: string, signals: ReadonlyArray<string>): boolean => {
|
|
28
|
+
const normalized = text.toLowerCase()
|
|
29
|
+
return signals.some((signal) => normalized.includes(signal))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const isGeminiQuota = (text: string): boolean => includesSignal(text, geminiQuotaSignals)
|
|
33
|
+
|
|
34
|
+
const isGenericQuota = (text: string): boolean => includesSignal(text, genericQuotaSignals)
|
|
35
|
+
|
|
36
|
+
const resetDuration = (specification: string): Duration.Duration => {
|
|
37
|
+
let milliseconds = 0
|
|
38
|
+
|
|
39
|
+
for (const match of specification.matchAll(resetComponent)) {
|
|
40
|
+
const amount = Number(match[1] ?? "0")
|
|
41
|
+
const unit = (match[2] ?? "ms").toLowerCase()
|
|
42
|
+
|
|
43
|
+
switch (unit) {
|
|
44
|
+
case "d":
|
|
45
|
+
milliseconds += amount * 86_400_000
|
|
46
|
+
break
|
|
47
|
+
case "h":
|
|
48
|
+
milliseconds += amount * 3_600_000
|
|
49
|
+
break
|
|
50
|
+
case "m":
|
|
51
|
+
milliseconds += amount * 60_000
|
|
52
|
+
break
|
|
53
|
+
case "s":
|
|
54
|
+
milliseconds += amount * 1_000
|
|
55
|
+
break
|
|
56
|
+
default:
|
|
57
|
+
milliseconds += amount
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Duration.millis(milliseconds)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface WallClockTime {
|
|
65
|
+
readonly hour: number
|
|
66
|
+
readonly minute: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parseWallClockTime = (raw: string): WallClockTime | undefined => {
|
|
70
|
+
const match = raw.trim().match(/^(\d{1,2})(?::(\d{2}))?\s*([ap])m$/i)
|
|
71
|
+
if (match === null) {
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const clockHour = Number(match[1])
|
|
76
|
+
const minute = Number(match[2] ?? "0")
|
|
77
|
+
const period = (match[3] ?? "").toLowerCase()
|
|
78
|
+
if (clockHour < 1 || clockHour > 12 || minute < 0 || minute > 59) {
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
hour: (clockHour % 12) + (period === "p" ? 12 : 0),
|
|
84
|
+
minute
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface CalendarDate {
|
|
89
|
+
readonly year: number
|
|
90
|
+
readonly month: number
|
|
91
|
+
readonly day: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const dateTimeFormatter = (timeZone: string): Intl.DateTimeFormat =>
|
|
95
|
+
new Intl.DateTimeFormat("en-US-u-nu-latn", {
|
|
96
|
+
timeZone,
|
|
97
|
+
year: "numeric",
|
|
98
|
+
month: "2-digit",
|
|
99
|
+
day: "2-digit",
|
|
100
|
+
hour: "2-digit",
|
|
101
|
+
minute: "2-digit",
|
|
102
|
+
second: "2-digit",
|
|
103
|
+
hourCycle: "h23"
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const numericPart = (
|
|
107
|
+
parts: ReadonlyArray<Intl.DateTimeFormatPart>,
|
|
108
|
+
type: Intl.DateTimeFormatPartTypes
|
|
109
|
+
): number => Number(parts.find((part) => part.type === type)?.value ?? "0")
|
|
110
|
+
|
|
111
|
+
const localParts = (epochMillis: number, timeZone: string) => {
|
|
112
|
+
const parts = dateTimeFormatter(timeZone).formatToParts(new Date(epochMillis))
|
|
113
|
+
return {
|
|
114
|
+
year: numericPart(parts, "year"),
|
|
115
|
+
month: numericPart(parts, "month"),
|
|
116
|
+
day: numericPart(parts, "day"),
|
|
117
|
+
hour: numericPart(parts, "hour"),
|
|
118
|
+
minute: numericPart(parts, "minute"),
|
|
119
|
+
second: numericPart(parts, "second")
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const timeZoneOffsetMillis = (epochMillis: number, timeZone: string): number => {
|
|
124
|
+
const parts = localParts(epochMillis, timeZone)
|
|
125
|
+
const representedAsUtc = Date.UTC(
|
|
126
|
+
parts.year,
|
|
127
|
+
parts.month - 1,
|
|
128
|
+
parts.day,
|
|
129
|
+
parts.hour,
|
|
130
|
+
parts.minute,
|
|
131
|
+
parts.second
|
|
132
|
+
)
|
|
133
|
+
return representedAsUtc - Math.trunc(epochMillis / 1_000) * 1_000
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const epochForLocalTime = (date: CalendarDate, time: WallClockTime, timeZone: string): number => {
|
|
137
|
+
const utcGuess = Date.UTC(date.year, date.month - 1, date.day, time.hour, time.minute)
|
|
138
|
+
const firstCandidate = utcGuess - timeZoneOffsetMillis(utcGuess, timeZone)
|
|
139
|
+
return utcGuess - timeZoneOffsetMillis(firstCandidate, timeZone)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const nextCalendarDate = (date: CalendarDate): CalendarDate => {
|
|
143
|
+
const next = new Date(Date.UTC(date.year, date.month - 1, date.day + 1))
|
|
144
|
+
return {
|
|
145
|
+
year: next.getUTCFullYear(),
|
|
146
|
+
month: next.getUTCMonth() + 1,
|
|
147
|
+
day: next.getUTCDate()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const wallClockReset = (
|
|
152
|
+
text: string,
|
|
153
|
+
pattern: RegExp,
|
|
154
|
+
now: DateTime.Utc,
|
|
155
|
+
timeZone: string
|
|
156
|
+
): DateTime.Utc | undefined => {
|
|
157
|
+
const rawTime = text.match(pattern)?.[1]
|
|
158
|
+
const time = rawTime === undefined ? undefined : parseWallClockTime(rawTime)
|
|
159
|
+
if (time === undefined) {
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const nowMillis = DateTime.toEpochMillis(now)
|
|
165
|
+
const localNow = localParts(nowMillis, timeZone)
|
|
166
|
+
const today: CalendarDate = {
|
|
167
|
+
year: localNow.year,
|
|
168
|
+
month: localNow.month,
|
|
169
|
+
day: localNow.day
|
|
170
|
+
}
|
|
171
|
+
const todayCandidate = epochForLocalTime(today, time, timeZone)
|
|
172
|
+
const resetMillis =
|
|
173
|
+
todayCandidate > nowMillis
|
|
174
|
+
? todayCandidate
|
|
175
|
+
: epochForLocalTime(nextCalendarDate(today), time, timeZone)
|
|
176
|
+
|
|
177
|
+
return DateTime.makeUnsafe(resetMillis)
|
|
178
|
+
} catch {
|
|
179
|
+
return undefined
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const classifyUsageLimit = (
|
|
184
|
+
provider: string,
|
|
185
|
+
text: string,
|
|
186
|
+
now: DateTime.Utc,
|
|
187
|
+
timeZone: string
|
|
188
|
+
): LlmError | undefined => {
|
|
189
|
+
switch (provider) {
|
|
190
|
+
case "codex": {
|
|
191
|
+
const resetAt = wallClockReset(text, codexAt, now, timeZone)
|
|
192
|
+
return resetAt === undefined
|
|
193
|
+
? undefined
|
|
194
|
+
: UsageLimitError.make({
|
|
195
|
+
resetAt,
|
|
196
|
+
provider,
|
|
197
|
+
message: text
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
case "claude": {
|
|
201
|
+
const resetAt = wallClockReset(text, claudeAt, now, timeZone)
|
|
202
|
+
return resetAt === undefined
|
|
203
|
+
? undefined
|
|
204
|
+
: UsageLimitError.make({
|
|
205
|
+
resetAt,
|
|
206
|
+
provider,
|
|
207
|
+
message: text
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
case "gemini": {
|
|
211
|
+
const resetSpecification = text.match(geminiReset)?.[1]
|
|
212
|
+
if (resetSpecification !== undefined) {
|
|
213
|
+
const duration = resetDuration(resetSpecification)
|
|
214
|
+
return Duration.toMillis(duration) <= Duration.toMillis(shortResetLimit)
|
|
215
|
+
? RateLimitError.make({ retryAfter: duration })
|
|
216
|
+
: UsageLimitError.make({
|
|
217
|
+
resetAt: DateTime.addDuration(now, duration),
|
|
218
|
+
provider,
|
|
219
|
+
message: text
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return isGeminiQuota(text)
|
|
224
|
+
? UsageLimitError.make({
|
|
225
|
+
provider,
|
|
226
|
+
message: text
|
|
227
|
+
})
|
|
228
|
+
: undefined
|
|
229
|
+
}
|
|
230
|
+
case "grok":
|
|
231
|
+
case "cursor":
|
|
232
|
+
case "opencode":
|
|
233
|
+
return isGenericQuota(text)
|
|
234
|
+
? UsageLimitError.make({
|
|
235
|
+
provider,
|
|
236
|
+
message: text
|
|
237
|
+
})
|
|
238
|
+
: undefined
|
|
239
|
+
default:
|
|
240
|
+
return undefined
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import { DimensionScore, EvalResult } from "./Eval.ts"
|
|
3
|
+
import { makeEvaluator, type Evaluator } from "./Evaluator.ts"
|
|
4
|
+
|
|
5
|
+
const piiPatterns: ReadonlyArray<readonly [label: string, pattern: RegExp]> = [
|
|
6
|
+
["email", /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/],
|
|
7
|
+
["ssn", /\b\d{3}-\d{2}-\d{4}\b/],
|
|
8
|
+
["credit-card", /\b(?:\d[ -]?){13,16}\b/],
|
|
9
|
+
["phone", /\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/],
|
|
10
|
+
["ipv4", /\b(?:\d{1,3}\.){3}\d{1,3}\b/]
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const one = (name: string, ok: boolean, maxScore: number, why: string): EvalResult =>
|
|
14
|
+
EvalResult.make({
|
|
15
|
+
scores: [
|
|
16
|
+
DimensionScore.make({
|
|
17
|
+
name,
|
|
18
|
+
score: ok ? maxScore : 0,
|
|
19
|
+
reasoning: why
|
|
20
|
+
})
|
|
21
|
+
]
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const noPii = (maxScore = 2): Evaluator<string> =>
|
|
25
|
+
makeEvaluator((input) => {
|
|
26
|
+
const hits = piiPatterns.flatMap(([label, pattern]) => (pattern.test(input) ? [label] : []))
|
|
27
|
+
return Effect.succeed(
|
|
28
|
+
one(
|
|
29
|
+
"no-pii",
|
|
30
|
+
hits.length === 0,
|
|
31
|
+
maxScore,
|
|
32
|
+
hits.length === 0 ? "no PII detected" : `matched: ${hits.join(", ")}`
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const validJson = (maxScore = 2): Evaluator<string> =>
|
|
38
|
+
makeEvaluator((input) => {
|
|
39
|
+
let valid = true
|
|
40
|
+
try {
|
|
41
|
+
JSON.parse(input)
|
|
42
|
+
} catch {
|
|
43
|
+
valid = false
|
|
44
|
+
}
|
|
45
|
+
return Effect.succeed(
|
|
46
|
+
one("valid-json", valid, maxScore, valid ? "parses as JSON" : "not valid JSON")
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
export const matches = (regex: string, name = "format", maxScore = 2): Evaluator<string> =>
|
|
51
|
+
makeEvaluator((input) => {
|
|
52
|
+
const ok = new RegExp(`^(?:${regex})$`).test(input)
|
|
53
|
+
return Effect.succeed(
|
|
54
|
+
one(name, ok, maxScore, ok ? `matches /${regex}/` : `does not match /${regex}/`)
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export const lengthBetween = (min: number, max: number, maxScore = 2): Evaluator<string> =>
|
|
59
|
+
makeEvaluator((input) =>
|
|
60
|
+
Effect.succeed(
|
|
61
|
+
one(
|
|
62
|
+
"length",
|
|
63
|
+
input.length >= min && input.length <= max,
|
|
64
|
+
maxScore,
|
|
65
|
+
`length ${input.length}; bounds [${min}, ${max}]`
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
)
|
package/src/eval/Eval.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import type { LlmError } from "../Errors.ts"
|
|
4
|
+
import type { Evaluator } from "./Evaluator.ts"
|
|
5
|
+
|
|
6
|
+
export class Dimension extends Schema.Class<Dimension>("Dimension")({
|
|
7
|
+
name: Schema.String,
|
|
8
|
+
rubric: Schema.String,
|
|
9
|
+
maxScore: Schema.Int.pipe(Schema.withConstructorDefault(Effect.succeed(2)))
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
export class DimensionScore extends Schema.Class<DimensionScore>("DimensionScore")({
|
|
13
|
+
name: Schema.String,
|
|
14
|
+
score: Schema.Int,
|
|
15
|
+
reasoning: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("")))
|
|
16
|
+
}) {}
|
|
17
|
+
|
|
18
|
+
export class EvalResult extends Schema.Class<EvalResult>("EvalResult")({
|
|
19
|
+
scores: Schema.Array(DimensionScore),
|
|
20
|
+
summary: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("")))
|
|
21
|
+
}) {
|
|
22
|
+
score(name: string): number | undefined {
|
|
23
|
+
return this.scores.find((item) => item.name === name)?.score
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get total(): number {
|
|
27
|
+
return this.scores.reduce((sum, item) => sum + item.score, 0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
meets(minPerDimension: number): boolean {
|
|
31
|
+
return this.scores.every((item) => item.score >= minPerDimension)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class Sample extends Schema.Class<Sample>("Sample")({
|
|
36
|
+
response: Schema.String,
|
|
37
|
+
query: Schema.optionalKey(Schema.String),
|
|
38
|
+
context: Schema.optionalKey(Schema.String),
|
|
39
|
+
expected: Schema.optionalKey(Schema.String)
|
|
40
|
+
}) {}
|
|
41
|
+
|
|
42
|
+
export class DimensionStats extends Schema.Class<DimensionStats>("DimensionStats")({
|
|
43
|
+
name: Schema.String,
|
|
44
|
+
median: Schema.Int,
|
|
45
|
+
min: Schema.Int,
|
|
46
|
+
max: Schema.Int
|
|
47
|
+
}) {
|
|
48
|
+
get spread(): number {
|
|
49
|
+
return this.max - this.min
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
isFlaky(threshold: number): boolean {
|
|
53
|
+
return this.spread > threshold
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class RepeatedEval extends Schema.Class<RepeatedEval>("RepeatedEval")({
|
|
58
|
+
runs: Schema.Array(EvalResult),
|
|
59
|
+
stats: Schema.Array(DimensionStats),
|
|
60
|
+
spreadThreshold: Schema.Int
|
|
61
|
+
}) {
|
|
62
|
+
get flakyDimensions(): ReadonlyArray<string> {
|
|
63
|
+
return this.stats.filter((stat) => stat.isFlaky(this.spreadThreshold)).map((stat) => stat.name)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get isFlaky(): boolean {
|
|
67
|
+
return this.flakyDimensions.length > 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get aggregate(): EvalResult {
|
|
71
|
+
return EvalResult.make({
|
|
72
|
+
scores: this.stats.map((stat) =>
|
|
73
|
+
DimensionScore.make({
|
|
74
|
+
name: stat.name,
|
|
75
|
+
score: stat.median,
|
|
76
|
+
reasoning: `median of ${this.runs.length} runs`
|
|
77
|
+
})
|
|
78
|
+
),
|
|
79
|
+
summary: `aggregate of ${this.runs.length} runs`
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const median = (sorted: ReadonlyArray<number>): number =>
|
|
85
|
+
sorted.length === 0 ? 0 : (sorted[Math.floor((sorted.length - 1) / 2)] ?? 0)
|
|
86
|
+
|
|
87
|
+
export const repeat = Effect.fn("@llm4ts/core/eval/Eval.repeat")(function* <In>(
|
|
88
|
+
evaluator: Evaluator<In>,
|
|
89
|
+
input: In,
|
|
90
|
+
n = 3,
|
|
91
|
+
spreadThreshold = 1
|
|
92
|
+
): Effect.fn.Return<RepeatedEval, LlmError> {
|
|
93
|
+
const runs = yield* Effect.forEach(Array.from({ length: Math.max(0, n) }), () =>
|
|
94
|
+
evaluator.evaluate(input)
|
|
95
|
+
)
|
|
96
|
+
const names = [...new Set(runs.flatMap((run) => run.scores.map((score) => score.name)))]
|
|
97
|
+
const stats = names.map((name) => {
|
|
98
|
+
const values = runs
|
|
99
|
+
.flatMap((run) => {
|
|
100
|
+
const score = run.score(name)
|
|
101
|
+
return score === undefined ? [] : [score]
|
|
102
|
+
})
|
|
103
|
+
.sort((left, right) => left - right)
|
|
104
|
+
return DimensionStats.make({
|
|
105
|
+
name,
|
|
106
|
+
median: median(values),
|
|
107
|
+
min: values.at(0) ?? 0,
|
|
108
|
+
max: values.at(-1) ?? 0
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
return RepeatedEval.make({ runs, stats, spreadThreshold })
|
|
112
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import type { LlmError } from "../Errors.ts"
|
|
3
|
+
import { repeat } from "./Eval.ts"
|
|
4
|
+
import type { EvalResult, RepeatedEval } from "./Eval.ts"
|
|
5
|
+
import type { Evaluator } from "./Evaluator.ts"
|
|
6
|
+
|
|
7
|
+
export interface EvalCase<In> {
|
|
8
|
+
readonly name: string
|
|
9
|
+
readonly input: In
|
|
10
|
+
readonly minPerDimension?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CaseReport {
|
|
14
|
+
readonly name: string
|
|
15
|
+
readonly result: EvalResult
|
|
16
|
+
readonly repeated: RepeatedEval | undefined
|
|
17
|
+
readonly passed: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SuiteReport {
|
|
21
|
+
readonly cases: ReadonlyArray<CaseReport>
|
|
22
|
+
readonly passed: boolean
|
|
23
|
+
readonly failures: ReadonlyArray<CaseReport>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const run = Effect.fn("@llm4ts/core/eval/EvalSuite.run")(function* <In>(
|
|
27
|
+
evaluator: Evaluator<In>,
|
|
28
|
+
cases: ReadonlyArray<EvalCase<In>>,
|
|
29
|
+
repeats = 1,
|
|
30
|
+
spreadThreshold = 1
|
|
31
|
+
): Effect.fn.Return<SuiteReport, LlmError> {
|
|
32
|
+
const reports = yield* Effect.forEach(cases, (testCase) => {
|
|
33
|
+
const minimum = testCase.minPerDimension ?? 1
|
|
34
|
+
if (repeats <= 1) {
|
|
35
|
+
return evaluator.evaluate(testCase.input).pipe(
|
|
36
|
+
Effect.map(
|
|
37
|
+
(result): CaseReport => ({
|
|
38
|
+
name: testCase.name,
|
|
39
|
+
result,
|
|
40
|
+
repeated: undefined,
|
|
41
|
+
passed: result.meets(minimum)
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return repeat(evaluator, testCase.input, repeats, spreadThreshold).pipe(
|
|
47
|
+
Effect.map((repeated): CaseReport => {
|
|
48
|
+
const result = repeated.aggregate
|
|
49
|
+
return {
|
|
50
|
+
name: testCase.name,
|
|
51
|
+
result,
|
|
52
|
+
repeated,
|
|
53
|
+
passed: result.meets(minimum) && !repeated.isFlaky
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
return {
|
|
59
|
+
cases: reports,
|
|
60
|
+
passed: reports.every((report) => report.passed),
|
|
61
|
+
failures: reports.filter((report) => !report.passed)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import type { LlmError } from "../Errors.ts"
|
|
3
|
+
import { EvalResult } from "./Eval.ts"
|
|
4
|
+
|
|
5
|
+
export interface Evaluator<in In> {
|
|
6
|
+
readonly evaluate: (input: In) => Effect.Effect<EvalResult, LlmError>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const makeEvaluator = <In>(evaluate: Evaluator<In>["evaluate"]): Evaluator<In> => ({
|
|
10
|
+
evaluate
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const contramap = <In, In2>(
|
|
14
|
+
evaluator: Evaluator<In>,
|
|
15
|
+
project: (input: In2) => In
|
|
16
|
+
): Evaluator<In2> => makeEvaluator((input) => evaluator.evaluate(project(input)))
|
|
17
|
+
|
|
18
|
+
export const all = <In>(...evaluators: ReadonlyArray<Evaluator<In>>): Evaluator<In> =>
|
|
19
|
+
makeEvaluator((input) =>
|
|
20
|
+
Effect.forEach(evaluators, (evaluator) => evaluator.evaluate(input)).pipe(
|
|
21
|
+
Effect.map((results) =>
|
|
22
|
+
EvalResult.make({
|
|
23
|
+
scores: results.flatMap((result) => result.scores),
|
|
24
|
+
summary: results
|
|
25
|
+
.map((result) => result.summary)
|
|
26
|
+
.filter((summary) => summary.length > 0)
|
|
27
|
+
.join("; ")
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import type { LlmServiceShape } from "../LlmService.ts"
|
|
4
|
+
import type { JsonSchema } from "../Models.ts"
|
|
5
|
+
import { DimensionScore, EvalResult } from "./Eval.ts"
|
|
6
|
+
import type { Dimension, Sample } from "./Eval.ts"
|
|
7
|
+
import { makeEvaluator, type Evaluator } from "./Evaluator.ts"
|
|
8
|
+
|
|
9
|
+
export class JudgeResponse extends Schema.Class<JudgeResponse>("JudgeResponse")({
|
|
10
|
+
scores: Schema.Array(DimensionScore)
|
|
11
|
+
}) {}
|
|
12
|
+
|
|
13
|
+
export const defaultSystem =
|
|
14
|
+
"You are an impartial evaluator. Score each dimension strictly on its rubric; when unsure, score lower. " +
|
|
15
|
+
"Return ONLY valid JSON, no prose."
|
|
16
|
+
|
|
17
|
+
const judgeJsonSchema: JsonSchema = {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
scores: {
|
|
21
|
+
type: "array",
|
|
22
|
+
items: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
name: { type: "string" },
|
|
26
|
+
score: { type: "integer" },
|
|
27
|
+
reasoning: { type: "string" }
|
|
28
|
+
},
|
|
29
|
+
required: ["name", "score"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
required: ["scores"],
|
|
34
|
+
additionalProperties: false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const buildPrompt = (
|
|
38
|
+
system: string,
|
|
39
|
+
dimensions: ReadonlyArray<Dimension>,
|
|
40
|
+
sample: Sample
|
|
41
|
+
): string => {
|
|
42
|
+
const rubric = dimensions
|
|
43
|
+
.map((dimension) => `- ${dimension.name} (0..${dimension.maxScore}): ${dimension.rubric}`)
|
|
44
|
+
.join("\n")
|
|
45
|
+
const user = [
|
|
46
|
+
sample.query === undefined ? undefined : `Query: ${sample.query}`,
|
|
47
|
+
sample.context === undefined ? undefined : `Context: ${sample.context}`,
|
|
48
|
+
`Response: ${sample.response}`,
|
|
49
|
+
sample.expected === undefined ? undefined : `Expected: ${sample.expected}`
|
|
50
|
+
]
|
|
51
|
+
.filter((line): line is string => line !== undefined)
|
|
52
|
+
.join("\n")
|
|
53
|
+
return `${system}
|
|
54
|
+
|
|
55
|
+
Score each dimension on its 0..max scale per the rubric:
|
|
56
|
+
${rubric}
|
|
57
|
+
|
|
58
|
+
Return ONLY this JSON: {"scores":[{"name":"<dimension>","score":<int>,"reasoning":"<one sentence>"}]}
|
|
59
|
+
|
|
60
|
+
${user}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const toResult = (dimensions: ReadonlyArray<Dimension>, response: JudgeResponse): EvalResult => {
|
|
64
|
+
const byName = new Map(response.scores.map((score) => [score.name, score]))
|
|
65
|
+
return EvalResult.make({
|
|
66
|
+
scores: dimensions.map((dimension) => {
|
|
67
|
+
const score = byName.get(dimension.name)
|
|
68
|
+
return score === undefined
|
|
69
|
+
? DimensionScore.make({
|
|
70
|
+
name: dimension.name,
|
|
71
|
+
score: 0,
|
|
72
|
+
reasoning: "missing"
|
|
73
|
+
})
|
|
74
|
+
: DimensionScore.make({
|
|
75
|
+
name: dimension.name,
|
|
76
|
+
score: Math.max(0, Math.min(dimension.maxScore, score.score)),
|
|
77
|
+
reasoning: score.reasoning
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const judge = (
|
|
84
|
+
llm: LlmServiceShape,
|
|
85
|
+
dimensions: ReadonlyArray<Dimension>,
|
|
86
|
+
system = defaultSystem
|
|
87
|
+
): Evaluator<Sample> =>
|
|
88
|
+
makeEvaluator((sample) =>
|
|
89
|
+
llm
|
|
90
|
+
.executeStructured(buildPrompt(system, dimensions, sample), JudgeResponse, judgeJsonSchema)
|
|
91
|
+
.pipe(Effect.map((response) => toResult(dimensions, response)))
|
|
92
|
+
)
|