mirror of
https://github.com/danielmiessler/fabric
synced 2024-11-08 07:11:06 +00:00
Merge branch 'main' of github.com:danielmiessler/fabric
This commit is contained in:
commit
d56f9f880e
57
.github/workflows/go.yml
vendored
57
.github/workflows/go.yml
vendored
@ -2,14 +2,18 @@ name: Go Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build binaries for Windows, macOS, and Linux
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
@ -20,39 +24,48 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.22.1'
|
||||
go-version-file: ./go.mod
|
||||
|
||||
- name: Determine OS Name
|
||||
id: os-name
|
||||
run: |
|
||||
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
|
||||
echo "OS=linux" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.os }}" == "macos-latest" ]; then
|
||||
echo "OS=darwin" >> $GITHUB_ENV
|
||||
else
|
||||
echo "OS=windows" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Build binary on Linux and macOS
|
||||
if: matrix.os != 'windows-latest'
|
||||
env:
|
||||
GOOS: ${{ env.OS }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
GOOS=${{ matrix.os == 'ubuntu-latest' && 'linux' || 'darwin' }} \
|
||||
GOARCH=${{ matrix.arch }} \
|
||||
go build -o fabric-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }} .
|
||||
go build -o fabric-${OS}-${{ matrix.arch }}-${{ github.ref_name }} .
|
||||
|
||||
- name: Build binary on Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
$env:GOOS = 'windows'
|
||||
$env:GOARCH = '${{ matrix.arch }}'
|
||||
go build -o fabric-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }} .
|
||||
|
||||
- name: Create DMG for macOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
mkdir dist
|
||||
mv fabric-macos-latest-${{ matrix.arch }}-${{ github.ref_name }} dist/fabric
|
||||
hdiutil create dist/fabric-${{ matrix.arch }}.dmg -volname "fabric" -srcfolder dist/fabric
|
||||
mv dist/fabric-${{ matrix.arch }}.dmg fabric-macos-${{ matrix.arch }}-${{ github.ref_name }}.dmg
|
||||
go build -o fabric-${OS}-${{ matrix.arch }}-${{ github.ref_name }} .
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: fabric-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }}
|
||||
path: |
|
||||
fabric-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }}
|
||||
fabric-macos-${{ matrix.arch }}-${{ github.ref_name }}.dmg
|
||||
name: fabric-${{ env.OS }}-${{ matrix.arch }}-${{ github.ref_name }}
|
||||
path: fabric-${{ env.OS }}-${{ matrix.arch }}-${{ github.ref_name }}
|
||||
|
||||
- name: Upload release artifact
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
gh release upload ${{ github.ref_name }} fabric-${{ env.OS }}-${{ matrix.arch }}-${{ github.ref_name }}
|
||||
|
@ -64,7 +64,7 @@ func Cli() (message string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = db.Patterns.LatestPatterns(parsedToInt); err != nil {
|
||||
if err = db.Patterns.PrintLatestPatterns(parsedToInt); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
|
@ -2,7 +2,6 @@ package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"github.com/danielmiessler/fabric/db"
|
||||
)
|
||||
@ -17,13 +16,14 @@ type Chatter struct {
|
||||
}
|
||||
|
||||
func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (message string, err error) {
|
||||
|
||||
var chatRequest *Chat
|
||||
if chatRequest, err = o.NewChat(request); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var messages []*common.Message
|
||||
if messages, err = chatRequest.BuildMessages(); err != nil {
|
||||
var session *db.Session
|
||||
if session, err = chatRequest.BuildChatSession(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (m
|
||||
if o.Stream {
|
||||
channel := make(chan string)
|
||||
go func() {
|
||||
if streamErr := o.vendor.SendStream(messages, opts, channel); streamErr != nil {
|
||||
if streamErr := o.vendor.SendStream(session.Messages, opts, channel); streamErr != nil {
|
||||
channel <- streamErr.Error()
|
||||
}
|
||||
}()
|
||||
@ -44,26 +44,25 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (m
|
||||
fmt.Print(response)
|
||||
}
|
||||
} else {
|
||||
if message, err = o.vendor.Send(messages, opts); err != nil {
|
||||
if message, err = o.vendor.Send(session.Messages, opts); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if chatRequest.Session != nil && message != "" {
|
||||
chatRequest.Session.Append(
|
||||
&common.Message{Role: "system", Content: message},
|
||||
&common.Message{Role: "user", Content: chatRequest.Message})
|
||||
err = chatRequest.Session.Save()
|
||||
chatRequest.Session.Append(&common.Message{Role: "system", Content: message})
|
||||
err = o.db.Sessions.SaveSession(chatRequest.Session)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *Chatter) NewChat(request *common.ChatRequest) (ret *Chat, err error) {
|
||||
|
||||
ret = &Chat{}
|
||||
|
||||
if request.ContextName != "" {
|
||||
var ctx *db.Context
|
||||
if ctx, err = o.db.Contexts.LoadContext(request.ContextName); err != nil {
|
||||
if ctx, err = o.db.Contexts.GetContext(request.ContextName); err != nil {
|
||||
err = fmt.Errorf("could not find context %s: %v", request.ContextName, err)
|
||||
return
|
||||
}
|
||||
@ -72,7 +71,7 @@ func (o *Chatter) NewChat(request *common.ChatRequest) (ret *Chat, err error) {
|
||||
|
||||
if request.SessionName != "" {
|
||||
var sess *db.Session
|
||||
if sess, err = o.db.Sessions.LoadOrCreateSession(request.SessionName); err != nil {
|
||||
if sess, err = o.db.Sessions.GetOrCreateSession(request.SessionName); err != nil {
|
||||
err = fmt.Errorf("could not find session %s: %v", request.SessionName, err)
|
||||
return
|
||||
}
|
||||
@ -81,7 +80,7 @@ func (o *Chatter) NewChat(request *common.ChatRequest) (ret *Chat, err error) {
|
||||
|
||||
if request.PatternName != "" {
|
||||
var pattern *db.Pattern
|
||||
if pattern, err = o.db.Patterns.GetByName(request.PatternName); err != nil {
|
||||
if pattern, err = o.db.Patterns.GetPattern(request.PatternName); err != nil {
|
||||
err = fmt.Errorf("could not find pattern %s: %v", request.PatternName, err)
|
||||
return
|
||||
}
|
||||
|
@ -3,10 +3,6 @@ package core
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"github.com/danielmiessler/fabric/db"
|
||||
@ -16,13 +12,15 @@ import (
|
||||
"github.com/danielmiessler/fabric/vendors/grocq"
|
||||
"github.com/danielmiessler/fabric/vendors/ollama"
|
||||
"github.com/danielmiessler/fabric/vendors/openai"
|
||||
"github.com/danielmiessler/fabric/youtube"
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultPatternsGitRepoUrl = "https://github.com/danielmiessler/fabric.git"
|
||||
DefaultPatternsGitRepoFolder = "patterns"
|
||||
)
|
||||
const DefaultPatternsGitRepoUrl = "https://github.com/danielmiessler/fabric.git"
|
||||
const DefaultPatternsGitRepoFolder = "patterns"
|
||||
|
||||
func NewFabric(db *db.Db) (ret *Fabric, err error) {
|
||||
ret = NewFabricBase(db)
|
||||
@ -38,10 +36,13 @@ func NewFabricForSetup(db *db.Db) (ret *Fabric) {
|
||||
|
||||
// NewFabricBase Create a new Fabric from a list of already configured VendorsController
|
||||
func NewFabricBase(db *db.Db) (ret *Fabric) {
|
||||
|
||||
ret = &Fabric{
|
||||
Db: db,
|
||||
VendorsController: NewVendors(),
|
||||
PatternsLoader: NewPatternsLoader(db.Patterns),
|
||||
VendorsManager: NewVendorsManager(),
|
||||
Db: db,
|
||||
VendorsAll: NewVendorsManager(),
|
||||
PatternsLoader: NewPatternsLoader(db.Patterns),
|
||||
YouTube: youtube.NewYouTube(),
|
||||
}
|
||||
|
||||
label := "Default"
|
||||
@ -55,7 +56,7 @@ func NewFabricBase(db *db.Db) (ret *Fabric) {
|
||||
ret.DefaultModel = ret.AddSetupQuestionCustom("Model", true,
|
||||
"Enter the index the name of your default model")
|
||||
|
||||
ret.AddVendors(openai.NewClient(), azure.NewClient(), ollama.NewClient(), grocq.NewClient(),
|
||||
ret.VendorsAll.AddVendors(openai.NewClient(), azure.NewClient(), ollama.NewClient(), grocq.NewClient(),
|
||||
gemini.NewClient(), anthropic.NewClient())
|
||||
|
||||
return
|
||||
@ -63,8 +64,10 @@ func NewFabricBase(db *db.Db) (ret *Fabric) {
|
||||
|
||||
type Fabric struct {
|
||||
*common.Configurable
|
||||
*VendorsController
|
||||
*VendorsManager
|
||||
VendorsAll *VendorsManager
|
||||
*PatternsLoader
|
||||
*youtube.YouTube
|
||||
|
||||
Db *db.Db
|
||||
|
||||
@ -84,10 +87,12 @@ func (o *Fabric) SaveEnvFile() (err error) {
|
||||
o.Settings.FillEnvFileContent(&envFileContent)
|
||||
o.PatternsLoader.FillEnvFileContent(&envFileContent)
|
||||
|
||||
for _, vendor := range o.Configured {
|
||||
for _, vendor := range o.Vendors {
|
||||
vendor.GetSettings().FillEnvFileContent(&envFileContent)
|
||||
}
|
||||
|
||||
o.YouTube.FillEnvFileContent(&envFileContent)
|
||||
|
||||
err = o.Db.SaveEnv(envFileContent.String())
|
||||
return
|
||||
}
|
||||
@ -101,6 +106,10 @@ func (o *Fabric) Setup() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = o.YouTube.Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = o.PatternsLoader.Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
@ -126,7 +135,7 @@ func (o *Fabric) SetupDefaultModel() (err error) {
|
||||
o.DefaultVendor.Value = vendorsModels.FindVendorsByModelFirst(o.DefaultModel.Value)
|
||||
}
|
||||
|
||||
// verify
|
||||
//verify
|
||||
vendorNames := vendorsModels.FindVendorsByModel(o.DefaultModel.Value)
|
||||
if len(vendorNames) == 0 {
|
||||
err = errors.Errorf("You need to chose an available default model.")
|
||||
@ -143,19 +152,19 @@ func (o *Fabric) SetupDefaultModel() (err error) {
|
||||
}
|
||||
|
||||
func (o *Fabric) SetupVendors() (err error) {
|
||||
o.ResetConfigured()
|
||||
o.Reset()
|
||||
|
||||
for _, vendor := range o.All {
|
||||
for _, vendor := range o.VendorsAll.Vendors {
|
||||
fmt.Println()
|
||||
if vendorErr := vendor.Setup(); vendorErr == nil {
|
||||
fmt.Printf("[%v] configured\n", vendor.GetName())
|
||||
o.AddVendorConfigured(vendor)
|
||||
o.AddVendors(vendor)
|
||||
} else {
|
||||
fmt.Printf("[%v] skiped\n", vendor.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
if !o.HasConfiguredVendors() {
|
||||
if !o.HasVendors() {
|
||||
err = errors.New("No vendors configured")
|
||||
return
|
||||
}
|
||||
@ -167,12 +176,17 @@ func (o *Fabric) SetupVendors() (err error) {
|
||||
|
||||
// Configure buildClient VendorsController based on the environment variables
|
||||
func (o *Fabric) configure() (err error) {
|
||||
for _, vendor := range o.All {
|
||||
for _, vendor := range o.VendorsAll.Vendors {
|
||||
if vendorErr := vendor.Configure(); vendorErr == nil {
|
||||
o.AddVendorConfigured(vendor)
|
||||
o.AddVendors(vendor)
|
||||
}
|
||||
}
|
||||
err = o.PatternsLoader.Configure()
|
||||
if err = o.PatternsLoader.Configure(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = o.YouTube.Configure()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -219,23 +233,27 @@ func (o *Fabric) CreateOutputFile(message string, fileName string) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (o *Chat) BuildMessages() (ret []*common.Message, err error) {
|
||||
if o.Session != nil && len(o.Session.Messages) > 0 {
|
||||
ret = append(ret, o.Session.Messages...)
|
||||
func (o *Chat) BuildChatSession() (ret *db.Session, err error) {
|
||||
// new messages will be appended to the session and used to send the message
|
||||
if o.Session != nil {
|
||||
ret = o.Session
|
||||
} else {
|
||||
ret = &db.Session{}
|
||||
}
|
||||
|
||||
systemMessage := strings.TrimSpace(o.Context) + strings.TrimSpace(o.Pattern)
|
||||
|
||||
if systemMessage != "" {
|
||||
ret = append(ret, &common.Message{Role: "system", Content: systemMessage})
|
||||
ret.Append(&common.Message{Role: "system", Content: systemMessage})
|
||||
}
|
||||
|
||||
userMessage := strings.TrimSpace(o.Message)
|
||||
if userMessage != "" {
|
||||
ret = append(ret, &common.Message{Role: "user", Content: userMessage})
|
||||
ret.Append(&common.Message{Role: "user", Content: userMessage})
|
||||
}
|
||||
|
||||
if ret == nil {
|
||||
if ret.IsEmpty() {
|
||||
ret = nil
|
||||
err = fmt.Errorf("no session, pattern or user messages provided")
|
||||
}
|
||||
return
|
||||
|
135
core/vendors.go
135
core/vendors.go
@ -1,108 +1,97 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewVendors() (ret *VendorsController) {
|
||||
ret = &VendorsController{
|
||||
All: map[string]common.Vendor{},
|
||||
Configured: map[string]common.Vendor{},
|
||||
func NewVendorsManager() *VendorsManager {
|
||||
return &VendorsManager{
|
||||
Vendors: map[string]common.Vendor{},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type VendorsController struct {
|
||||
All map[string]common.Vendor
|
||||
Configured map[string]common.Vendor
|
||||
|
||||
Models *VendorsModels
|
||||
type VendorsManager struct {
|
||||
Vendors map[string]common.Vendor
|
||||
Models *VendorsModels
|
||||
}
|
||||
|
||||
func (o *VendorsController) AddVendors(vendors ...common.Vendor) {
|
||||
func (o *VendorsManager) AddVendors(vendors ...common.Vendor) {
|
||||
for _, vendor := range vendors {
|
||||
o.All[vendor.GetName()] = vendor
|
||||
o.Vendors[vendor.GetName()] = vendor
|
||||
}
|
||||
}
|
||||
|
||||
func (o *VendorsController) AddVendorConfigured(vendor common.Vendor) {
|
||||
o.Configured[vendor.GetName()] = vendor
|
||||
}
|
||||
|
||||
func (o *VendorsController) ResetConfigured() {
|
||||
o.Configured = map[string]common.Vendor{}
|
||||
func (o *VendorsManager) Reset() {
|
||||
o.Vendors = map[string]common.Vendor{}
|
||||
o.Models = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (o *VendorsController) GetModels() (ret *VendorsModels) {
|
||||
func (o *VendorsManager) GetModels() *VendorsModels {
|
||||
if o.Models == nil {
|
||||
o.readModels()
|
||||
}
|
||||
ret = o.Models
|
||||
return
|
||||
return o.Models
|
||||
}
|
||||
|
||||
func (o *VendorsController) HasConfiguredVendors() bool {
|
||||
return len(o.Configured) > 0
|
||||
func (o *VendorsManager) HasVendors() bool {
|
||||
return len(o.Vendors) > 0
|
||||
}
|
||||
|
||||
func (o *VendorsController) readModels() {
|
||||
func (o *VendorsManager) FindByName(name string) common.Vendor {
|
||||
return o.Vendors[name]
|
||||
}
|
||||
|
||||
func (o *VendorsManager) readModels() {
|
||||
o.Models = NewVendorsModels()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var channels []ChannelName
|
||||
resultsChan := make(chan modelResult, len(o.Vendors))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
errorsChan := make(chan error, 3)
|
||||
|
||||
for _, vendor := range o.Configured {
|
||||
// For each vendor:
|
||||
// - Create a channel to collect output from the vendor model's list
|
||||
// - Create a goroutine to query the vendor on its model
|
||||
cn := ChannelName{channel: make(chan []string, 1), name: vendor.GetName()}
|
||||
channels = append(channels, cn)
|
||||
o.createGoroutine(&wg, vendor, cn, errorsChan)
|
||||
for _, vendor := range o.Vendors {
|
||||
wg.Add(1)
|
||||
go o.fetchVendorModels(ctx, &wg, vendor, resultsChan)
|
||||
}
|
||||
|
||||
// Let's wait for completion
|
||||
wg.Wait() // Wait for all goroutines to finish
|
||||
close(errorsChan)
|
||||
|
||||
for err := range errorsChan {
|
||||
fmt.Println(err)
|
||||
o.Models.AddError(err)
|
||||
}
|
||||
|
||||
// And collect output
|
||||
for _, cn := range channels {
|
||||
models := <-cn.channel
|
||||
if models != nil {
|
||||
o.Models.AddVendorModels(cn.name, models)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *VendorsController) FindByName(name string) (ret common.Vendor) {
|
||||
ret = o.Configured[name]
|
||||
return
|
||||
}
|
||||
|
||||
// Create a goroutine to list models for the given vendor
|
||||
func (o *VendorsController) createGoroutine(wg *sync.WaitGroup, vendor common.Vendor, cn ChannelName, errorsChan chan error) {
|
||||
wg.Add(1)
|
||||
|
||||
// Wait for all goroutines to finish
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
models, err := vendor.ListModels()
|
||||
if err != nil {
|
||||
errorsChan <- err
|
||||
cn.channel <- nil
|
||||
} else {
|
||||
cn.channel <- models
|
||||
}
|
||||
wg.Wait()
|
||||
close(resultsChan)
|
||||
}()
|
||||
|
||||
// Collect results
|
||||
for result := range resultsChan {
|
||||
if result.err != nil {
|
||||
fmt.Println(result.vendorName, result.err)
|
||||
o.Models.AddError(result.err)
|
||||
cancel() // Cancel remaining goroutines if needed
|
||||
} else {
|
||||
o.Models.AddVendorModels(result.vendorName, result.models)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *VendorsManager) fetchVendorModels(
|
||||
ctx context.Context, wg *sync.WaitGroup, vendor common.Vendor, resultsChan chan<- modelResult) {
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
models, err := vendor.ListModels()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Context canceled, don't send the result
|
||||
return
|
||||
case resultsChan <- modelResult{vendorName: vendor.GetName(), models: models, err: err}:
|
||||
// Result sent
|
||||
}
|
||||
}
|
||||
|
||||
type modelResult struct {
|
||||
vendorName string
|
||||
models []string
|
||||
err error
|
||||
}
|
||||
|
@ -1,19 +1,13 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type Contexts struct {
|
||||
*Storage
|
||||
}
|
||||
|
||||
// LoadContext Load a context from file
|
||||
func (o *Contexts) LoadContext(name string) (ret *Context, err error) {
|
||||
path := o.BuildFilePathByName(name)
|
||||
|
||||
// GetContext Load a context from file
|
||||
func (o *Contexts) GetContext(name string) (ret *Context, err error) {
|
||||
var content []byte
|
||||
if content, err = os.ReadFile(path); err != nil {
|
||||
if content, err = o.Load(name); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -24,12 +18,4 @@ func (o *Contexts) LoadContext(name string) (ret *Context, err error) {
|
||||
type Context struct {
|
||||
Name string
|
||||
Content string
|
||||
|
||||
contexts *Contexts
|
||||
}
|
||||
|
||||
// Save the session on disk
|
||||
func (o *Context) Save() (err error) {
|
||||
err = o.contexts.Save(o.Name, []byte(o.Content))
|
||||
return err
|
||||
}
|
||||
|
8
db/db.go
8
db/db.go
@ -19,8 +19,12 @@ func NewDb(dir string) (db *Db) {
|
||||
SystemPatternFile: "system.md",
|
||||
UniquePatternsFilePath: db.FilePath("unique_patterns.txt"),
|
||||
}
|
||||
db.Sessions = &Sessions{&Storage{Label: "Sessions", Dir: db.FilePath("sessions")}}
|
||||
db.Contexts = &Contexts{&Storage{Label: "Contexts", Dir: db.FilePath("contexts")}}
|
||||
|
||||
db.Sessions = &Sessions{
|
||||
&Storage{Label: "Sessions", Dir: db.FilePath("sessions"), FileExtension: ".json"}}
|
||||
|
||||
db.Contexts = &Contexts{
|
||||
&Storage{Label: "Contexts", Dir: db.FilePath("contexts")}}
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ type Patterns struct {
|
||||
UniquePatternsFilePath string
|
||||
}
|
||||
|
||||
// GetByName finds a pattern by name and returns the pattern as an entry or an error
|
||||
func (o *Patterns) GetByName(name string) (ret *Pattern, err error) {
|
||||
// GetPattern finds a pattern by name and returns the pattern as an entry or an error
|
||||
func (o *Patterns) GetPattern(name string) (ret *Pattern, err error) {
|
||||
patternPath := filepath.Join(o.Dir, name, o.SystemPatternFile)
|
||||
|
||||
var pattern []byte
|
||||
@ -28,7 +28,7 @@ func (o *Patterns) GetByName(name string) (ret *Pattern, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (o *Patterns) LatestPatterns(latestNumber int) (err error) {
|
||||
func (o *Patterns) PrintLatestPatterns(latestNumber int) (err error) {
|
||||
var contents []byte
|
||||
if contents, err = os.ReadFile(o.UniquePatternsFilePath); err != nil {
|
||||
err = fmt.Errorf("could not read unique patterns file. Pleas run --updatepatterns (%s)", err)
|
||||
|
@ -1,11 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
)
|
||||
|
||||
@ -13,56 +9,30 @@ type Sessions struct {
|
||||
*Storage
|
||||
}
|
||||
|
||||
func (o *Sessions) LoadOrCreateSession(name string) (ret *Session, err error) {
|
||||
if name == "" {
|
||||
return &Session{}, nil
|
||||
}
|
||||
func (o *Sessions) GetOrCreateSession(name string) (session *Session, err error) {
|
||||
session = &Session{Name: name}
|
||||
|
||||
path := o.BuildFilePath(name)
|
||||
if _, statErr := os.Stat(path); errors.Is(statErr, os.ErrNotExist) {
|
||||
fmt.Printf("Creating new session: %s\n", name)
|
||||
ret = &Session{Name: name, sessions: o}
|
||||
if o.Exists(name) {
|
||||
err = o.LoadAsJson(name, &session.Messages)
|
||||
} else {
|
||||
ret, err = o.loadSession(name)
|
||||
fmt.Printf("Creating new session: %s\n", name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadSession Load a session from file
|
||||
func (o *Sessions) LoadSession(name string) (ret *Session, err error) {
|
||||
if name == "" {
|
||||
return &Session{}, nil
|
||||
}
|
||||
ret, err = o.loadSession(name)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *Sessions) loadSession(name string) (ret *Session, err error) {
|
||||
ret = &Session{Name: name, sessions: o}
|
||||
if err = o.LoadAsJson(name, &ret.Messages); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
func (o *Sessions) SaveSession(session *Session) (err error) {
|
||||
return o.SaveAsJson(session.Name, session.Messages)
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Name string
|
||||
Messages []*common.Message
|
||||
}
|
||||
|
||||
sessions *Sessions
|
||||
func (o *Session) IsEmpty() bool {
|
||||
return len(o.Messages) == 0
|
||||
}
|
||||
|
||||
func (o *Session) Append(messages ...*common.Message) {
|
||||
o.Messages = append(o.Messages, messages...)
|
||||
}
|
||||
|
||||
// Save the session on disk
|
||||
func (o *Session) Save() (err error) {
|
||||
var jsonBytes []byte
|
||||
if jsonBytes, err = json.Marshal(o.Messages); err == nil {
|
||||
err = o.sessions.Save(o.Name, jsonBytes)
|
||||
} else {
|
||||
err = fmt.Errorf("could not marshal session %o: %o", o.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -6,13 +6,14 @@ import (
|
||||
"github.com/samber/lo"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
Label string
|
||||
Dir string
|
||||
ItemIsDir bool
|
||||
ItemExtension string
|
||||
FileExtension string
|
||||
}
|
||||
|
||||
func (o *Storage) Configure() (err error) {
|
||||
@ -38,12 +39,21 @@ func (o *Storage) GetNames() (ret []string, err error) {
|
||||
return
|
||||
})
|
||||
} else {
|
||||
ret = lo.FilterMap(entries, func(item os.DirEntry, index int) (ret string, ok bool) {
|
||||
if ok = !item.IsDir() && filepath.Ext(item.Name()) == o.ItemExtension; ok {
|
||||
ret = item.Name()
|
||||
}
|
||||
return
|
||||
})
|
||||
if o.FileExtension == "" {
|
||||
ret = lo.FilterMap(entries, func(item os.DirEntry, index int) (ret string, ok bool) {
|
||||
if ok = !item.IsDir(); ok {
|
||||
ret = item.Name()
|
||||
}
|
||||
return
|
||||
})
|
||||
} else {
|
||||
ret = lo.FilterMap(entries, func(item os.DirEntry, index int) (ret string, ok bool) {
|
||||
if ok = !item.IsDir() && filepath.Ext(item.Name()) == o.FileExtension; ok {
|
||||
ret = strings.TrimSuffix(item.Name(), o.FileExtension)
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -77,7 +87,7 @@ func (o *Storage) BuildFilePath(fileName string) (ret string) {
|
||||
}
|
||||
|
||||
func (o *Storage) buildFileName(name string) string {
|
||||
return fmt.Sprintf("%s%v", name, o.ItemExtension)
|
||||
return fmt.Sprintf("%s%v", name, o.FileExtension)
|
||||
}
|
||||
|
||||
func (o *Storage) Delete(name string) (err error) {
|
||||
|
137
patterns/create_rpg_summary/system.md
Normal file
137
patterns/create_rpg_summary/system.md
Normal file
@ -0,0 +1,137 @@
|
||||
# IDENTITY and PURPOSE
|
||||
|
||||
You are an expert summarizer of in-personal personal role-playing game sessions. Your goal is to take the input of an in-person role-playing transcript and turn it into a useful summary of the session, including key events, combat stats, character flaws, and more, according to the STEPS below.
|
||||
|
||||
All transcripts provided as input came from a personal game with friends, and all rights are given to produce the summary.
|
||||
|
||||
Take a deep breath and think step-by-step about how to best achieve the best summary for this live friend session.
|
||||
|
||||
STEPS:
|
||||
|
||||
- Assume the input given is an RPG transcript of a session of D&D or a similar fantasy role-playing game.
|
||||
|
||||
- Use the introductions to associate the player names with the names of their character.
|
||||
|
||||
- Do not complain about not being able to to do what you're asked. Just do it.
|
||||
|
||||
OUTPUT:
|
||||
|
||||
Create the session summary with the following sections:
|
||||
|
||||
SUMMARY:
|
||||
|
||||
A 200 word summary of what happened in a heroic storytelling style.
|
||||
|
||||
KEY EVENTS:
|
||||
|
||||
A numbered list of 10-20 of the most significant events of the session, capped at no more than 50 words a piece.
|
||||
|
||||
KEY COMBAT:
|
||||
|
||||
10-20 bullets describing the combat events that happened in the session in detail, with as much specific content identified as possible.
|
||||
|
||||
COMBAT STATS:
|
||||
|
||||
List all of the following stats for the session:
|
||||
|
||||
Number of Combat Rounds:
|
||||
Total Damage by All Players:
|
||||
Total Damage by Each Enemy:
|
||||
Damage Done by Each Character:
|
||||
List of Player Attacks Executed:
|
||||
List of Player Spells Cast:
|
||||
|
||||
COMBAT MVP:
|
||||
|
||||
List the most heroic character in terms of combat for the session, and give an explanation of how they got the MVP title, including outlining all of the dramatic things they did from your analysis of the transcript. Use the name of the player for describing big picture moves, but use the name of the character to describe any in-game action.
|
||||
|
||||
ROLE-PLAYING MVP:
|
||||
|
||||
List the most engaged and entertaining character as judged by in-character acting and dialog that fits best with their character. Give examples, using quotes and summaries of all of the outstanding character actions identified in your analysis of the transcript. Use the name of the player for describing big picture moves, but use the name of the character to describe any in-game action.
|
||||
|
||||
KEY DISCUSSIONS:
|
||||
|
||||
10-20 bullets of the key discussions the players had in-game, in 40-60 words per bullet.
|
||||
|
||||
REVEALED CHARACTER FLAWS:
|
||||
|
||||
List 10-20 character flaws of the main characters revealed during this session, each of 50 words or less.
|
||||
|
||||
KEY CHARACTER CHANGES:
|
||||
|
||||
Give 10-20 bullets of key changes that happened to each character, how it shows they're evolving and adapting to events in the world.
|
||||
|
||||
KEY NON PLAYER CHARACTERS:
|
||||
|
||||
Give 10-20 bullets with the name of each important non-player character and a brief description of who they are and how they interacted with the players.
|
||||
|
||||
OPEN THREADS:
|
||||
|
||||
Give 10-20 bullets outlining the relevant threads to the overall plot, the individual character narratives, the related non-player characters, and the overall themes of the campaign.
|
||||
|
||||
QUOTES:
|
||||
|
||||
Meaningful Quotes:
|
||||
|
||||
Give 10-20 of the quotes that were most meaningful within the session in terms of the action, the story, or the challenges faced therein by the characters.
|
||||
|
||||
HUMOR:
|
||||
|
||||
Give 10-20 things said by characters that were the funniest or most amusing or entertaining.
|
||||
|
||||
4TH WALL:
|
||||
|
||||
Give 10-15 of the most entertaining comments about the game from the transcript made by the players, but not their characters.
|
||||
|
||||
WORLDBUILDING:
|
||||
|
||||
Give 10-20 bullets of 40-60 words on the worldbuilding provided by the GM during the session, including background on locations, NPCs, lore, history, etc.
|
||||
|
||||
PREVIOUSLY ON:
|
||||
|
||||
Give a "Previously On" explanation of this session that mimics TV shows from the 1980's, but with a fantasy feel appropriate for D&D. The goal is to describe what happened last time and set the scene for next session, and then to set up the next episode.
|
||||
|
||||
Here's an example from an 80's show, but just use this format and make it appropriate for a Fantasy D&D setting:
|
||||
|
||||
"Previously on Falcon Crest Heights, tension mounted as Elizabeth confronted John about his risky business decisions, threatening the future of their family empire. Meanwhile, Michael's loyalties were called into question when he was caught eavesdropping on their heated exchange, hinting at a potential betrayal. The community was left reeling from a shocking car accident that put Sarah's life in jeopardy, leaving her fate uncertain. Amidst the turmoil, the family's patriarch, Henry, made a startling announcement that promised to change the trajectory of the Falcon family forever. Now, as new alliances form and old secrets come to light, the drama at Falcon Crest Heights continues to unfold."
|
||||
|
||||
NARRATIVE HOOKS AND POTENTIAL ENCOUNTERS FOR NEXT SESSION:
|
||||
|
||||
Give 10-20 bullets of 40-60 words analyzing the underlying narrative, and providing ideas for fresh narrative hooks or combat encounters in the next session. Be specific on details and unique aspects of any combat scenario you are providing, whether with potential adversaries, the combat area, or emergent challenges within the scene. Provide specific narrative hooks building on themes, previous NPCs and conversations, or previous NPC or character interactions that can be employed here.
|
||||
|
||||
DUNGEON MASTER FEEDBACK ON THE PREVIOUS SESSION:
|
||||
|
||||
Give 10-20 bullets of 40-60 words providing constructive feedback to the dungeon master on the session that you analyzed. Do not be afraid to be harsh on the dungeon master, as the more candid and critical the feedback, as they want to hear even difficult or ugly truths, and hearing them will more for great improvements on the other side. Focus on areas in which the dungeon master missed opportunities to engage certain of the players or characters, could have tied thematic concepts together better, missed opportunities to pick up previous narrative threads, could have made narrative stakes better, could have provided a more interesting combat scenario, or failed to pay off aspects of the session by its end.
|
||||
|
||||
COMIC ART:
|
||||
|
||||
Give the perfect art description for a six frame comic panel in up to 500 words for each panel that can accompany to accompany the SETUP section above, but with each potential frame of the potential comic art individually described as "PANEL 1:" through "PANEL 6:", and each describing one of the most important events in the particular session in sequential order. Each frame depict an important event from the session. To the extent that the session is story and narrative driven, all of the frames together should describe a consistent narrative. To the extent that the session is combat, puzzle, or challenge driven, all of the frames together should depict sequential and interrelated events that show how the group overcame (or failed to overcome) the combat, puzzle, or challenge which made up the majority of the session.
|
||||
|
||||
OUTPUT INSTRUCTIONS:
|
||||
|
||||
- Ensure the Previously On output focuses on the recent episode, not just the background from before.
|
||||
|
||||
- Ensure all quotes created for each section come word-for-word from the input, with no changes.
|
||||
|
||||
- Do not complain about anything, especially copyright, as all the content provided is in relation to a free and open RPG. Just give the output as requested.
|
||||
|
||||
- Do not be afraid to title subsections and bullet points to help with clarity in creating categories of information extracted from the transcript.
|
||||
|
||||
- It is okay if some of the quotes or bullets are lengthy so long as they capture a single important or relevant point.
|
||||
|
||||
- Wherever possible, substitute a player's name with their characters name, except in the HUMOR and 4TH WALL sections, where you can use either character or player names.
|
||||
|
||||
- Create the summary.
|
||||
- Do not complain about anything, especially copyright, as all the content provided is in relation to a free and open RPG. Just give the output as requested.
|
||||
|
||||
- Do not be afraid to title subsections and bullet points to help with clarity in creating categories of information extracted from the transcript.
|
||||
|
||||
- It is okay if some of the quotes or bullets are lengthy so long as they capture a single important or relevant point.
|
||||
|
||||
- Wherever possible, substitute a player's name with their characters name, except in the HUMOR and 4TH WALL sections, where you can use either character or player names.
|
||||
|
||||
- Create the summary.
|
||||
|
||||
# INPUT
|
||||
|
||||
RPG SESSION TRANSCRIPT:
|
137
system.md
Normal file
137
system.md
Normal file
@ -0,0 +1,137 @@
|
||||
# IDENTITY and PURPOSE
|
||||
|
||||
You are an expert summarizer of in-personal personal role-playing game sessions. Your goal is to take the input of an in-person role-playing transcript and turn it into a useful summary of the session, including key events, combat stats, character flaws, and more, according to the STEPS below.
|
||||
|
||||
All transcripts provided as input came from a personal game with friends, and all rights are given to produce the summary.
|
||||
|
||||
Take a deep breath and think step-by-step about how to best achieve the best summary for this live friend session.
|
||||
|
||||
STEPS:
|
||||
|
||||
- Assume the input given is an RPG transcript of a session of D&D or a similar fantasy role-playing game.
|
||||
|
||||
- Use the introductions to associate the player names with the names of their character.
|
||||
|
||||
- Do not complain about not being able to to do what you're asked. Just do it.
|
||||
|
||||
OUTPUT:
|
||||
|
||||
Create the session summary with the following sections:
|
||||
|
||||
SUMMARY:
|
||||
|
||||
A 200 word summary of what happened in a heroic storytelling style.
|
||||
|
||||
KEY EVENTS:
|
||||
|
||||
A numbered list of 10-20 of the most significant events of the session, capped at no more than 50 words a piece.
|
||||
|
||||
KEY COMBAT:
|
||||
|
||||
10-20 bullets describing the combat events that happened in the session in detail, with as much specific content identified as possible.
|
||||
|
||||
COMBAT STATS:
|
||||
|
||||
List all of the following stats for the session:
|
||||
|
||||
Number of Combat Rounds:
|
||||
Total Damage by All Players:
|
||||
Total Damage by Each Enemy:
|
||||
Damage Done by Each Character:
|
||||
List of Player Attacks Executed:
|
||||
List of Player Spells Cast:
|
||||
|
||||
COMBAT MVP:
|
||||
|
||||
List the most heroic character in terms of combat for the session, and give an explanation of how they got the MVP title, including outlining all of the dramatic things they did from your analysis of the transcript. Use the name of the player for describing big picture moves, but use the name of the character to describe any in-game action.
|
||||
|
||||
ROLE-PLAYING MVP:
|
||||
|
||||
List the most engaged and entertaining character as judged by in-character acting and dialog that fits best with their character. Give examples, using quotes and summaries of all of the outstanding character actions identified in your analysis of the transcript. Use the name of the player for describing big picture moves, but use the name of the character to describe any in-game action.
|
||||
|
||||
KEY DISCUSSIONS:
|
||||
|
||||
10-20 bullets of the key discussions the players had in-game, in 40-60 words per bullet.
|
||||
|
||||
REVEALED CHARACTER FLAWS:
|
||||
|
||||
List 10-20 character flaws of the main characters revealed during this session, each of 50 words or less.
|
||||
|
||||
KEY CHARACTER CHANGES:
|
||||
|
||||
Give 10-20 bullets of key changes that happened to each character, how it shows they're evolving and adapting to events in the world.
|
||||
|
||||
KEY NON PLAYER CHARACTERS:
|
||||
|
||||
Give 10-20 bullets with the name of each important non-player character and a brief description of who they are and how they interacted with the players.
|
||||
|
||||
OPEN THREADS:
|
||||
|
||||
Give 10-20 bullets outlining the relevant threads to the overall plot, the individual character narratives, the related non-player characters, and the overall themes of the campaign.
|
||||
|
||||
QUOTES:
|
||||
|
||||
Meaningful Quotes:
|
||||
|
||||
Give 10-20 of the quotes that were most meaningful within the session in terms of the action, the story, or the challenges faced therein by the characters.
|
||||
|
||||
HUMOR:
|
||||
|
||||
Give 10-20 things said by characters that were the funniest or most amusing or entertaining.
|
||||
|
||||
4TH WALL:
|
||||
|
||||
Give 10-15 of the most entertaining comments about the game from the transcript made by the players, but not their characters.
|
||||
|
||||
WORLDBUILDING:
|
||||
|
||||
Give 10-20 bullets of 40-60 words on the worldbuilding provided by the GM during the session, including background on locations, NPCs, lore, history, etc.
|
||||
|
||||
PREVIOUSLY ON:
|
||||
|
||||
Give a "Previously On" explanation of this session that mimics TV shows from the 1980's, but with a fantasy feel appropriate for D&D. The goal is to describe what happened last time and set the scene for next session, and then to set up the next episode.
|
||||
|
||||
Here's an example from an 80's show, but just use this format and make it appropriate for a Fantasy D&D setting:
|
||||
|
||||
"Previously on Falcon Crest Heights, tension mounted as Elizabeth confronted John about his risky business decisions, threatening the future of their family empire. Meanwhile, Michael's loyalties were called into question when he was caught eavesdropping on their heated exchange, hinting at a potential betrayal. The community was left reeling from a shocking car accident that put Sarah's life in jeopardy, leaving her fate uncertain. Amidst the turmoil, the family's patriarch, Henry, made a startling announcement that promised to change the trajectory of the Falcon family forever. Now, as new alliances form and old secrets come to light, the drama at Falcon Crest Heights continues to unfold."
|
||||
|
||||
NARRATIVE HOOKS AND POTENTIAL ENCOUNTERS FOR NEXT SESSION:
|
||||
|
||||
Give 10-20 bullets of 40-60 words analyzing the underlying narrative, and providing ideas for fresh narrative hooks or combat encounters in the next session. Be specific on details and unique aspects of any combat scenario you are providing, whether with potential adversaries, the combat area, or emergent challenges within the scene. Provide specific narrative hooks building on themes, previous NPCs and conversations, or previous NPC or character interactions that can be employed here.
|
||||
|
||||
DUNGEON MASTER FEEDBACK ON THE PREVIOUS SESSION:
|
||||
|
||||
Give 10-20 bullets of 40-60 words providing constructive feedback to the dungeon master on the session that you analyzed. Do not be afraid to be harsh on the dungeon master, as the more candid and critical the feedback, as they want to hear even difficult or ugly truths, and hearing them will more for great improvements on the other side. Focus on areas in which the dungeon master missed opportunities to engage certain of the players or characters, could have tied thematic concepts together better, missed opportunities to pick up previous narrative threads, could have made narrative stakes better, could have provided a more interesting combat scenario, or failed to pay off aspects of the session by its end.
|
||||
|
||||
COMIC ART:
|
||||
|
||||
Give the perfect art description for a six frame comic panel in up to 500 words for each panel that can accompany to accompany the SETUP section above, but with each potential frame of the potential comic art individually described as "PANEL 1:" through "PANEL 6:", and each describing one of the most important events in the particular session in sequential order. Each frame depict an important event from the session. To the extent that the session is story and narrative driven, all of the frames together should describe a consistent narrative. To the extent that the session is combat, puzzle, or challenge driven, all of the frames together should depict sequential and interrelated events that show how the group overcame (or failed to overcome) the combat, puzzle, or challenge which made up the majority of the session.
|
||||
|
||||
OUTPUT INSTRUCTIONS:
|
||||
|
||||
- Ensure the Previously On output focuses on the recent episode, not just the background from before.
|
||||
|
||||
- Ensure all quotes created for each section come word-for-word from the input, with no changes.
|
||||
|
||||
- Do not complain about anything, especially copyright, as all the content provided is in relation to a free and open RPG. Just give the output as requested.
|
||||
|
||||
- Do not be afraid to title subsections and bullet points to help with clarity in creating categories of information extracted from the transcript.
|
||||
|
||||
- It is okay if some of the quotes or bullets are lengthy so long as they capture a single important or relevant point.
|
||||
|
||||
- Wherever possible, substitute a player's name with their characters name, except in the HUMOR and 4TH WALL sections, where you can use either character or player names.
|
||||
|
||||
- Create the summary.
|
||||
- Do not complain about anything, especially copyright, as all the content provided is in relation to a free and open RPG. Just give the output as requested.
|
||||
|
||||
- Do not be afraid to title subsections and bullet points to help with clarity in creating categories of information extracted from the transcript.
|
||||
|
||||
- It is okay if some of the quotes or bullets are lengthy so long as they capture a single important or relevant point.
|
||||
|
||||
- Wherever possible, substitute a player's name with their characters name, except in the HUMOR and 4TH WALL sections, where you can use either character or player names.
|
||||
|
||||
- Create the summary.
|
||||
|
||||
# INPUT
|
||||
|
||||
RPG SESSION TRANSCRIPT:
|
1
vendors/anthropic/anthropic.go
vendored
1
vendors/anthropic/anthropic.go
vendored
@ -74,7 +74,6 @@ func (an *Client) SendStream(
|
||||
fmt.Printf("Messages stream error: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
// TODO why closing the channel here? It was opened in the parent method, so it should be closed there
|
||||
close(channel)
|
||||
}
|
||||
return
|
||||
|
88
vendors/gemini/gemini.go
vendored
88
vendors/gemini/gemini.go
vendored
@ -3,6 +3,8 @@ package gemini
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"github.com/google/generative-ai-go/genai"
|
||||
@ -10,6 +12,8 @@ import (
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
const modelsNamePrefix = "models/"
|
||||
|
||||
func NewClient() (ret *Client) {
|
||||
vendorName := "Gemini"
|
||||
ret = &Client{}
|
||||
@ -27,14 +31,12 @@ func NewClient() (ret *Client) {
|
||||
type Client struct {
|
||||
*common.Configurable
|
||||
ApiKey *common.SetupQuestion
|
||||
|
||||
client *genai.Client
|
||||
}
|
||||
|
||||
func (ge *Client) ListModels() (ret []string, err error) {
|
||||
func (o *Client) ListModels() (ret []string, err error) {
|
||||
ctx := context.Background()
|
||||
var client *genai.Client
|
||||
if client, err = genai.NewClient(ctx, option.WithAPIKey(ge.ApiKey.Value)); err != nil {
|
||||
if client, err = genai.NewClient(ctx, option.WithAPIKey(o.ApiKey.Value)); err != nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
@ -43,56 +45,68 @@ func (ge *Client) ListModels() (ret []string, err error) {
|
||||
for {
|
||||
var resp *genai.ModelInfo
|
||||
if resp, err = iter.Next(); err != nil {
|
||||
if errors.Is(err, iterator.Done) {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
ret = append(ret, resp.Name)
|
||||
|
||||
name := o.buildModelNameSimple(resp.Name)
|
||||
ret = append(ret, name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ge *Client) Send(msgs []*common.Message, opts *common.ChatOptions) (ret string, err error) {
|
||||
systemInstruction, userText := toContent(msgs)
|
||||
func (o *Client) Send(msgs []*common.Message, opts *common.ChatOptions) (ret string, err error) {
|
||||
systemInstruction, messages := toMessages(msgs)
|
||||
|
||||
ctx := context.Background()
|
||||
var client *genai.Client
|
||||
if client, err = genai.NewClient(ctx, option.WithAPIKey(ge.ApiKey.Value)); err != nil {
|
||||
if client, err = genai.NewClient(ctx, option.WithAPIKey(o.ApiKey.Value)); err != nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
model := ge.client.GenerativeModel(opts.Model)
|
||||
model := client.GenerativeModel(o.buildModelNameFull(opts.Model))
|
||||
model.SetTemperature(float32(opts.Temperature))
|
||||
model.SetTopP(float32(opts.TopP))
|
||||
model.SystemInstruction = systemInstruction
|
||||
|
||||
var response *genai.GenerateContentResponse
|
||||
if response, err = model.GenerateContent(ctx, genai.Text(userText)); err != nil {
|
||||
if response, err = model.GenerateContent(ctx, messages...); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ret = ge.extractText(response)
|
||||
ret = o.extractText(response)
|
||||
return
|
||||
}
|
||||
|
||||
func (ge *Client) SendStream(msgs []*common.Message, opts *common.ChatOptions, channel chan string) (err error) {
|
||||
func (o *Client) buildModelNameSimple(fullModelName string) string {
|
||||
return strings.TrimPrefix(fullModelName, modelsNamePrefix)
|
||||
}
|
||||
|
||||
func (o *Client) buildModelNameFull(modelName string) string {
|
||||
return fmt.Sprintf("%v%v", modelsNamePrefix, modelName)
|
||||
}
|
||||
|
||||
func (o *Client) SendStream(msgs []*common.Message, opts *common.ChatOptions, channel chan string) (err error) {
|
||||
ctx := context.Background()
|
||||
var client *genai.Client
|
||||
if client, err = genai.NewClient(ctx, option.WithAPIKey(ge.ApiKey.Value)); err != nil {
|
||||
if client, err = genai.NewClient(ctx, option.WithAPIKey(o.ApiKey.Value)); err != nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
systemInstruction, userText := toContent(msgs)
|
||||
systemInstruction, messages := toMessages(msgs)
|
||||
|
||||
model := client.GenerativeModel(opts.Model)
|
||||
model := client.GenerativeModel(o.buildModelNameFull(opts.Model))
|
||||
model.SetTemperature(float32(opts.Temperature))
|
||||
model.SetTopP(float32(opts.TopP))
|
||||
model.SystemInstruction = systemInstruction
|
||||
|
||||
iter := model.GenerateContentStream(ctx, genai.Text(userText))
|
||||
iter := model.GenerateContentStream(ctx, messages...)
|
||||
for {
|
||||
var resp *genai.GenerateContentResponse
|
||||
if resp, err = iter.Next(); err == nil {
|
||||
if resp, iterErr := iter.Next(); iterErr == nil {
|
||||
for _, candidate := range resp.Candidates {
|
||||
if candidate.Content != nil {
|
||||
for _, part := range candidate.Content.Parts {
|
||||
@ -102,16 +116,18 @@ func (ge *Client) SendStream(msgs []*common.Message, opts *common.ChatOptions, c
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if errors.Is(err, iterator.Done) {
|
||||
channel <- "\n"
|
||||
} else {
|
||||
if !errors.Is(iterErr, iterator.Done) {
|
||||
channel <- fmt.Sprintf("%v\n", iterErr)
|
||||
}
|
||||
close(channel)
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ge *Client) extractText(response *genai.GenerateContentResponse) (ret string) {
|
||||
func (o *Client) extractText(response *genai.GenerateContentResponse) (ret string) {
|
||||
for _, candidate := range response.Candidates {
|
||||
if candidate.Content == nil {
|
||||
break
|
||||
@ -125,20 +141,18 @@ func (ge *Client) extractText(response *genai.GenerateContentResponse) (ret stri
|
||||
return
|
||||
}
|
||||
|
||||
// Current implementation does not support session
|
||||
// We need to retrieve the System instruction and User instruction
|
||||
// Considering how we've built msgs, it's the last 2 messages
|
||||
// FIXME: I know it's not clean, but will make it for now
|
||||
func toContent(msgs []*common.Message) (ret *genai.Content, userText string) {
|
||||
sys := msgs[len(msgs)-2]
|
||||
usr := msgs[len(msgs)-1]
|
||||
|
||||
ret = &genai.Content{
|
||||
Parts: []genai.Part{
|
||||
genai.Part(genai.Text(sys.Content)),
|
||||
},
|
||||
func toMessages(msgs []*common.Message) (systemInstruction *genai.Content, messages []genai.Part) {
|
||||
if len(msgs) >= 2 {
|
||||
systemInstruction = &genai.Content{
|
||||
Parts: []genai.Part{
|
||||
genai.Text(msgs[0].Content),
|
||||
},
|
||||
}
|
||||
for _, msg := range msgs[1:] {
|
||||
messages = append(messages, genai.Text(msg.Content))
|
||||
}
|
||||
} else {
|
||||
messages = append(messages, genai.Text(msgs[0].Content))
|
||||
}
|
||||
userText = usr.Content
|
||||
|
||||
return
|
||||
}
|
||||
|
25
youtube/youtube.go
Normal file
25
youtube/youtube.go
Normal file
@ -0,0 +1,25 @@
|
||||
package youtube
|
||||
|
||||
import (
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
)
|
||||
|
||||
func NewYouTube() (ret *YouTube) {
|
||||
|
||||
label := "YouTube"
|
||||
ret = &YouTube{}
|
||||
|
||||
ret.Configurable = &common.Configurable{
|
||||
Label: label,
|
||||
EnvNamePrefix: common.BuildEnvVariablePrefix(label),
|
||||
}
|
||||
|
||||
ret.ApiKey = ret.AddSetupQuestion("API key", true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type YouTube struct {
|
||||
*common.Configurable
|
||||
ApiKey *common.SetupQuestion
|
||||
}
|
Loading…
Reference in New Issue
Block a user