Compare commits
3 Commits
5e522bad8b
...
03d4f27d2b
| Author | SHA1 | Date | |
|---|---|---|---|
| 03d4f27d2b | |||
| 4ac2038331 | |||
| 14a7e7e04c |
@@ -25,6 +25,7 @@ This command renders the prepared prompt and effective runtime settings without
|
|||||||
- [Configuration reference](docs/config.md)
|
- [Configuration reference](docs/config.md)
|
||||||
- [Operations guide](docs/operations.md)
|
- [Operations guide](docs/operations.md)
|
||||||
- [Troubleshooting](docs/troubleshooting.md)
|
- [Troubleshooting](docs/troubleshooting.md)
|
||||||
|
- [Go library package](docs/consumers/pkg-scriptorium.md)
|
||||||
- [HTTP API integration](docs/integrations/http-api.md)
|
- [HTTP API integration](docs/integrations/http-api.md)
|
||||||
- [OpenAI-compatible chat integration](docs/integrations/openai-compatible-chat.md)
|
- [OpenAI-compatible chat integration](docs/integrations/openai-compatible-chat.md)
|
||||||
- [Narratio subprocess integration](docs/integrations/narratio.md)
|
- [Narratio subprocess integration](docs/integrations/narratio.md)
|
||||||
@@ -34,3 +35,4 @@ This command renders the prepared prompt and effective runtime settings without
|
|||||||
|
|
||||||
- `examples/render-markdown-summary.sh`
|
- `examples/render-markdown-summary.sh`
|
||||||
- `examples/http-run.json`
|
- `examples/http-run.json`
|
||||||
|
- `examples/go-library/prepare`
|
||||||
|
|||||||
335
convert.go
Normal file
335
convert.go
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
package scriptorium
|
||||||
|
|
||||||
|
import "gitea.maximumdirect.net/eric/scriptorium/internal/domain"
|
||||||
|
|
||||||
|
func toDomainRunRequest(req RunRequest) domain.RunRequest {
|
||||||
|
return domain.RunRequest{
|
||||||
|
PromptID: req.PromptID,
|
||||||
|
PromptVersion: req.PromptVersion,
|
||||||
|
ProfileID: req.ProfileID,
|
||||||
|
Inputs: toDomainArtifactRefMap(req.Inputs),
|
||||||
|
Vars: copyStringMap(req.Vars),
|
||||||
|
Execution: toDomainExecutionTargetOverride(req.Execution),
|
||||||
|
Validation: toDomainOutputContractPtr(req.Validation),
|
||||||
|
Metadata: copyStringMap(req.Metadata),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainPreparedRun(prepared *domain.PreparedRun) *PreparedRun {
|
||||||
|
if prepared == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &PreparedRun{
|
||||||
|
PromptID: prepared.PromptID,
|
||||||
|
PromptVersion: prepared.PromptVersion,
|
||||||
|
PromptHash: prepared.PromptHash,
|
||||||
|
SelectedProfileID: prepared.SelectedProfileID,
|
||||||
|
EffectiveModelParams: fromDomainExecutionTarget(prepared.EffectiveModelParams),
|
||||||
|
OutputContract: fromDomainOutputContract(prepared.OutputContract),
|
||||||
|
StructuredOutput: fromDomainStructuredOutputSpec(prepared.StructuredOutput),
|
||||||
|
InputHashes: copyStringMap(prepared.InputHashes),
|
||||||
|
SessionID: prepared.SessionID,
|
||||||
|
RenderedPromptHash: prepared.RenderedPromptHash,
|
||||||
|
Messages: fromDomainRenderedMessages(prepared.Messages),
|
||||||
|
StartTime: prepared.StartTime,
|
||||||
|
EndTime: prepared.EndTime,
|
||||||
|
DurationMS: prepared.DurationMS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainRunResult(result *domain.RunResult) *RunResult {
|
||||||
|
if result == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &RunResult{
|
||||||
|
RunID: result.RunID,
|
||||||
|
Artifact: fromDomainArtifact(result.Artifact),
|
||||||
|
RawOutput: result.RawOutput,
|
||||||
|
Validation: fromDomainValidationResult(result.Validation),
|
||||||
|
PromptID: result.PromptID,
|
||||||
|
PromptVersion: result.PromptVersion,
|
||||||
|
PromptHash: result.PromptHash,
|
||||||
|
RenderedPromptHash: result.RenderedPromptHash,
|
||||||
|
SelectedProfileID: result.SelectedProfileID,
|
||||||
|
ModelName: result.ModelName,
|
||||||
|
Endpoint: result.Endpoint,
|
||||||
|
EffectiveModelParams: fromDomainExecutionTarget(result.EffectiveModelParams),
|
||||||
|
InputHashes: copyStringMap(result.InputHashes),
|
||||||
|
Usage: fromDomainTokenUsage(result.Usage),
|
||||||
|
StartTime: result.StartTime,
|
||||||
|
EndTime: result.EndTime,
|
||||||
|
Duration: result.Duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainGenerateRequest(req domain.GenerateRequest) GenerateRequest {
|
||||||
|
return GenerateRequest{
|
||||||
|
Prompt: fromDomainRenderedPrompt(req.Prompt),
|
||||||
|
Target: fromDomainExecutionTarget(req.Target),
|
||||||
|
TargetPresence: fromDomainExecutionTargetPresence(req.TargetPresence),
|
||||||
|
StructuredOutput: fromDomainStructuredOutputSpec(req.StructuredOutput),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDomainGenerateResponse(resp *GenerateResponse) *domain.GenerateResponse {
|
||||||
|
if resp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &domain.GenerateResponse{
|
||||||
|
Content: resp.Content,
|
||||||
|
Usage: toDomainTokenUsage(resp.Usage),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainRenderedPrompt(prompt domain.RenderedPrompt) RenderedPrompt {
|
||||||
|
return RenderedPrompt{
|
||||||
|
SessionID: prompt.SessionID,
|
||||||
|
Messages: fromDomainRenderedMessages(prompt.Messages),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDomainArtifactRefMap(src map[string]ArtifactRef) map[string]domain.ArtifactRef {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]domain.ArtifactRef, len(src))
|
||||||
|
for k, v := range src {
|
||||||
|
out[k] = toDomainArtifactRef(v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDomainArtifactRef(ref ArtifactRef) domain.ArtifactRef {
|
||||||
|
return domain.ArtifactRef{
|
||||||
|
Type: domain.ArtifactRefType(ref.Type),
|
||||||
|
URI: ref.URI,
|
||||||
|
Body: ref.Body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainArtifact(artifact domain.Artifact) Artifact {
|
||||||
|
return Artifact{
|
||||||
|
Name: artifact.Name,
|
||||||
|
ContentType: artifact.ContentType,
|
||||||
|
Body: copyBytes(artifact.Body),
|
||||||
|
URI: artifact.URI,
|
||||||
|
Size: artifact.Size,
|
||||||
|
Hash: artifact.Hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDomainExecutionTargetOverride(override *ExecutionTargetOverride) *domain.ExecutionTargetOverride {
|
||||||
|
if override == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &domain.ExecutionTargetOverride{
|
||||||
|
Endpoint: override.Endpoint,
|
||||||
|
Model: override.Model,
|
||||||
|
Temperature: copyFloat64Ptr(override.Temperature),
|
||||||
|
MaxTokens: copyIntPtr(override.MaxTokens),
|
||||||
|
TopP: copyFloat64Ptr(override.TopP),
|
||||||
|
TimeoutSeconds: copyIntPtr(override.TimeoutSeconds),
|
||||||
|
ServiceTier: override.ServiceTier,
|
||||||
|
ReasoningEffort: override.ReasoningEffort,
|
||||||
|
APIKeyEnv: override.APIKeyEnv,
|
||||||
|
ExtraParams: copyAnyMap(override.ExtraParams),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainExecutionTarget(target domain.ExecutionTarget) ExecutionTarget {
|
||||||
|
return ExecutionTarget{
|
||||||
|
Endpoint: target.Endpoint,
|
||||||
|
Model: target.Model,
|
||||||
|
Temperature: target.Temperature,
|
||||||
|
MaxTokens: target.MaxTokens,
|
||||||
|
TopP: target.TopP,
|
||||||
|
TimeoutSeconds: target.TimeoutSeconds,
|
||||||
|
ServiceTier: target.ServiceTier,
|
||||||
|
ReasoningEffort: target.ReasoningEffort,
|
||||||
|
APIKeyEnv: target.APIKeyEnv,
|
||||||
|
ExtraParams: copyAnyMap(target.ExtraParams),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainExecutionTargetPresence(presence domain.ExecutionTargetPresence) ExecutionTargetPresence {
|
||||||
|
return ExecutionTargetPresence{
|
||||||
|
Temperature: presence.Temperature,
|
||||||
|
MaxTokens: presence.MaxTokens,
|
||||||
|
TopP: presence.TopP,
|
||||||
|
TimeoutSeconds: presence.TimeoutSeconds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDomainOutputContractPtr(contract *OutputContract) *domain.OutputContract {
|
||||||
|
if contract == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := toDomainOutputContract(*contract)
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDomainOutputContract(contract OutputContract) domain.OutputContract {
|
||||||
|
return domain.OutputContract{
|
||||||
|
Format: domain.OutputFormat(contract.Format),
|
||||||
|
ValidationMode: domain.ValidationMode(contract.ValidationMode),
|
||||||
|
SchemaPath: contract.SchemaPath,
|
||||||
|
RepairAttempts: contract.RepairAttempts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainOutputContract(contract domain.OutputContract) OutputContract {
|
||||||
|
return OutputContract{
|
||||||
|
Format: OutputFormat(contract.Format),
|
||||||
|
ValidationMode: ValidationMode(contract.ValidationMode),
|
||||||
|
SchemaPath: contract.SchemaPath,
|
||||||
|
RepairAttempts: contract.RepairAttempts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainValidationResult(result domain.ValidationResult) ValidationResult {
|
||||||
|
return ValidationResult{
|
||||||
|
Status: ValidationStatus(result.Status),
|
||||||
|
Mode: ValidationMode(result.Mode),
|
||||||
|
Errors: copyStringSlice(result.Errors),
|
||||||
|
SchemaPath: result.SchemaPath,
|
||||||
|
RepairAttempts: result.RepairAttempts,
|
||||||
|
IsValid: result.IsValid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainTokenUsage(usage domain.TokenUsage) TokenUsage {
|
||||||
|
return TokenUsage{
|
||||||
|
PromptTokens: usage.PromptTokens,
|
||||||
|
CompletionTokens: usage.CompletionTokens,
|
||||||
|
TotalTokens: usage.TotalTokens,
|
||||||
|
CachedTokens: usage.CachedTokens,
|
||||||
|
CacheWriteTokens: usage.CacheWriteTokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDomainTokenUsage(usage TokenUsage) domain.TokenUsage {
|
||||||
|
return domain.TokenUsage{
|
||||||
|
PromptTokens: usage.PromptTokens,
|
||||||
|
CompletionTokens: usage.CompletionTokens,
|
||||||
|
TotalTokens: usage.TotalTokens,
|
||||||
|
CachedTokens: usage.CachedTokens,
|
||||||
|
CacheWriteTokens: usage.CacheWriteTokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainRenderedMessages(messages []domain.RenderedMessage) []RenderedMessage {
|
||||||
|
if messages == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]RenderedMessage, len(messages))
|
||||||
|
for i, msg := range messages {
|
||||||
|
out[i] = RenderedMessage{
|
||||||
|
Role: msg.Role,
|
||||||
|
Content: msg.Content,
|
||||||
|
CacheControl: fromDomainCacheControl(msg.CacheControl),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainCacheControl(cacheControl *domain.CacheControl) *CacheControl {
|
||||||
|
if cacheControl == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &CacheControl{
|
||||||
|
Type: CacheControlType(cacheControl.Type),
|
||||||
|
TTL: cacheControl.TTL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainStructuredOutputSpec(spec *domain.StructuredOutputSpec) *StructuredOutputSpec {
|
||||||
|
if spec == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := &StructuredOutputSpec{
|
||||||
|
Type: StructuredOutputType(spec.Type),
|
||||||
|
}
|
||||||
|
if spec.JSONSchema != nil {
|
||||||
|
out.JSONSchema = &StructuredOutputJSONSpec{
|
||||||
|
Name: spec.JSONSchema.Name,
|
||||||
|
Strict: spec.JSONSchema.Strict,
|
||||||
|
Schema: copyAny(spec.JSONSchema.Schema),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyStringMap(src map[string]string) map[string]string {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]string, len(src))
|
||||||
|
for k, v := range src {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyAnyMap(src map[string]any) map[string]any {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]any, len(src))
|
||||||
|
for k, v := range src {
|
||||||
|
out[k] = copyAny(v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyAny(value any) any {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
return copyAnyMap(v)
|
||||||
|
case []any:
|
||||||
|
out := make([]any, len(v))
|
||||||
|
for i, item := range v {
|
||||||
|
out[i] = copyAny(item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case []string:
|
||||||
|
return copyStringSlice(v)
|
||||||
|
case []byte:
|
||||||
|
return copyBytes(v)
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyStringSlice(src []string) []string {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, len(src))
|
||||||
|
copy(out, src)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyBytes(src []byte) []byte {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]byte, len(src))
|
||||||
|
copy(out, src)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFloat64Ptr(src *float64) *float64 {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := *src
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyIntPtr(src *int) *int {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := *src
|
||||||
|
return &v
|
||||||
|
}
|
||||||
13
docs/consumers/api.md
Normal file
13
docs/consumers/api.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Consumer API Overview
|
||||||
|
|
||||||
|
Scriptorium can be used by consumers through three implemented surfaces:
|
||||||
|
|
||||||
|
- CLI commands, documented in [CLI reference](../cli.md).
|
||||||
|
- HTTP `POST /v1/runs`, documented in [HTTP API integration](../integrations/http-api.md).
|
||||||
|
- Go package `gitea.maximumdirect.net/eric/scriptorium`, documented in [pkg-scriptorium](pkg-scriptorium.md).
|
||||||
|
|
||||||
|
The Go package is the typed in-process API. It prepares prompts, runs prompts, accepts file or inline artifacts, supports per-request execution overrides, and exposes stable public errors for `errors.Is`.
|
||||||
|
|
||||||
|
Use the Go package when the caller is a Go program that wants typed requests/results, context cancellation, repeated calls without subprocess overhead, or fake LLM injection for tests. Use the CLI or HTTP surfaces when process isolation, language neutrality, or an HTTP boundary is preferred.
|
||||||
|
|
||||||
|
Raw API key values are not accepted in public payloads and are not returned in prepared or run results. Execution profiles may reference an environment variable name through `api_key_env`.
|
||||||
129
docs/consumers/pkg-scriptorium.md
Normal file
129
docs/consumers/pkg-scriptorium.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Package scriptorium
|
||||||
|
|
||||||
|
Import path:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "gitea.maximumdirect.net/eric/scriptorium"
|
||||||
|
```
|
||||||
|
|
||||||
|
The root package is a public facade over Scriptorium's prompt execution use case. It keeps `internal/*` packages private while exposing typed construction, preparation, execution, inputs, results, and errors.
|
||||||
|
|
||||||
|
## Construct An Engine
|
||||||
|
|
||||||
|
```go
|
||||||
|
engine, err := scriptorium.NewEngine(scriptorium.Config{
|
||||||
|
PromptDir: "./examples/prompts",
|
||||||
|
ProfileDir: "./examples/profiles",
|
||||||
|
SchemaDir: "./examples/schemas",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`PromptDir` and `ProfileDir` are required. `SchemaDir` defaults to the built-in schema directory. `Timeout` and `HTTPClient` configure the default OpenAI-compatible client used by `Run` when no custom LLM client is supplied.
|
||||||
|
|
||||||
|
## Prepare A Prompt
|
||||||
|
|
||||||
|
`Prepare` resolves the prompt definition, profile, inputs, variables, output contract, structured-output metadata, and rendered messages without calling an LLM.
|
||||||
|
|
||||||
|
```go
|
||||||
|
prepared, err := engine.Prepare(ctx, scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.File("./examples/fixtures/transcript.md"),
|
||||||
|
"glossary": scriptorium.File("./examples/fixtures/glossary.yml"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = prepared.Messages
|
||||||
|
```
|
||||||
|
|
||||||
|
Input helpers:
|
||||||
|
|
||||||
|
- `scriptorium.File(path)` loads an input artifact from a file.
|
||||||
|
- `scriptorium.Inline(body)` passes inline input content.
|
||||||
|
- `scriptorium.InlineWithURI(uri, body)` passes inline content with URI metadata.
|
||||||
|
|
||||||
|
## Run A Prompt
|
||||||
|
|
||||||
|
`Run` prepares the prompt, calls the configured LLM client, builds the output artifact, and validates the output.
|
||||||
|
|
||||||
|
```go
|
||||||
|
result, err := engine.Run(ctx, scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.File("./examples/fixtures/transcript.md"),
|
||||||
|
"glossary": scriptorium.File("./examples/fixtures/glossary.yml"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = result.Artifact
|
||||||
|
```
|
||||||
|
|
||||||
|
`RunResult` includes the run ID, output artifact, raw output, validation result, prompt/profile/model metadata, effective model parameters, input hashes, token/cache usage, and timing fields. Validation content failures return a successful `RunResult` with failed validation status. Runtime validation errors return `ErrValidation`.
|
||||||
|
|
||||||
|
## Inject An LLM Client
|
||||||
|
|
||||||
|
Use `WithLLMClient` for tests or custom model integrations:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type fakeLLM struct{}
|
||||||
|
|
||||||
|
func (fakeLLM) Generate(ctx context.Context, req scriptorium.GenerateRequest) (*scriptorium.GenerateResponse, error) {
|
||||||
|
return &scriptorium.GenerateResponse{
|
||||||
|
Content: "generated text",
|
||||||
|
Usage: scriptorium.TokenUsage{TotalTokens: 12},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := scriptorium.NewEngine(cfg, scriptorium.WithLLMClient(fakeLLM{}))
|
||||||
|
```
|
||||||
|
|
||||||
|
The injected client receives the rendered prompt, effective execution target, target presence metadata for explicit numeric overrides, and structured-output spec. `WithLLMClient(nil)` returns `ErrInvalidConfig`.
|
||||||
|
|
||||||
|
## Request Overrides
|
||||||
|
|
||||||
|
`RunRequest.Execution` accepts per-request overrides. Numeric override fields are pointers so explicit zero values are preserved:
|
||||||
|
|
||||||
|
```go
|
||||||
|
zero := 0
|
||||||
|
req.Execution = &scriptorium.ExecutionTargetOverride{
|
||||||
|
MaxTokens: &zero,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
Public methods wrap context while preserving stable sentinel checks with `errors.Is`:
|
||||||
|
|
||||||
|
- `ErrInvalidConfig`
|
||||||
|
- `ErrInvalidRequest`
|
||||||
|
- `ErrPromptNotFound`
|
||||||
|
- `ErrProfileNotFound`
|
||||||
|
- `ErrPromptLoad`
|
||||||
|
- `ErrProfileLoad`
|
||||||
|
- `ErrArtifactLoad`
|
||||||
|
- `ErrPromptRender`
|
||||||
|
- `ErrLLMGenerate`
|
||||||
|
- `ErrValidation`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if errors.Is(err, scriptorium.ErrPromptNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Run the prepare-only example from the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./examples/go-library/prepare
|
||||||
|
```
|
||||||
@@ -8,6 +8,7 @@ This document describes implemented adapter/repository boundaries and their curr
|
|||||||
|
|
||||||
- `internal/adapter/cli`: CLI command parsing, app wiring, stdout/stderr handling, exit codes.
|
- `internal/adapter/cli`: CLI command parsing, app wiring, stdout/stderr handling, exit codes.
|
||||||
- `internal/adapter/http`: HTTP request/response mapping for `POST /v1/runs`.
|
- `internal/adapter/http`: HTTP request/response mapping for `POST /v1/runs`.
|
||||||
|
- root package `scriptorium`: public Go library facade for preparing and running prompt requests.
|
||||||
- `internal/promptdef`: filesystem prompt-definition repository.
|
- `internal/promptdef`: filesystem prompt-definition repository.
|
||||||
- `internal/profile`: filesystem execution-profile repository.
|
- `internal/profile`: filesystem execution-profile repository.
|
||||||
- `internal/artifact`: input artifact reader.
|
- `internal/artifact`: input artifact reader.
|
||||||
@@ -30,6 +31,13 @@ HTTP adapter:
|
|||||||
- Output: JSON success/error body with mapped status codes.
|
- Output: JSON success/error body with mapped status codes.
|
||||||
- Success metadata includes token usage plus cache usage counters.
|
- Success metadata includes token usage plus cache usage counters.
|
||||||
|
|
||||||
|
Public library facade:
|
||||||
|
|
||||||
|
- Input: typed `scriptorium.RunRequest` values.
|
||||||
|
- Output: typed `PreparedRun` and `RunResult` values plus public sentinel errors.
|
||||||
|
- Custom LLM behavior is injected with `WithLLMClient`; otherwise the default OpenAI-compatible client is used.
|
||||||
|
- Public types are facade types converted at the package boundary; internal domain types remain internal.
|
||||||
|
|
||||||
Filesystem repositories:
|
Filesystem repositories:
|
||||||
|
|
||||||
- Input: prompt/profile YAML files under configured directories.
|
- Input: prompt/profile YAML files under configured directories.
|
||||||
|
|||||||
141
engine.go
Normal file
141
engine.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package scriptorium
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
artifactadapter "gitea.maximumdirect.net/eric/scriptorium/internal/artifact"
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/defaults"
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/llm"
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/profile"
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/prompt"
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/promptdef"
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/usecase"
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/validate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInvalidConfig indicates invalid public engine configuration.
|
||||||
|
var ErrInvalidConfig = errors.New("invalid engine configuration")
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidRequest = errors.New("invalid run request")
|
||||||
|
ErrPromptNotFound = errors.New("prompt not found")
|
||||||
|
ErrProfileNotFound = errors.New("profile not found")
|
||||||
|
ErrPromptLoad = errors.New("failed to load prompt definition")
|
||||||
|
ErrProfileLoad = errors.New("failed to load execution profile")
|
||||||
|
ErrArtifactLoad = errors.New("failed to load artifact")
|
||||||
|
ErrPromptRender = errors.New("failed to render prompt")
|
||||||
|
ErrLLMGenerate = errors.New("failed to generate output")
|
||||||
|
ErrValidation = errors.New("failed to validate output")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Engine prepares and runs Scriptorium prompt requests.
|
||||||
|
type Engine struct {
|
||||||
|
runner *usecase.Runner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config configures a public Scriptorium engine.
|
||||||
|
type Config struct {
|
||||||
|
PromptDir string
|
||||||
|
ProfileDir string
|
||||||
|
SchemaDir string
|
||||||
|
Timeout time.Duration
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option customizes engine construction.
|
||||||
|
type Option func(*engineOptions) error
|
||||||
|
|
||||||
|
type engineOptions struct {
|
||||||
|
llmClient llm.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLLMClient injects a custom LLM client for execution.
|
||||||
|
func WithLLMClient(client LLMClient) Option {
|
||||||
|
return func(options *engineOptions) error {
|
||||||
|
if client == nil {
|
||||||
|
return ErrInvalidConfig
|
||||||
|
}
|
||||||
|
options.llmClient = publicLLMClientAdapter{client: client}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEngine constructs an Engine using the same default internal components as
|
||||||
|
// the CLI and HTTP adapters.
|
||||||
|
func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
|
||||||
|
if strings.TrimSpace(cfg.PromptDir) == "" {
|
||||||
|
return nil, fmt.Errorf("%w: prompt directory is required", ErrInvalidConfig)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.ProfileDir) == "" {
|
||||||
|
return nil, fmt.Errorf("%w: profile directory is required", ErrInvalidConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
var options engineOptions
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := opt(&options); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaDir := cfg.SchemaDir
|
||||||
|
if strings.TrimSpace(schemaDir) == "" {
|
||||||
|
schemaDir = defaults.SchemaDirDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
llmClient := options.llmClient
|
||||||
|
if llmClient == nil {
|
||||||
|
var err error
|
||||||
|
llmClient, err = llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{
|
||||||
|
Timeout: cfg.Timeout,
|
||||||
|
HTTPClient: cfg.HTTPClient,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Engine{
|
||||||
|
runner: usecase.NewRunner(
|
||||||
|
promptdef.NewFilesystemRepository(cfg.PromptDir),
|
||||||
|
profile.NewFilesystemRepository(cfg.ProfileDir),
|
||||||
|
artifactadapter.NewCompositeReader(),
|
||||||
|
prompt.NewGoRenderer(),
|
||||||
|
llmClient,
|
||||||
|
validate.NewStandardValidator(schemaDir),
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare resolves a prompt request without calling an LLM.
|
||||||
|
func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, error) {
|
||||||
|
if e == nil || e.runner == nil {
|
||||||
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepared, err := e.runner.Prepare(ctx, toDomainRunRequest(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, mapPublicError(err)
|
||||||
|
}
|
||||||
|
return fromDomainPreparedRun(prepared), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes a prompt request and returns the generated artifact and metadata.
|
||||||
|
func (e *Engine) Run(ctx context.Context, req RunRequest) (*RunResult, error) {
|
||||||
|
if e == nil || e.runner == nil {
|
||||||
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := e.runner.Run(ctx, toDomainRunRequest(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, mapPublicError(err)
|
||||||
|
}
|
||||||
|
return fromDomainRunResult(result), nil
|
||||||
|
}
|
||||||
429
engine_test.go
Normal file
429
engine_test.go
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
package scriptorium_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewEngineRejectsMissingPromptDir(t *testing.T) {
|
||||||
|
_, err := scriptorium.NewEngine(scriptorium.Config{ProfileDir: "./examples/profiles"})
|
||||||
|
if !errors.Is(err, scriptorium.ErrInvalidConfig) {
|
||||||
|
t.Fatalf("expected ErrInvalidConfig, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewEngineRejectsMissingProfileDir(t *testing.T) {
|
||||||
|
_, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: "./examples/prompts"})
|
||||||
|
if !errors.Is(err, scriptorium.ErrInvalidConfig) {
|
||||||
|
t.Fatalf("expected ErrInvalidConfig, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareWorksWithExampleDirectoriesAndFileInputs(t *testing.T) {
|
||||||
|
engine := newExampleEngine(t)
|
||||||
|
|
||||||
|
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.File("./examples/fixtures/transcript.md"),
|
||||||
|
"glossary": scriptorium.File("./examples/fixtures/glossary.yml"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected prepare to succeed, got %v", err)
|
||||||
|
}
|
||||||
|
if prepared.PromptID != "generic.markdown_summary" {
|
||||||
|
t.Fatalf("unexpected prompt id: %q", prepared.PromptID)
|
||||||
|
}
|
||||||
|
if prepared.SelectedProfileID != "local-fast" {
|
||||||
|
t.Fatalf("unexpected selected profile: %q", prepared.SelectedProfileID)
|
||||||
|
}
|
||||||
|
if prepared.EffectiveModelParams.Model != "gpt-4o-mini" {
|
||||||
|
t.Fatalf("unexpected effective model: %q", prepared.EffectiveModelParams.Model)
|
||||||
|
}
|
||||||
|
if len(prepared.Messages) != 2 {
|
||||||
|
t.Fatalf("expected rendered messages, got %d", len(prepared.Messages))
|
||||||
|
}
|
||||||
|
if prepared.InputHashes["transcript"] == "" || prepared.InputHashes["glossary"] == "" {
|
||||||
|
t.Fatalf("expected input hashes, got %#v", prepared.InputHashes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareWorksWithInlineInputs(t *testing.T) {
|
||||||
|
engine := newExampleEngine(t)
|
||||||
|
|
||||||
|
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.Inline("Rin scouts the tower.\nKara lights a lantern."),
|
||||||
|
"glossary": scriptorium.InlineWithURI("memory://glossary.yml", "party:\n - Rin\n - Kara\n"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected prepare to succeed, got %v", err)
|
||||||
|
}
|
||||||
|
if len(prepared.Messages) != 2 {
|
||||||
|
t.Fatalf("expected rendered messages, got %d", len(prepared.Messages))
|
||||||
|
}
|
||||||
|
rendered := prepared.Messages[1].Content
|
||||||
|
if !strings.Contains(rendered, "Rin scouts the tower.") || !strings.Contains(rendered, "party:") {
|
||||||
|
t.Fatalf("expected inline inputs in rendered prompt, got %q", rendered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreparedRunJSONDoesNotExposeSecretOrTargetPresence(t *testing.T) {
|
||||||
|
const envName = "SCRIPTORIUM_API_KEY"
|
||||||
|
const secret = "public-api-test-secret"
|
||||||
|
t.Setenv(envName, secret)
|
||||||
|
|
||||||
|
engine := newExampleEngine(t)
|
||||||
|
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.structured_events",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.File("./examples/fixtures/transcript.md"),
|
||||||
|
"glossary": scriptorium.File("./examples/fixtures/glossary.yml"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected prepare to succeed, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(prepared)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected prepared run to marshal, got %v", err)
|
||||||
|
}
|
||||||
|
out := string(payload)
|
||||||
|
if strings.Contains(out, secret) {
|
||||||
|
t.Fatalf("prepared run JSON leaked raw API key value: %s", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, envName) {
|
||||||
|
t.Fatalf("prepared run JSON should retain api_key_env name, got %s", out)
|
||||||
|
}
|
||||||
|
for _, forbidden := range []string{"TargetPresence", "target_presence"} {
|
||||||
|
if strings.Contains(out, forbidden) {
|
||||||
|
t.Fatalf("prepared run JSON exposed internal target presence metadata %q: %s", forbidden, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreparePreservesExplicitZeroExecutionOverrides(t *testing.T) {
|
||||||
|
engine := newExampleEngine(t)
|
||||||
|
zeroFloat := 0.0
|
||||||
|
zeroInt := 0
|
||||||
|
|
||||||
|
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.File("./examples/fixtures/transcript.md"),
|
||||||
|
"glossary": scriptorium.File("./examples/fixtures/glossary.yml"),
|
||||||
|
},
|
||||||
|
Execution: &scriptorium.ExecutionTargetOverride{
|
||||||
|
Temperature: &zeroFloat,
|
||||||
|
MaxTokens: &zeroInt,
|
||||||
|
TopP: &zeroFloat,
|
||||||
|
TimeoutSeconds: &zeroInt,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected prepare to succeed, got %v", err)
|
||||||
|
}
|
||||||
|
target := prepared.EffectiveModelParams
|
||||||
|
if target.Temperature != 0 || target.MaxTokens != 0 || target.TopP != 0 || target.TimeoutSeconds != 0 {
|
||||||
|
t.Fatalf("expected explicit zero overrides in effective target, got %+v", target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunSucceedsWithInjectedLLMClient(t *testing.T) {
|
||||||
|
const envName = "SCRIPTORIUM_API_KEY"
|
||||||
|
const secret = "run-secret-value"
|
||||||
|
t.Setenv(envName, secret)
|
||||||
|
|
||||||
|
fake := &fakeLLMClient{
|
||||||
|
response: &scriptorium.GenerateResponse{
|
||||||
|
Content: "# Summary\n\nDone.",
|
||||||
|
Usage: scriptorium.TokenUsage{
|
||||||
|
PromptTokens: 10,
|
||||||
|
CompletionTokens: 5,
|
||||||
|
TotalTokens: 15,
|
||||||
|
CachedTokens: 3,
|
||||||
|
CacheWriteTokens: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake))
|
||||||
|
|
||||||
|
result, err := engine.Run(context.Background(), scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.Inline("Rin opens the gate."),
|
||||||
|
"glossary": scriptorium.Inline("gate: A guarded passage."),
|
||||||
|
},
|
||||||
|
Execution: &scriptorium.ExecutionTargetOverride{
|
||||||
|
APIKeyEnv: envName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected run to succeed, got %v", err)
|
||||||
|
}
|
||||||
|
if result.RunID == "" {
|
||||||
|
t.Fatalf("expected run id")
|
||||||
|
}
|
||||||
|
if result.RawOutput != fake.response.Content {
|
||||||
|
t.Fatalf("unexpected raw output: %q", result.RawOutput)
|
||||||
|
}
|
||||||
|
if string(result.Artifact.Body) != fake.response.Content {
|
||||||
|
t.Fatalf("unexpected artifact body: %q", string(result.Artifact.Body))
|
||||||
|
}
|
||||||
|
if result.Artifact.ContentType != "text/markdown" {
|
||||||
|
t.Fatalf("unexpected artifact content type: %q", result.Artifact.ContentType)
|
||||||
|
}
|
||||||
|
if result.Validation.Status != scriptorium.ValidationPassed || !result.Validation.IsValid {
|
||||||
|
t.Fatalf("expected passed validation, got %+v", result.Validation)
|
||||||
|
}
|
||||||
|
if result.PromptID != "generic.markdown_summary" || result.SelectedProfileID != "local-fast" || result.ModelName != "gpt-4o-mini" {
|
||||||
|
t.Fatalf("unexpected run metadata: %+v", result)
|
||||||
|
}
|
||||||
|
if result.Usage.TotalTokens != 15 || result.Usage.CachedTokens != 3 || result.Usage.CacheWriteTokens != 2 {
|
||||||
|
t.Fatalf("unexpected usage: %+v", result.Usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected run result to marshal, got %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(payload), secret) {
|
||||||
|
t.Fatalf("run result JSON leaked raw API key value: %s", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunPassesPreparedRequestToInjectedLLMClient(t *testing.T) {
|
||||||
|
fake := &fakeLLMClient{
|
||||||
|
response: &scriptorium.GenerateResponse{Content: "ok"},
|
||||||
|
}
|
||||||
|
engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake))
|
||||||
|
zeroFloat := 0.0
|
||||||
|
zeroInt := 0
|
||||||
|
|
||||||
|
_, err := engine.Run(context.Background(), scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.Inline("Rin opens the gate."),
|
||||||
|
"glossary": scriptorium.Inline("gate: A guarded passage."),
|
||||||
|
},
|
||||||
|
Execution: &scriptorium.ExecutionTargetOverride{
|
||||||
|
Temperature: &zeroFloat,
|
||||||
|
MaxTokens: &zeroInt,
|
||||||
|
TopP: &zeroFloat,
|
||||||
|
TimeoutSeconds: &zeroInt,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected run to succeed, got %v", err)
|
||||||
|
}
|
||||||
|
if len(fake.requests) != 1 {
|
||||||
|
t.Fatalf("expected one generate request, got %d", len(fake.requests))
|
||||||
|
}
|
||||||
|
req := fake.requests[0]
|
||||||
|
if len(req.Prompt.Messages) != 2 || !strings.Contains(req.Prompt.Messages[1].Content, "Rin opens the gate.") {
|
||||||
|
t.Fatalf("expected rendered prompt in generate request, got %+v", req.Prompt)
|
||||||
|
}
|
||||||
|
if req.Target.Model != "gpt-4o-mini" || req.Target.Temperature != 0 || req.Target.MaxTokens != 0 || req.Target.TopP != 0 || req.Target.TimeoutSeconds != 0 {
|
||||||
|
t.Fatalf("unexpected effective target: %+v", req.Target)
|
||||||
|
}
|
||||||
|
if !req.TargetPresence.Temperature || !req.TargetPresence.MaxTokens || !req.TargetPresence.TopP || !req.TargetPresence.TimeoutSeconds {
|
||||||
|
t.Fatalf("expected explicit zero target presence, got %+v", req.TargetPresence)
|
||||||
|
}
|
||||||
|
if req.StructuredOutput != nil {
|
||||||
|
t.Fatalf("did not expect structured output for markdown prompt: %+v", req.StructuredOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidationFailureReturnsResult(t *testing.T) {
|
||||||
|
fake := &fakeLLMClient{
|
||||||
|
response: &scriptorium.GenerateResponse{Content: ""},
|
||||||
|
}
|
||||||
|
engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake))
|
||||||
|
|
||||||
|
result, err := engine.Run(context.Background(), scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.Inline("Rin opens the gate."),
|
||||||
|
"glossary": scriptorium.Inline("gate: A guarded passage."),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected validation failure as successful result, got %v", err)
|
||||||
|
}
|
||||||
|
if result.Validation.Status != scriptorium.ValidationFailed || result.Validation.IsValid {
|
||||||
|
t.Fatalf("expected failed validation result, got %+v", result.Validation)
|
||||||
|
}
|
||||||
|
if len(result.Validation.Errors) == 0 {
|
||||||
|
t.Fatalf("expected validation errors")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicErrorsSupportErrorsIs(t *testing.T) {
|
||||||
|
llmErr := errors.New("llm failed")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
req scriptorium.RunRequest
|
||||||
|
client scriptorium.LLMClient
|
||||||
|
schemaDir string
|
||||||
|
want error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid request",
|
||||||
|
req: scriptorium.RunRequest{},
|
||||||
|
client: &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}},
|
||||||
|
want: scriptorium.ErrInvalidRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prompt not found",
|
||||||
|
req: scriptorium.RunRequest{PromptID: "missing.prompt"},
|
||||||
|
client: &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}},
|
||||||
|
want: scriptorium.ErrPromptNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "profile not found",
|
||||||
|
req: scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
ProfileID: "missing-profile",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.Inline("Rin opens the gate."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}},
|
||||||
|
want: scriptorium.ErrProfileNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "artifact load",
|
||||||
|
req: scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.File("./examples/fixtures/does-not-exist.md"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}},
|
||||||
|
want: scriptorium.ErrArtifactLoad,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prompt render",
|
||||||
|
req: scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
},
|
||||||
|
client: &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}},
|
||||||
|
want: scriptorium.ErrPromptRender,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "llm failure",
|
||||||
|
req: scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.Inline("Rin opens the gate."),
|
||||||
|
"glossary": scriptorium.Inline("gate: A guarded passage."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: &fakeLLMClient{err: llmErr},
|
||||||
|
want: scriptorium.ErrLLMGenerate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validation runtime failure",
|
||||||
|
req: scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.structured_events",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.Inline("Rin opens the gate."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: `{"events":[]}`}},
|
||||||
|
schemaDir: t.TempDir(),
|
||||||
|
want: scriptorium.ErrValidation,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv("SCRIPTORIUM_API_KEY", "test-secret")
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
schemaDir := tc.schemaDir
|
||||||
|
if schemaDir == "" {
|
||||||
|
schemaDir = "./examples/schemas"
|
||||||
|
}
|
||||||
|
engine := newExampleEngineWithOptions(t, schemaDir, scriptorium.WithLLMClient(tc.client))
|
||||||
|
_, err := engine.Run(context.Background(), tc.req)
|
||||||
|
if !errors.Is(err, tc.want) {
|
||||||
|
t.Fatalf("expected errors.Is(%v), got %v", tc.want, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithLLMClientRejectsNilClient(t *testing.T) {
|
||||||
|
_, err := scriptorium.NewEngine(exampleConfig("./examples/schemas"), scriptorium.WithLLMClient(nil))
|
||||||
|
if !errors.Is(err, scriptorium.ErrInvalidConfig) {
|
||||||
|
t.Fatalf("expected ErrInvalidConfig, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewEngineConstructsDefaultLLMClientWithoutCredentials(t *testing.T) {
|
||||||
|
if _, err := scriptorium.NewEngine(exampleConfig("./examples/schemas")); err != nil {
|
||||||
|
t.Fatalf("expected default engine construction without credentials to succeed, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExampleEngine(t *testing.T) *scriptorium.Engine {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, path := range []string{
|
||||||
|
"./examples/prompts",
|
||||||
|
"./examples/profiles",
|
||||||
|
"./examples/schemas",
|
||||||
|
} {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
t.Fatalf("expected example path %s to exist: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := scriptorium.NewEngine(exampleConfig("./examples/schemas"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected engine construction to succeed, got %v", err)
|
||||||
|
}
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExampleEngineWithOptions(t *testing.T, schemaDir string, opts ...scriptorium.Option) *scriptorium.Engine {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
engine, err := scriptorium.NewEngine(exampleConfig(schemaDir), opts...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected engine construction to succeed, got %v", err)
|
||||||
|
}
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleConfig(schemaDir string) scriptorium.Config {
|
||||||
|
return scriptorium.Config{
|
||||||
|
PromptDir: "./examples/prompts",
|
||||||
|
ProfileDir: "./examples/profiles",
|
||||||
|
SchemaDir: schemaDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeLLMClient struct {
|
||||||
|
response *scriptorium.GenerateResponse
|
||||||
|
err error
|
||||||
|
requests []scriptorium.GenerateRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeLLMClient) Generate(_ context.Context, req scriptorium.GenerateRequest) (*scriptorium.GenerateResponse, error) {
|
||||||
|
f.requests = append(f.requests, req)
|
||||||
|
if f.err != nil {
|
||||||
|
return nil, f.err
|
||||||
|
}
|
||||||
|
return f.response, nil
|
||||||
|
}
|
||||||
71
errors.go
Normal file
71
errors.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package scriptorium
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/profile"
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/promptdef"
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/usecase"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mapPublicError(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if hasPublicError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
publicErr := publicErrorFor(err)
|
||||||
|
if publicErr == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w: %w", publicErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasPublicError(err error) bool {
|
||||||
|
for _, publicErr := range []error{
|
||||||
|
ErrInvalidConfig,
|
||||||
|
ErrInvalidRequest,
|
||||||
|
ErrPromptNotFound,
|
||||||
|
ErrProfileNotFound,
|
||||||
|
ErrPromptLoad,
|
||||||
|
ErrProfileLoad,
|
||||||
|
ErrArtifactLoad,
|
||||||
|
ErrPromptRender,
|
||||||
|
ErrLLMGenerate,
|
||||||
|
ErrValidation,
|
||||||
|
} {
|
||||||
|
if errors.Is(err, publicErr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func publicErrorFor(err error) error {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, promptdef.ErrPromptDefinitionNotFound):
|
||||||
|
return ErrPromptNotFound
|
||||||
|
case errors.Is(err, profile.ErrProfileNotFound):
|
||||||
|
return ErrProfileNotFound
|
||||||
|
case errors.Is(err, promptdef.ErrInvalidYAML), errors.Is(err, promptdef.ErrInvalidPromptDefinition):
|
||||||
|
return ErrPromptLoad
|
||||||
|
case errors.Is(err, profile.ErrInvalidYAML), errors.Is(err, profile.ErrInvalidProfile):
|
||||||
|
return ErrProfileLoad
|
||||||
|
case errors.Is(err, usecase.ErrArtifactLoad):
|
||||||
|
return ErrArtifactLoad
|
||||||
|
case errors.Is(err, usecase.ErrPromptRender):
|
||||||
|
return ErrPromptRender
|
||||||
|
case errors.Is(err, usecase.ErrLLMGenerate):
|
||||||
|
return ErrLLMGenerate
|
||||||
|
case errors.Is(err, usecase.ErrValidation):
|
||||||
|
return ErrValidation
|
||||||
|
case errors.Is(err, usecase.ErrInvalidRequest):
|
||||||
|
return ErrInvalidRequest
|
||||||
|
case errors.Is(err, usecase.ErrProfileLoad):
|
||||||
|
return ErrPromptLoad
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
50
examples/go-library/prepare/main.go
Normal file
50
examples/go-library/prepare/main.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
engine, err := scriptorium.NewEngine(scriptorium.Config{
|
||||||
|
PromptDir: "./examples/prompts",
|
||||||
|
ProfileDir: "./examples/profiles",
|
||||||
|
SchemaDir: "./examples/schemas",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
|
||||||
|
PromptID: "generic.markdown_summary",
|
||||||
|
Inputs: map[string]scriptorium.ArtifactRef{
|
||||||
|
"transcript": scriptorium.File("./examples/fixtures/transcript.md"),
|
||||||
|
"glossary": scriptorium.File("./examples/fixtures/glossary.yml"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := struct {
|
||||||
|
PromptID string `json:"prompt_id"`
|
||||||
|
SelectedProfileID string `json:"selected_profile_id"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
MessageCount int `json:"message_count"`
|
||||||
|
InputHashes map[string]string `json:"input_hashes"`
|
||||||
|
}{
|
||||||
|
PromptID: prepared.PromptID,
|
||||||
|
SelectedProfileID: prepared.SelectedProfileID,
|
||||||
|
Model: prepared.EffectiveModelParams.Model,
|
||||||
|
MessageCount: len(prepared.Messages),
|
||||||
|
InputHashes: prepared.InputHashes,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(os.Stdout).Encode(summary); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
llm_adapter.go
Normal file
23
llm_adapter.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package scriptorium
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type publicLLMClientAdapter struct {
|
||||||
|
client LLMClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a publicLLMClientAdapter) Generate(ctx context.Context, req domain.GenerateRequest) (*domain.GenerateResponse, error) {
|
||||||
|
resp, err := a.client.Generate(ctx, fromDomainGenerateRequest(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
return nil, fmt.Errorf("%w: llm client returned nil response", ErrLLMGenerate)
|
||||||
|
}
|
||||||
|
return toDomainGenerateResponse(resp), nil
|
||||||
|
}
|
||||||
256
types.go
Normal file
256
types.go
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
package scriptorium
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ArtifactRefType defines how an artifact is referenced.
|
||||||
|
type ArtifactRefType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ArtifactRefInline ArtifactRefType = "inline"
|
||||||
|
ArtifactRefFile ArtifactRefType = "file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OutputFormat defines the desired output format.
|
||||||
|
type OutputFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatText OutputFormat = "text"
|
||||||
|
FormatMarkdown OutputFormat = "markdown"
|
||||||
|
FormatJSON OutputFormat = "json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationMode defines the output validation strategy.
|
||||||
|
type ValidationMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ValidationNone ValidationMode = "none"
|
||||||
|
ValidationBasic ValidationMode = "basic"
|
||||||
|
ValidationJSON ValidationMode = "json"
|
||||||
|
ValidationJSONSchema ValidationMode = "json_schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationStatus defines the result of a validation check.
|
||||||
|
type ValidationStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ValidationPassed ValidationStatus = "passed"
|
||||||
|
ValidationFailed ValidationStatus = "failed"
|
||||||
|
ValidationSkipped ValidationStatus = "skipped"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CacheControlType defines provider cache behavior for prompt content.
|
||||||
|
type CacheControlType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CacheControlEphemeral CacheControlType = "ephemeral"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StructuredOutputType identifies provider-level structured output modes.
|
||||||
|
type StructuredOutputType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StructuredOutputJSONSchema StructuredOutputType = "json_schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunRequest represents a request to prepare or run a single prompt.
|
||||||
|
type RunRequest struct {
|
||||||
|
PromptID string
|
||||||
|
PromptVersion string
|
||||||
|
ProfileID string
|
||||||
|
Inputs map[string]ArtifactRef
|
||||||
|
Vars map[string]string
|
||||||
|
Execution *ExecutionTargetOverride
|
||||||
|
Validation *OutputContract
|
||||||
|
Metadata map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreparedRun contains prepared prompt execution state. It does not include
|
||||||
|
// resolved API key values, model output, validation results, or internal target
|
||||||
|
// presence metadata.
|
||||||
|
type PreparedRun struct {
|
||||||
|
PromptID string `json:"prompt_id"`
|
||||||
|
PromptVersion string `json:"prompt_version,omitempty"`
|
||||||
|
PromptHash string `json:"prompt_hash,omitempty"`
|
||||||
|
SelectedProfileID string `json:"selected_profile_id"`
|
||||||
|
EffectiveModelParams ExecutionTarget `json:"effective_model_params"`
|
||||||
|
OutputContract OutputContract `json:"output_contract"`
|
||||||
|
StructuredOutput *StructuredOutputSpec `json:"structured_output,omitempty"`
|
||||||
|
InputHashes map[string]string `json:"input_hashes,omitempty"`
|
||||||
|
SessionID string `json:"session_id,omitempty"`
|
||||||
|
RenderedPromptHash string `json:"rendered_prompt_hash"`
|
||||||
|
Messages []RenderedMessage `json:"messages"`
|
||||||
|
StartTime time.Time `json:"start_time,omitempty"`
|
||||||
|
EndTime time.Time `json:"end_time,omitempty"`
|
||||||
|
DurationMS int64 `json:"duration_ms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunResult contains generated output, validation state, and run metadata.
|
||||||
|
type RunResult struct {
|
||||||
|
RunID string `json:"run_id"`
|
||||||
|
Artifact Artifact `json:"artifact"`
|
||||||
|
RawOutput string `json:"raw_output"`
|
||||||
|
Validation ValidationResult `json:"validation"`
|
||||||
|
PromptID string `json:"prompt_id"`
|
||||||
|
PromptVersion string `json:"prompt_version,omitempty"`
|
||||||
|
PromptHash string `json:"prompt_hash,omitempty"`
|
||||||
|
RenderedPromptHash string `json:"rendered_prompt_hash"`
|
||||||
|
SelectedProfileID string `json:"selected_profile_id"`
|
||||||
|
ModelName string `json:"model_name"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
EffectiveModelParams ExecutionTarget `json:"effective_model_params"`
|
||||||
|
InputHashes map[string]string `json:"input_hashes,omitempty"`
|
||||||
|
Usage TokenUsage `json:"usage"`
|
||||||
|
StartTime time.Time `json:"start_time,omitempty"`
|
||||||
|
EndTime time.Time `json:"end_time,omitempty"`
|
||||||
|
Duration time.Duration `json:"duration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArtifactRef represents a reference to prompt input content.
|
||||||
|
type ArtifactRef struct {
|
||||||
|
Type ArtifactRefType
|
||||||
|
URI string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Artifact represents loaded artifact content.
|
||||||
|
type Artifact struct {
|
||||||
|
Name string
|
||||||
|
ContentType string
|
||||||
|
Body []byte
|
||||||
|
URI string
|
||||||
|
Size int64
|
||||||
|
Hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionTarget represents effective model runtime settings.
|
||||||
|
type ExecutionTarget struct {
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Temperature float64 `json:"temperature"`
|
||||||
|
MaxTokens int `json:"max_tokens"`
|
||||||
|
TopP float64 `json:"top_p"`
|
||||||
|
TimeoutSeconds int `json:"timeout_seconds"`
|
||||||
|
ServiceTier string `json:"service_tier"`
|
||||||
|
ReasoningEffort string `json:"reasoning_effort"`
|
||||||
|
APIKeyEnv string `json:"api_key_env"`
|
||||||
|
ExtraParams map[string]any `json:"extra_params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionTargetOverride represents per-request runtime setting overrides.
|
||||||
|
type ExecutionTargetOverride struct {
|
||||||
|
Endpoint string
|
||||||
|
Model string
|
||||||
|
Temperature *float64
|
||||||
|
MaxTokens *int
|
||||||
|
TopP *float64
|
||||||
|
TimeoutSeconds *int
|
||||||
|
ServiceTier string
|
||||||
|
ReasoningEffort string
|
||||||
|
APIKeyEnv string
|
||||||
|
ExtraParams map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionTargetPresence tracks which numeric runtime settings were explicit
|
||||||
|
// request overrides.
|
||||||
|
type ExecutionTargetPresence struct {
|
||||||
|
Temperature bool
|
||||||
|
MaxTokens bool
|
||||||
|
TopP bool
|
||||||
|
TimeoutSeconds bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutputContract defines output and validation requirements.
|
||||||
|
type OutputContract struct {
|
||||||
|
Format OutputFormat `json:"format"`
|
||||||
|
ValidationMode ValidationMode `json:"validation_mode"`
|
||||||
|
SchemaPath string `json:"schema_path"`
|
||||||
|
RepairAttempts int `json:"repair_attempts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationResult represents output validation state.
|
||||||
|
type ValidationResult struct {
|
||||||
|
Status ValidationStatus `json:"status"`
|
||||||
|
Mode ValidationMode `json:"mode"`
|
||||||
|
Errors []string `json:"errors,omitempty"`
|
||||||
|
SchemaPath string `json:"schema_path,omitempty"`
|
||||||
|
RepairAttempts int `json:"repair_attempts"`
|
||||||
|
IsValid bool `json:"is_valid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenUsage tracks token consumption.
|
||||||
|
type TokenUsage struct {
|
||||||
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
|
TotalTokens int `json:"total_tokens"`
|
||||||
|
CachedTokens int `json:"cached_tokens"`
|
||||||
|
CacheWriteTokens int `json:"cache_write_tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderedPrompt is the fully rendered prompt passed to an LLM client.
|
||||||
|
type RenderedPrompt struct {
|
||||||
|
SessionID string `json:"session_id,omitempty"`
|
||||||
|
Messages []RenderedMessage `json:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderedMessage is a rendered chat message.
|
||||||
|
type RenderedMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
CacheControl *CacheControl `json:"cache_control,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheControl describes provider cache metadata attached to prompt content.
|
||||||
|
type CacheControl struct {
|
||||||
|
Type CacheControlType `json:"type"`
|
||||||
|
TTL string `json:"ttl,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StructuredOutputSpec describes provider-level structured output.
|
||||||
|
type StructuredOutputSpec struct {
|
||||||
|
Type StructuredOutputType `json:"type"`
|
||||||
|
JSONSchema *StructuredOutputJSONSpec `json:"json_schema,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StructuredOutputJSONSpec contains JSON Schema output constraints.
|
||||||
|
type StructuredOutputJSONSpec struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Strict bool `json:"strict"`
|
||||||
|
Schema any `json:"schema"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMClient executes rendered prompts for Engine.Run.
|
||||||
|
type LLMClient interface {
|
||||||
|
Generate(context.Context, GenerateRequest) (*GenerateResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRequest is passed to an injected LLM client.
|
||||||
|
type GenerateRequest struct {
|
||||||
|
Prompt RenderedPrompt `json:"prompt"`
|
||||||
|
Target ExecutionTarget `json:"target"`
|
||||||
|
TargetPresence ExecutionTargetPresence `json:"target_presence"`
|
||||||
|
StructuredOutput *StructuredOutputSpec `json:"structured_output,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateResponse is returned by an injected LLM client.
|
||||||
|
type GenerateResponse struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Usage TokenUsage `json:"usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// File returns a file-backed artifact reference.
|
||||||
|
func File(path string) ArtifactRef {
|
||||||
|
return ArtifactRef{Type: ArtifactRefFile, URI: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline returns an inline artifact reference.
|
||||||
|
func Inline(body string) ArtifactRef {
|
||||||
|
return ArtifactRef{Type: ArtifactRefInline, Body: body}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InlineWithURI returns an inline artifact reference with URI metadata.
|
||||||
|
func InlineWithURI(uri string, body string) ArtifactRef {
|
||||||
|
return ArtifactRef{Type: ArtifactRefInline, URI: uri, Body: body}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user