diff --git a/api/openapi.yaml b/api/openapi.yaml
index 682b9a095..b0c172680 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -3077,6 +3077,7 @@ components:
InstanceCapability:
type: string
enum:
+ - gen_ai
- semdex
- email_client
- sms_client
diff --git a/app/services/semdex/semdexer/refhydrate/hydrator.go b/app/resources/datagraph/hydrate/hydrator.go
similarity index 90%
rename from app/services/semdex/semdexer/refhydrate/hydrator.go
rename to app/resources/datagraph/hydrate/hydrator.go
index 4dbe23e6f..7fff0d476 100644
--- a/app/services/semdex/semdexer/refhydrate/hydrator.go
+++ b/app/resources/datagraph/hydrate/hydrator.go
@@ -1,8 +1,5 @@
-// Package refhydrate provides a Semdexer implementation which wraps an instance
-// of a RefSemdexer which will provide references for read-path methods instead
-// of fully hydrated Storyden objects (Post, Node, etc.) The Semdexer provided
-// by this package hydrates those references by looking them up in the database.
-package refhydrate
+// Package hydrate provides a generic datagraph item lookup conversion.
+package hydrate
import (
"context"
diff --git a/app/resources/resources.go b/app/resources/resources.go
index 9741aac03..6378806f8 100644
--- a/app/resources/resources.go
+++ b/app/resources/resources.go
@@ -20,6 +20,7 @@ import (
collection_items "github.com/Southclaws/storyden/app/resources/collection/collection_item"
"github.com/Southclaws/storyden/app/resources/collection/collection_querier"
"github.com/Southclaws/storyden/app/resources/collection/collection_writer"
+ "github.com/Southclaws/storyden/app/resources/datagraph/hydrate"
"github.com/Southclaws/storyden/app/resources/event/event_querier"
"github.com/Southclaws/storyden/app/resources/event/event_writer"
"github.com/Southclaws/storyden/app/resources/event/participation/participant_querier"
@@ -92,6 +93,7 @@ func Build() fx.Option {
event_writer.New,
participant_querier.New,
participant_writer.New,
+ hydrate.New,
),
)
}
diff --git a/app/services/generative/generative.go b/app/services/generative/generative.go
new file mode 100644
index 000000000..2102daa4b
--- /dev/null
+++ b/app/services/generative/generative.go
@@ -0,0 +1,42 @@
+package generative
+
+import (
+ "context"
+
+ "go.uber.org/fx"
+
+ "github.com/Southclaws/storyden/app/resources/datagraph"
+ "github.com/Southclaws/storyden/app/resources/tag/tag_ref"
+ "github.com/Southclaws/storyden/internal/infrastructure/ai"
+)
+
+type Tagger interface {
+ SuggestTags(ctx context.Context, content datagraph.Content, available tag_ref.Names) (tag_ref.Names, error)
+}
+
+type Summariser interface {
+ Summarise(ctx context.Context, object datagraph.Item) (string, error)
+}
+
+var (
+ _ Tagger = &generator{}
+ _ Summariser = &generator{}
+)
+
+type generator struct {
+ prompter ai.Prompter
+}
+
+func newGenerator(prompter ai.Prompter) *generator {
+ return &generator{prompter: prompter}
+}
+
+func Build() fx.Option {
+ return fx.Provide(
+ fx.Annotate(
+ newGenerator,
+ fx.As(new(Tagger)),
+ fx.As(new(Summariser)),
+ ),
+ )
+}
diff --git a/app/services/generative/summary.go b/app/services/generative/summary.go
new file mode 100644
index 000000000..a460b7303
--- /dev/null
+++ b/app/services/generative/summary.go
@@ -0,0 +1,48 @@
+package generative
+
+import (
+ "context"
+ "html/template"
+ "strings"
+
+ "github.com/Southclaws/fault"
+ "github.com/Southclaws/fault/fctx"
+
+ "github.com/Southclaws/storyden/app/resources/datagraph"
+)
+
+var SummarisePrompt = template.Must(template.New("").Parse(`
+Write a short few paragraphs that are somewhat engaging but remaining relatively neutral in tone in the style of a wikipedia introduction about "{{ .Name }}". Focus on providing unique insights and interesting details while keeping the tone conversational and approachable. Imagine this will be read by someone browsing a directory or knowledgebase.
+
+Be aware that the input to this may include broken HTML and other artifacts from the web and due to the nature of web scraping, there may be parts that do not make sense.
+
+- Ignore any HTML tags, malformed content, or text that does not contribute meaningfully to the main topic.
+- Based on the clear and coherent sections of the input, write short but engaging paragraphs. If the input lacks meaningful context, produce a neutral placeholder.
+- If the input content is too fragmented or lacks sufficient context to produce a coherent response, produce a neutral placeholder.
+- Do not describe the appearance of the input (e.g., broken HTML or artifacts). Instead, infer the main idea or purpose and expand on it creatively.
+- If key parts of the content are missing or ambiguous, use creativity to fill gaps while maintaining relevance to the topic.
+
+Output Format: Provide the output as a correctly formatted HTML document, you are free to use basic HTML formatting tags for emphasis, lists and headings. However, do not include the content title as a
tag at the top. Start with a paragraph block immediately.
+
+Content:
+
+{{ .Content }}
+`))
+
+func (g *generator) Summarise(ctx context.Context, object datagraph.Item) (string, error) {
+ template := strings.Builder{}
+ err := SummarisePrompt.Execute(&template, map[string]any{
+ "Name": object.GetName(),
+ "Content": object.GetContent().Plaintext(),
+ })
+ if err != nil {
+ return "", fault.Wrap(err, fctx.With(ctx))
+ }
+
+ result, err := g.prompter.Prompt(ctx, template.String())
+ if err != nil {
+ return "", fault.Wrap(err, fctx.With(ctx))
+ }
+
+ return result.Answer, nil
+}
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/tagger.go b/app/services/generative/tags.go
similarity index 67%
rename from app/services/semdex/semdexer/weaviate_semdexer/tagger.go
rename to app/services/generative/tags.go
index 2faedd7f4..a441e9205 100644
--- a/app/services/semdex/semdexer/weaviate_semdexer/tagger.go
+++ b/app/services/generative/tags.go
@@ -1,9 +1,9 @@
-package weaviate_semdexer
+package generative
import (
"context"
+ "html/template"
"strings"
- "text/template"
"github.com/Southclaws/dt"
"github.com/Southclaws/fault"
@@ -11,10 +11,9 @@ import (
"github.com/Southclaws/storyden/app/resources/datagraph"
"github.com/Southclaws/storyden/app/resources/tag/tag_ref"
"github.com/samber/lo"
- "github.com/weaviate/weaviate-go-client/v4/weaviate/graphql"
)
-var SuggestTagsPrompt = template.Must(template.New("").Parse(`Analyze the provided content of \"{name}\" and generate relevant tags. Tags are either single words or multiple words separated only by a hyphen, no spaces.
+var SuggestTagsPrompt = template.Must(template.New("").Parse(`Analyze the provided content and generate up to three relevant tags. Tags are either single words or multiple words separated only by a hyphen, no spaces.
It's very important that only tags that are relevant to the content are returned, any tags of low confidence MUST be omitted. Do not generate tags that are too vague or tags that are too specific and cannot easily be used in other contexts for other types of content. Generally avoid tags that are singular and not plural that too closely match phrases or words in the content.
@@ -30,7 +29,7 @@ Content:
{{ .Content }}
`))
-func (s *weaviateRefIndex) SuggestTags(ctx context.Context, content datagraph.Content, available tag_ref.Names) (tag_ref.Names, error) {
+func (g *generator) SuggestTags(ctx context.Context, content datagraph.Content, available tag_ref.Names) (tag_ref.Names, error) {
// cap the available tags at 50, we don't to blow out the prompt size limit.
sliced := lo.Splice(available, 50)
@@ -43,31 +42,12 @@ func (s *weaviateRefIndex) SuggestTags(ctx context.Context, content datagraph.Co
return nil, fault.Wrap(err, fctx.With(ctx))
}
- prompt := strings.ReplaceAll(template.String(), "\n", `\n`)
-
- gs := graphql.NewGenerativeSearch().SingleResult(prompt)
-
- r, err := mergeErrors(s.wc.GraphQL().
- Get().
- WithClassName(s.cn.String()).
- WithLimit(1).
- WithGenerativeSearch(gs).
- Do(ctx))
- if err != nil {
- return nil, fault.Wrap(err, fctx.With(ctx))
- }
-
- wr, err := mapResponseObjects(r.Data)
- if err != nil {
- return nil, fault.Wrap(err, fctx.With(ctx))
- }
-
- object, err := s.getFirstResult(wr)
+ result, err := g.prompter.Prompt(ctx, template.String())
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}
- strings := strings.Split(object.Additional.Generate.SingleResult, ", ")
+ strings := strings.Split(result.Answer, ", ")
tags := dt.Map(strings, func(s string) tag_ref.Name {
return tag_ref.NewName(s)
diff --git a/app/services/library/node_semdex/indexer.go b/app/services/library/node_semdex/indexer.go
index d7ff24c9f..e96552ce7 100644
--- a/app/services/library/node_semdex/indexer.go
+++ b/app/services/library/node_semdex/indexer.go
@@ -25,7 +25,7 @@ func (i *semdexer) index(ctx context.Context, id library.NodeID, summarise bool,
return fault.Wrap(err, fctx.With(ctx))
}
- err = i.indexer.Index(ctx, node)
+ err = i.semdexMutator.Index(ctx, node)
if err != nil {
return fault.Wrap(err, fctx.With(ctx))
}
@@ -81,7 +81,7 @@ func (i *semdexer) getSummary(ctx context.Context, p datagraph.Item) (*datagraph
func (i *semdexer) deindex(ctx context.Context, id library.NodeID) error {
qk := library.NewID(xid.ID(id))
- err := i.deleter.Delete(ctx, xid.ID(id))
+ err := i.semdexMutator.Delete(ctx, xid.ID(id))
if err != nil {
return fault.Wrap(err, fctx.With(ctx))
}
diff --git a/app/services/library/node_semdex/node_semdex.go b/app/services/library/node_semdex/node_semdex.go
index f40410365..34a314616 100644
--- a/app/services/library/node_semdex/node_semdex.go
+++ b/app/services/library/node_semdex/node_semdex.go
@@ -11,6 +11,7 @@ import (
"github.com/Southclaws/storyden/app/resources/library/node_writer"
"github.com/Southclaws/storyden/app/resources/mq"
"github.com/Southclaws/storyden/app/resources/tag/tag_writer"
+ "github.com/Southclaws/storyden/app/services/generative"
"github.com/Southclaws/storyden/app/services/semdex"
"github.com/Southclaws/storyden/app/services/tag/autotagger"
"github.com/Southclaws/storyden/internal/config"
@@ -39,18 +40,17 @@ var (
)
type semdexer struct {
- logger *zap.Logger
- db *ent.Client
- nodeQuerier *node_querier.Querier
- nodeWriter *node_writer.Writer
- indexQueue pubsub.Topic[mq.IndexNode]
- deleteQueue pubsub.Topic[mq.DeleteNode]
- indexer semdex.Indexer
- deleter semdex.Deleter
- retriever semdex.Retriever
- summariser semdex.Summariser
- tagger *autotagger.Tagger
- tagWriter *tag_writer.Writer
+ logger *zap.Logger
+ db *ent.Client
+ nodeQuerier *node_querier.Querier
+ nodeWriter *node_writer.Writer
+ indexQueue pubsub.Topic[mq.IndexNode]
+ deleteQueue pubsub.Topic[mq.DeleteNode]
+ semdexMutator semdex.Mutator
+ semdexQuerier semdex.Querier
+ summariser generative.Summariser
+ tagger *autotagger.Tagger
+ tagWriter *tag_writer.Writer
}
func newSemdexer(
@@ -64,10 +64,9 @@ func newSemdexer(
nodeWriter *node_writer.Writer,
indexQueue pubsub.Topic[mq.IndexNode],
deleteQueue pubsub.Topic[mq.DeleteNode],
- indexer semdex.Indexer,
- deleter semdex.Deleter,
- retriever semdex.Retriever,
- summariser semdex.Summariser,
+ semdexMutator semdex.Mutator,
+ semdexQuerier semdex.Querier,
+ summariser generative.Summariser,
tagger *autotagger.Tagger,
tagWriter *tag_writer.Writer,
) {
@@ -76,18 +75,17 @@ func newSemdexer(
}
re := semdexer{
- logger: l,
- db: db,
- nodeQuerier: nodeQuerier,
- nodeWriter: nodeWriter,
- indexQueue: indexQueue,
- deleteQueue: deleteQueue,
- indexer: indexer,
- deleter: deleter,
- retriever: retriever,
- summariser: summariser,
- tagger: tagger,
- tagWriter: tagWriter,
+ logger: l,
+ db: db,
+ nodeQuerier: nodeQuerier,
+ nodeWriter: nodeWriter,
+ indexQueue: indexQueue,
+ deleteQueue: deleteQueue,
+ semdexMutator: semdexMutator,
+ semdexQuerier: semdexQuerier,
+ summariser: summariser,
+ tagger: tagger,
+ tagWriter: tagWriter,
}
lc.Append(fx.StartHook(func(hctx context.Context) error {
diff --git a/app/services/library/node_semdex/reindexer.go b/app/services/library/node_semdex/reindexer.go
index 30a8a8468..f8d2f9ecf 100644
--- a/app/services/library/node_semdex/reindexer.go
+++ b/app/services/library/node_semdex/reindexer.go
@@ -53,7 +53,7 @@ func (r *semdexer) reindex(ctx context.Context, reindexThreshold time.Duration,
keepIDs := dt.Map(keep, func(p *ent.Node) xid.ID { return p.ID })
discardIDs := dt.Map(discard, func(p *ent.Node) xid.ID { return p.ID })
- indexed, err := r.retriever.GetMany(ctx, uint(reindexChunk), keepIDs...)
+ indexed, err := r.semdexQuerier.GetMany(ctx, uint(reindexChunk), keepIDs...)
if err != nil {
return fault.Wrap(err, fctx.With(ctx))
}
diff --git a/app/services/semdex/disabled.go b/app/services/semdex/disabled.go
new file mode 100644
index 000000000..c1b313364
--- /dev/null
+++ b/app/services/semdex/disabled.go
@@ -0,0 +1,47 @@
+package semdex
+
+import (
+ "context"
+
+ "github.com/rs/xid"
+
+ "github.com/Southclaws/storyden/app/resources/datagraph"
+ "github.com/Southclaws/storyden/app/resources/pagination"
+ "github.com/Southclaws/storyden/app/services/search/searcher"
+)
+
+type Disabled struct{}
+
+var _ Semdexer = &Disabled{}
+
+func (*Disabled) Index(ctx context.Context, object datagraph.Item) error {
+ return nil
+}
+
+func (*Disabled) Delete(ctx context.Context, object xid.ID) error {
+ return nil
+}
+
+func (*Disabled) Search(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[datagraph.Item], error) {
+ panic("semdex disabled: searcher switch bug")
+}
+
+func (*Disabled) SearchRefs(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[*datagraph.Ref], error) {
+ panic("semdex disabled: searcher switch bug")
+}
+
+func (*Disabled) Recommend(ctx context.Context, object datagraph.Item) (datagraph.ItemList, error) {
+ return nil, nil
+}
+
+func (*Disabled) RecommendRefs(ctx context.Context, object datagraph.Item) (datagraph.RefList, error) {
+ return nil, nil
+}
+
+func (*Disabled) ScoreRelevance(ctx context.Context, object datagraph.Item, idx ...xid.ID) (map[xid.ID]float64, error) {
+ return nil, nil
+}
+
+func (*Disabled) GetMany(ctx context.Context, limit uint, ids ...xid.ID) (datagraph.RefList, error) {
+ return nil, nil
+}
diff --git a/app/services/semdex/index_job/indexer.go b/app/services/semdex/index_job/indexer.go
index dfacbbfcb..68a3206b8 100644
--- a/app/services/semdex/index_job/indexer.go
+++ b/app/services/semdex/index_job/indexer.go
@@ -31,8 +31,7 @@ type indexerConsumer struct {
qthread pubsub.Topic[mq.IndexThread]
qreply pubsub.Topic[mq.IndexReply]
- indexer semdex.Indexer
- retriever semdex.Retriever
+ indexer semdex.Mutator
}
func newIndexConsumer(
@@ -48,8 +47,7 @@ func newIndexConsumer(
qreply pubsub.Topic[mq.IndexReply],
qprofile pubsub.Topic[mq.IndexProfile],
- indexer semdex.Indexer,
- retriever semdex.Retriever,
+ indexer semdex.Mutator,
) *indexerConsumer {
return &indexerConsumer{
l: l,
@@ -59,10 +57,9 @@ func newIndexConsumer(
accountQuery: accountQuery,
qnode: qnode,
- qthread: qthread,
- qreply: qreply,
- indexer: indexer,
- retriever: retriever,
+ qthread: qthread,
+ qreply: qreply,
+ indexer: indexer,
}
}
diff --git a/app/services/semdex/semdex.go b/app/services/semdex/semdex.go
index d142be2c6..28c9cb672 100644
--- a/app/services/semdex/semdex.go
+++ b/app/services/semdex/semdex.go
@@ -8,105 +8,37 @@ import (
"github.com/Southclaws/storyden/app/resources/datagraph"
"github.com/Southclaws/storyden/app/resources/pagination"
- "github.com/Southclaws/storyden/app/resources/tag/tag_ref"
"github.com/Southclaws/storyden/app/services/search/searcher"
)
-type Indexer interface {
- Index(ctx context.Context, object datagraph.Item) error
+type Semdexer interface {
+ Mutator
+ Querier
}
-type Deleter interface {
+type Mutator interface {
+ Index(ctx context.Context, object datagraph.Item) error
Delete(ctx context.Context, object xid.ID) error
}
-type Searcher interface {
- Search(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[datagraph.Item], error)
-}
-
-type RefSearcher interface {
- Search(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[*datagraph.Ref], error)
-}
-
-type Recommender interface {
- Recommend(ctx context.Context, object datagraph.Item) (datagraph.ItemList, error)
-}
-
-type Tagger interface {
- SuggestTags(ctx context.Context, content datagraph.Content, available tag_ref.Names) (tag_ref.Names, error)
-}
-
-type RefRecommender interface {
- Recommend(ctx context.Context, object datagraph.Item) (datagraph.RefList, error)
-}
-
-type RelevanceScorer interface {
- ScoreRelevance(ctx context.Context, object datagraph.Item, idx ...xid.ID) (map[xid.ID]float64, error)
-}
-
-type Summariser interface {
- Summarise(ctx context.Context, object datagraph.Item) (string, error)
-}
-
-type Retriever interface {
- GetMany(ctx context.Context, limit uint, ids ...xid.ID) (datagraph.RefList, error)
- // GetVectorFor(ctx context.Context, idx ...xid.ID) ([]float64, error)
-}
-
-type RefSemdexer interface {
- Indexer
- Deleter
- RefSearcher
- RefRecommender
- Tagger
- Retriever
- RelevanceScorer
- Summariser
-}
-
-type Semdexer interface {
- Indexer
- Deleter
+type Querier interface {
Searcher
Recommender
- Tagger
- Retriever
RelevanceScorer
- Summariser
-}
-
-type Disabled struct{}
-
-var _ Semdexer = &Disabled{}
-
-func (*Disabled) Index(ctx context.Context, object datagraph.Item) error {
- return nil
-}
-
-func (*Disabled) Delete(ctx context.Context, object xid.ID) error {
- return nil
-}
-
-func (*Disabled) Search(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[datagraph.Item], error) {
- panic("semdex disabled: searcher switch bug")
-}
-func (*Disabled) Recommend(ctx context.Context, object datagraph.Item) (datagraph.ItemList, error) {
- return nil, nil
-}
-
-func (*Disabled) SuggestTags(ctx context.Context, content datagraph.Content, available tag_ref.Names) (tag_ref.Names, error) {
- return nil, nil
+ GetMany(ctx context.Context, limit uint, ids ...xid.ID) (datagraph.RefList, error)
}
-func (*Disabled) ScoreRelevance(ctx context.Context, object datagraph.Item, idx ...xid.ID) (map[xid.ID]float64, error) {
- return nil, nil
+type Searcher interface {
+ Search(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[datagraph.Item], error)
+ SearchRefs(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[*datagraph.Ref], error)
}
-func (*Disabled) Summarise(ctx context.Context, object datagraph.Item) (string, error) {
- return "", nil
+type Recommender interface {
+ Recommend(ctx context.Context, object datagraph.Item) (datagraph.ItemList, error)
+ RecommendRefs(ctx context.Context, object datagraph.Item) (datagraph.RefList, error)
}
-func (*Disabled) GetMany(ctx context.Context, limit uint, ids ...xid.ID) (datagraph.RefList, error) {
- return nil, nil
+type RelevanceScorer interface {
+ ScoreRelevance(ctx context.Context, object datagraph.Item, idx ...xid.ID) (map[xid.ID]float64, error)
}
diff --git a/app/services/semdex/semdexer/chromem_semdexer/chromem.go b/app/services/semdex/semdexer/chromem_semdexer/chromem.go
index a60e1223f..c1efe74ae 100644
--- a/app/services/semdex/semdexer/chromem_semdexer/chromem.go
+++ b/app/services/semdex/semdexer/chromem_semdexer/chromem.go
@@ -12,19 +12,20 @@ import (
"github.com/samber/lo"
"github.com/Southclaws/storyden/app/resources/datagraph"
+ "github.com/Southclaws/storyden/app/resources/datagraph/hydrate"
"github.com/Southclaws/storyden/app/resources/pagination"
- "github.com/Southclaws/storyden/app/resources/tag/tag_ref"
"github.com/Southclaws/storyden/app/services/search/searcher"
- "github.com/Southclaws/storyden/app/services/semdex/semdexer/refhydrate"
+ "github.com/Southclaws/storyden/app/services/semdex"
"github.com/Southclaws/storyden/internal/config"
)
type chromemRefIndex struct {
- db *chromem.DB
- c *chromem.Collection
+ db *chromem.DB
+ c *chromem.Collection
+ hydrator *hydrate.Hydrator
}
-func New(cfg config.Config, rh *refhydrate.Hydrator) (*refhydrate.HydratedSemdexer, error) {
+func New(cfg config.Config, rh *hydrate.Hydrator) (semdex.Semdexer, error) {
db, err := chromem.NewPersistentDB(cfg.SemdexLocalPath, false)
if err != nil {
return nil, err
@@ -41,9 +42,10 @@ func New(cfg config.Config, rh *refhydrate.Hydrator) (*refhydrate.HydratedSemdex
return nil, err
}
- return &refhydrate.HydratedSemdexer{
- RefSemdex: &chromemRefIndex{db: db, c: collection},
- Hydrator: rh,
+ return &chromemRefIndex{
+ db: db,
+ c: collection,
+ hydrator: rh,
}, nil
}
@@ -61,7 +63,38 @@ func (c *chromemRefIndex) Delete(ctx context.Context, object xid.ID) error {
return c.c.Delete(ctx, nil, nil, object.String())
}
-func (c *chromemRefIndex) Search(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[*datagraph.Ref], error) {
+func (c *chromemRefIndex) Search(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[datagraph.Item], error) {
+ nr := min(c.c.Count(), p.Size())
+ if nr == 0 {
+ res := pagination.NewPageResult[datagraph.Item](p, 0, nil)
+ return &res, nil
+ }
+
+ rs, err := c.c.Query(ctx, q, nr, nil, nil)
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ filtered := lo.Filter(rs, func(r chromem.Result, _ int) bool {
+ return r.Similarity > 0.2
+ })
+
+ list, err := mapResults(filtered)
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ hyd, err := c.hydrator.Hydrate(ctx, list...)
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ results := pagination.NewPageResult(p, len(rs), hyd)
+
+ return &results, nil
+}
+
+func (c *chromemRefIndex) SearchRefs(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[*datagraph.Ref], error) {
nr := min(c.c.Count(), p.Size())
if nr == 0 {
res := pagination.NewPageResult[*datagraph.Ref](p, 0, nil)
@@ -87,11 +120,28 @@ func (c *chromemRefIndex) Search(ctx context.Context, q string, p pagination.Par
return &results, nil
}
-func (c *chromemRefIndex) SuggestTags(ctx context.Context, content datagraph.Content, available tag_ref.Names) (tag_ref.Names, error) {
- return nil, nil
+func (c *chromemRefIndex) RecommendRefs(ctx context.Context, object datagraph.Item) (datagraph.RefList, error) {
+ doc, err := c.c.GetByID(ctx, object.GetID().String())
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ nr := min(c.c.Count(), 10)
+
+ rs, err := c.c.QueryEmbedding(ctx, doc.Embedding, nr, nil, nil)
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ list, err := mapResults(rs)
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ return list, nil
}
-func (c *chromemRefIndex) Recommend(ctx context.Context, object datagraph.Item) (datagraph.RefList, error) {
+func (c *chromemRefIndex) Recommend(ctx context.Context, object datagraph.Item) (datagraph.ItemList, error) {
doc, err := c.c.GetByID(ctx, object.GetID().String())
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
@@ -109,7 +159,12 @@ func (c *chromemRefIndex) Recommend(ctx context.Context, object datagraph.Item)
return nil, fault.Wrap(err, fctx.With(ctx))
}
- return list, nil
+ items, err := c.hydrator.Hydrate(ctx, list...)
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ return items, nil
}
func (c *chromemRefIndex) ScoreRelevance(ctx context.Context, object datagraph.Item, ids ...xid.ID) (map[xid.ID]float64, error) {
@@ -149,10 +204,6 @@ func (c *chromemRefIndex) ScoreRelevance(ctx context.Context, object datagraph.I
return result, nil
}
-func (c *chromemRefIndex) Summarise(ctx context.Context, object datagraph.Item) (string, error) {
- return "", nil
-}
-
func mapResults(rs []chromem.Result) (datagraph.RefList, error) {
return dt.MapErr(rs, mapResult)
}
diff --git a/app/services/semdex/semdexer/refhydrate/semdex.go b/app/services/semdex/semdexer/refhydrate/semdex.go
deleted file mode 100644
index 36aeaf0c9..000000000
--- a/app/services/semdex/semdexer/refhydrate/semdex.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package refhydrate
-
-import (
- "context"
-
- "github.com/Southclaws/fault"
- "github.com/Southclaws/fault/fctx"
- "github.com/rs/xid"
-
- "github.com/Southclaws/storyden/app/resources/datagraph"
- "github.com/Southclaws/storyden/app/resources/pagination"
- "github.com/Southclaws/storyden/app/resources/tag/tag_ref"
- "github.com/Southclaws/storyden/app/services/search/searcher"
- "github.com/Southclaws/storyden/app/services/semdex"
-)
-
-var _ semdex.Semdexer = &HydratedSemdexer{}
-
-// HydratedSemdexer implements the Semdexer interface for semantic indexing. It
-// wraps the weaviate ref index which works on non-hydrated lower level refs.
-type HydratedSemdexer struct {
- RefSemdex semdex.RefSemdexer
- Hydrator *Hydrator
-}
-
-func (h *HydratedSemdexer) Index(ctx context.Context, object datagraph.Item) error {
- return h.RefSemdex.Index(ctx, object)
-}
-
-func (h *HydratedSemdexer) Delete(ctx context.Context, id xid.ID) error {
- return h.RefSemdex.Delete(ctx, id)
-}
-
-func (h *HydratedSemdexer) Search(ctx context.Context, query string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[datagraph.Item], error) {
- rs, err := h.RefSemdex.Search(ctx, query, p, opts)
- if err != nil {
- return nil, fault.Wrap(err, fctx.With(ctx))
- }
-
- hydrated, err := h.Hydrator.Hydrate(ctx, rs.Items...)
- if err != nil {
- return nil, fault.Wrap(err, fctx.With(ctx))
- }
-
- results := pagination.ConvertPageResult[*datagraph.Ref, datagraph.Item](*rs, hydrated)
-
- return &results, nil
-}
-
-func (h *HydratedSemdexer) Recommend(ctx context.Context, object datagraph.Item) (datagraph.ItemList, error) {
- rs, err := h.RefSemdex.Recommend(ctx, object)
- if err != nil {
- return nil, fault.Wrap(err, fctx.With(ctx))
- }
-
- return h.Hydrator.Hydrate(ctx, rs...)
-}
-
-func (h *HydratedSemdexer) SuggestTags(ctx context.Context, content datagraph.Content, available tag_ref.Names) (tag_ref.Names, error) {
- return h.RefSemdex.SuggestTags(ctx, content, available)
-}
-
-func (h *HydratedSemdexer) GetMany(ctx context.Context, limit uint, ids ...xid.ID) (datagraph.RefList, error) {
- return h.RefSemdex.GetMany(ctx, limit, ids...)
-}
-
-func (h *HydratedSemdexer) ScoreRelevance(ctx context.Context, object datagraph.Item, idx ...xid.ID) (map[xid.ID]float64, error) {
- return h.RefSemdex.ScoreRelevance(ctx, object, idx...)
-}
-
-func (h *HydratedSemdexer) Summarise(ctx context.Context, object datagraph.Item) (string, error) {
- return h.RefSemdex.Summarise(ctx, object)
-}
diff --git a/app/services/semdex/semdexer/semdexer.go b/app/services/semdex/semdexer/semdexer.go
index 85e5d6bbc..16547dd5d 100644
--- a/app/services/semdex/semdexer/semdexer.go
+++ b/app/services/semdex/semdexer/semdexer.go
@@ -4,10 +4,10 @@ import (
"github.com/weaviate/weaviate-go-client/v4/weaviate"
"go.uber.org/fx"
+ "github.com/Southclaws/fault"
+ "github.com/Southclaws/storyden/app/resources/datagraph/hydrate"
"github.com/Southclaws/storyden/app/services/semdex"
"github.com/Southclaws/storyden/app/services/semdex/semdexer/chromem_semdexer"
- "github.com/Southclaws/storyden/app/services/semdex/semdexer/refhydrate"
-
"github.com/Southclaws/storyden/app/services/semdex/semdexer/weaviate_semdexer"
"github.com/Southclaws/storyden/internal/config"
weaviate_infra "github.com/Southclaws/storyden/internal/infrastructure/weaviate"
@@ -18,13 +18,19 @@ func newSemdexer(
wc *weaviate.Client,
weaviateClassName weaviate_infra.WeaviateClassName,
- hydrator *refhydrate.Hydrator,
+ hydrator *hydrate.Hydrator,
) (semdex.Semdexer, error) {
+ if cfg.SemdexProvider != "" && cfg.LanguageModelProvider == "" {
+ return nil, fault.New("semdex requires a language model provider to be enabled")
+ }
+
switch cfg.SemdexProvider {
case "chromem":
+
return chromem_semdexer.New(cfg, hydrator)
case "weaviate":
+
return weaviate_semdexer.New(wc, weaviateClassName, hydrator), nil
default:
@@ -34,18 +40,14 @@ func newSemdexer(
func Build() fx.Option {
return fx.Provide(
- refhydrate.New,
fx.Annotate(
newSemdexer,
fx.As(new(semdex.Semdexer)),
- fx.As(new(semdex.Indexer)),
- fx.As(new(semdex.Deleter)),
- fx.As(new(semdex.Searcher)),
+ fx.As(new(semdex.Querier)),
+ fx.As(new(semdex.Mutator)),
fx.As(new(semdex.Recommender)),
- fx.As(new(semdex.Tagger)),
- fx.As(new(semdex.Retriever)),
fx.As(new(semdex.RelevanceScorer)),
- fx.As(new(semdex.Summariser)),
+ fx.As(new(semdex.Searcher)),
),
)
}
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/average.go b/app/services/semdex/semdexer/weaviate_semdexer/average.go
deleted file mode 100644
index 2ec6dfa3e..000000000
--- a/app/services/semdex/semdexer/weaviate_semdexer/average.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package weaviate_semdexer
-
-import (
- "context"
-
- "github.com/rs/xid"
-)
-
-func (s *weaviateRefIndex) GetVectorFor(ctx context.Context, idx ...xid.ID) ([]float64, error) {
- // TODO: pull vectors for all items, compute average and return?
- return nil, nil
-}
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/delete.go b/app/services/semdex/semdexer/weaviate_semdexer/delete.go
index 19d663623..bc97fdae8 100644
--- a/app/services/semdex/semdexer/weaviate_semdexer/delete.go
+++ b/app/services/semdex/semdexer/weaviate_semdexer/delete.go
@@ -8,7 +8,7 @@ import (
"github.com/rs/xid"
)
-func (w *weaviateRefIndex) Delete(ctx context.Context, id xid.ID) error {
+func (w *weaviateSemdexer) Delete(ctx context.Context, id xid.ID) error {
wid := GetWeaviateID(id)
err := w.wc.Data().Deleter().
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/indexer.go b/app/services/semdex/semdexer/weaviate_semdexer/indexer.go
index c3b0a1947..5e3f18bbc 100644
--- a/app/services/semdex/semdexer/weaviate_semdexer/indexer.go
+++ b/app/services/semdex/semdexer/weaviate_semdexer/indexer.go
@@ -14,7 +14,7 @@ import (
"github.com/Southclaws/storyden/app/resources/datagraph"
)
-func (s *weaviateRefIndex) Index(ctx context.Context, object datagraph.Item) error {
+func (s *weaviateSemdexer) Index(ctx context.Context, object datagraph.Item) error {
rich := object.GetContent()
sid := object.GetID()
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/mapping.go b/app/services/semdex/semdexer/weaviate_semdexer/mapping.go
index dd73fc75e..df95ab96e 100644
--- a/app/services/semdex/semdexer/weaviate_semdexer/mapping.go
+++ b/app/services/semdex/semdexer/weaviate_semdexer/mapping.go
@@ -84,7 +84,7 @@ func mapResponseObjects(raw map[string]models.JSONObject) (*WeaviateResponse, er
return &parsed, nil
}
-func (s *weaviateRefIndex) getFirstResult(wr *WeaviateResponse) (*WeaviateObject, error) {
+func (s *weaviateSemdexer) getFirstResult(wr *WeaviateResponse) (*WeaviateObject, error) {
objects := wr.Get[s.cn.String()]
if len(objects) != 1 {
return nil, fault.Newf("expected exactly one result, got %d", len(objects))
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/recommend.go b/app/services/semdex/semdexer/weaviate_semdexer/recommend.go
index 355aa68a7..77b4a1092 100644
--- a/app/services/semdex/semdexer/weaviate_semdexer/recommend.go
+++ b/app/services/semdex/semdexer/weaviate_semdexer/recommend.go
@@ -14,7 +14,21 @@ import (
"github.com/Southclaws/storyden/app/resources/datagraph"
)
-func (w *weaviateRefIndex) Recommend(ctx context.Context, object datagraph.Item) (datagraph.RefList, error) {
+func (w *weaviateSemdexer) Recommend(ctx context.Context, object datagraph.Item) (datagraph.ItemList, error) {
+ refs, err := w.RecommendRefs(ctx, object)
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ items, err := w.hydrator.Hydrate(ctx, refs...)
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ return items, nil
+}
+
+func (w *weaviateSemdexer) RecommendRefs(ctx context.Context, object datagraph.Item) (datagraph.RefList, error) {
wid := GetWeaviateID(object.GetID())
result, err := w.wc.Data().ObjectsGetter().
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/relevance.go b/app/services/semdex/semdexer/weaviate_semdexer/relevance.go
index 3e9cffc61..3f62a1948 100644
--- a/app/services/semdex/semdexer/weaviate_semdexer/relevance.go
+++ b/app/services/semdex/semdexer/weaviate_semdexer/relevance.go
@@ -14,7 +14,7 @@ import (
"github.com/Southclaws/storyden/app/resources/datagraph"
)
-func (w *weaviateRefIndex) ScoreRelevance(ctx context.Context, object datagraph.Item, ids ...xid.ID) (map[xid.ID]float64, error) {
+func (w *weaviateSemdexer) ScoreRelevance(ctx context.Context, object datagraph.Item, ids ...xid.ID) (map[xid.ID]float64, error) {
if len(ids) == 0 {
return map[xid.ID]float64{}, nil
}
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/retrieval.go b/app/services/semdex/semdexer/weaviate_semdexer/retrieval.go
index 70978be6f..bc22920dd 100644
--- a/app/services/semdex/semdexer/weaviate_semdexer/retrieval.go
+++ b/app/services/semdex/semdexer/weaviate_semdexer/retrieval.go
@@ -14,7 +14,7 @@ import (
"github.com/Southclaws/storyden/app/resources/datagraph"
)
-func (o *weaviateRefIndex) GetMany(ctx context.Context, limit uint, ids ...xid.ID) (datagraph.RefList, error) {
+func (o *weaviateSemdexer) GetMany(ctx context.Context, limit uint, ids ...xid.ID) (datagraph.RefList, error) {
stringIDs := dt.Map(ids, func(x xid.ID) string { return x.String() })
objects, err := o.wc.
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/search.go b/app/services/semdex/semdexer/weaviate_semdexer/search.go
index 6c320a4c9..7fb4e7bfd 100644
--- a/app/services/semdex/semdexer/weaviate_semdexer/search.go
+++ b/app/services/semdex/semdexer/weaviate_semdexer/search.go
@@ -15,7 +15,22 @@ import (
"github.com/Southclaws/storyden/app/services/search/searcher"
)
-func (s *weaviateRefIndex) Search(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[*datagraph.Ref], error) {
+func (s *weaviateSemdexer) Search(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[datagraph.Item], error) {
+ refs, err := s.SearchRefs(ctx, q, p, opts)
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ items, err := s.hydrator.Hydrate(ctx, refs.Items...)
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ result := pagination.NewPageResult(p, refs.Results, items)
+ return &result, nil
+}
+
+func (s *weaviateSemdexer) SearchRefs(ctx context.Context, q string, p pagination.Parameters, opts searcher.Options) (*pagination.Result[*datagraph.Ref], error) {
fields := []graphql.Field{
{Name: "datagraph_id"},
{Name: "datagraph_type"},
@@ -95,7 +110,7 @@ func (s *weaviateRefIndex) Search(ctx context.Context, q string, p pagination.Pa
return &pagedResult, nil
}
-func (s *weaviateRefIndex) countObjects(ctx context.Context, countQuery graphql.AggregateBuilder) (int, error) {
+func (s *weaviateSemdexer) countObjects(ctx context.Context, countQuery graphql.AggregateBuilder) (int, error) {
r, err := mergeErrors(countQuery.Do(ctx))
if err != nil {
return 0, fault.Wrap(err, fctx.With(ctx))
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/summarise.go b/app/services/semdex/semdexer/weaviate_semdexer/summarise.go
deleted file mode 100644
index 014595791..000000000
--- a/app/services/semdex/semdexer/weaviate_semdexer/summarise.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package weaviate_semdexer
-
-import (
- "context"
- "fmt"
- "strings"
- "text/template"
-
- "github.com/Southclaws/fault"
- "github.com/Southclaws/fault/fctx"
- "github.com/weaviate/weaviate-go-client/v4/weaviate/filters"
- "github.com/weaviate/weaviate-go-client/v4/weaviate/graphql"
-
- "github.com/Southclaws/storyden/app/resources/datagraph"
-)
-
-var SummarisePrompt = template.Must(template.New("").Parse(`
-Write a short few paragraphs that are somewhat engaging but remaining relatively neutral in tone in the style of a wikipedia introduction about \"{name}\". Focus on providing unique insights and interesting details while keeping the tone conversational and approachable. Imagine this will be read by someone browsing a directory or knowledgebase.
-
-Be aware that the input to this may include broken HTML and other artifacts from the web and due to the nature of web scraping, there may be parts that do not make sense.
-
-- Ignore any HTML tags, malformed content, or text that does not contribute meaningfully to the main topic.
-- Based on the clear and coherent sections of the input, write short but engaging paragraphs. If the input lacks meaningful context, produce a neutral placeholder.
-- If the input content is too fragmented or lacks sufficient context to produce a coherent response, produce a neutral placeholder.
-- Do not describe the appearance of the input (e.g., broken HTML or artifacts). Instead, infer the main idea or purpose and expand on it creatively.
-- If key parts of the content are missing or ambiguous, use creativity to fill gaps while maintaining relevance to the topic.
-
-Output Format: Provide the output as a correctly formatted HTML document, you are free to use basic HTML formatting tags for emphasis, lists and headings. However, do not include the content title as a tag at the top. Start with a paragraph block immediately.
-
-Content:
-
-{content}
-`))
-
-func (s *weaviateRefIndex) Summarise(ctx context.Context, object datagraph.Item) (string, error) {
- fields := []graphql.Field{
- {Name: "datagraph_id"},
- {Name: "datagraph_type"},
- {Name: "name"},
- {Name: "content"},
- }
-
- // Switch summariser strategy based on the Weaviate class.
- // Local inference uses sum-transformers, remote inference uses openai.
- // TODO: Express this switcher in a better way at the top-level config.
- if s.cn.String() == "ContentText2vecTransformers" {
- fields = append(fields, graphql.Field{
- Name: "_additional",
- Fields: []graphql.Field{
- {Name: "summary(properties: [\"content\"])", Fields: []graphql.Field{
- {Name: "property"},
- {Name: "result"},
- }},
- },
- })
- } else if s.cn.String() == "ContentOpenAI" {
-
- template := strings.Builder{}
- err := SummarisePrompt.Execute(&template, map[string]any{})
- if err != nil {
- return "", fault.Wrap(err, fctx.With(ctx))
- }
-
- prompt := strings.ReplaceAll(template.String(), "\n", `\n`)
-
- summaryPrompt := fmt.Sprintf(`generate(singleResult: {
- prompt: """
- %s
- """
- })`, prompt)
-
- fields = append(fields, graphql.Field{
- Name: "_additional",
- Fields: []graphql.Field{
- {Name: summaryPrompt, Fields: []graphql.Field{
- {Name: "singleResult"},
- {Name: "error"},
- }},
- },
- })
- } else {
- // No summariser available
- // TODO: return an error maybe?
- return "", nil
- }
-
- where := filters.Where().
- WithPath([]string{"datagraph_id"}).
- WithOperator(filters.ContainsAny).
- WithValueString(object.GetID().String())
-
- result, err := mergeErrors(s.wc.GraphQL().Get().
- WithClassName(s.cn.String()).
- WithFields(fields...).
- WithWhere(where).
- Do(context.Background()))
- if err != nil {
- return "", fault.Wrap(err, fctx.With(ctx))
- }
-
- parsed, err := mapResponseObjects(result.Data)
- if err != nil {
- return "", err
- }
-
- classData := parsed.Get[s.cn.String()]
-
- if len(classData) != 1 {
- return "", fault.Newf("expected exactly one result, got %d", len(classData))
- }
-
- if s.cn.String() == "ContentText2vecTransformers" {
- if classData[0].Additional.Summary == nil || len(classData[0].Additional.Summary) != 1 {
- return "", fault.New("summary not found in response")
- }
-
- return classData[0].Additional.Summary[0].Result, nil
-
- } else if s.cn.String() == "ContentOpenAI" {
- if classData[0].Additional.Generate.Error != "" {
- return "", fault.New(classData[0].Additional.Generate.Error)
- }
-
- return classData[0].Additional.Generate.SingleResult, nil
- }
-
- // TODO: handle this edge case
- return "", nil
-}
diff --git a/app/services/semdex/semdexer/weaviate_semdexer/weaviate.go b/app/services/semdex/semdexer/weaviate_semdexer/weaviate.go
index 8b57b266a..34860415a 100644
--- a/app/services/semdex/semdexer/weaviate_semdexer/weaviate.go
+++ b/app/services/semdex/semdexer/weaviate_semdexer/weaviate.go
@@ -3,30 +3,24 @@ package weaviate_semdexer
import (
"github.com/weaviate/weaviate-go-client/v4/weaviate"
- "github.com/Southclaws/storyden/app/services/semdex/semdexer/refhydrate"
+ "github.com/Southclaws/storyden/app/resources/datagraph/hydrate"
weaviate_infra "github.com/Southclaws/storyden/internal/infrastructure/weaviate"
)
-// weaviateRefIndex implements what looks slightly like the Semdexer interface
-// but all of its methods return references, rather than fully hydrated objects.
-// This is because hydration is somewhat costly and not always what you need.
-// It also separates the responsibility of hydrating content from the resource
-// layer from the vector database. If you need to operate on lower level refs,
-// this is what is best to use because it doesn't make costly database calls.
-type weaviateRefIndex struct {
- wc *weaviate.Client
- cn weaviate_infra.WeaviateClassName
+type weaviateSemdexer struct {
+ wc *weaviate.Client
+ cn weaviate_infra.WeaviateClassName
+ hydrator *hydrate.Hydrator
}
func New(
wc *weaviate.Client,
cn weaviate_infra.WeaviateClassName,
- rh *refhydrate.Hydrator,
-) *refhydrate.HydratedSemdexer {
- ws := &weaviateRefIndex{wc, cn}
-
- return &refhydrate.HydratedSemdexer{
- RefSemdex: ws,
- Hydrator: rh,
+ hydrator *hydrate.Hydrator,
+) *weaviateSemdexer {
+ return &weaviateSemdexer{
+ wc: wc,
+ cn: cn,
+ hydrator: hydrator,
}
}
diff --git a/app/services/services.go b/app/services/services.go
index 7518169bb..2161ee634 100644
--- a/app/services/services.go
+++ b/app/services/services.go
@@ -16,6 +16,7 @@ import (
"github.com/Southclaws/storyden/app/services/collection"
"github.com/Southclaws/storyden/app/services/comms"
"github.com/Southclaws/storyden/app/services/event"
+ "github.com/Southclaws/storyden/app/services/generative"
"github.com/Southclaws/storyden/app/services/library"
"github.com/Southclaws/storyden/app/services/like/post_liker"
"github.com/Southclaws/storyden/app/services/link"
@@ -57,6 +58,7 @@ func Build() fx.Option {
link.Build(),
notify_job.Build(),
mention_job.Build(),
+ generative.Build(),
semdexer.Build(),
index_job.Build(),
event.Build(),
diff --git a/app/services/system/instance_info/capabilities.go b/app/services/system/instance_info/capabilities.go
index b698dc7e1..bcc0528e9 100644
--- a/app/services/system/instance_info/capabilities.go
+++ b/app/services/system/instance_info/capabilities.go
@@ -5,6 +5,7 @@ package instance_info
type capabilityEnum string
const (
+ capabilityGenAI capabilityEnum = `gen_ai`
capabilitySemdex capabilityEnum = `semdex`
capabilityEmailClient capabilityEnum = `email_client`
capabilitySMSClient capabilityEnum = `sms_client`
diff --git a/app/services/system/instance_info/info.go b/app/services/system/instance_info/info.go
index 5c913357a..3e9aca5b3 100644
--- a/app/services/system/instance_info/info.go
+++ b/app/services/system/instance_info/info.go
@@ -60,6 +60,10 @@ func (p *Provider) Get(ctx context.Context) (*Info, error) {
caps := Capabilities{}
+ if p.config.LanguageModelProvider != "" {
+ caps = append(caps, CapabilityGenAI)
+ }
+
if p.config.SemdexProvider != "" {
caps = append(caps, CapabilitySemdex)
}
diff --git a/app/services/system/instance_info/instance_info_enum_gen.go b/app/services/system/instance_info/instance_info_enum_gen.go
index ccc26d470..e2841754c 100644
--- a/app/services/system/instance_info/instance_info_enum_gen.go
+++ b/app/services/system/instance_info/instance_info_enum_gen.go
@@ -12,6 +12,7 @@ type Capability struct {
}
var (
+ CapabilityGenAI = Capability{capabilityGenAI}
CapabilitySemdex = Capability{capabilitySemdex}
CapabilityEmailClient = Capability{capabilityEmailClient}
CapabilitySMSClient = Capability{capabilitySMSClient}
@@ -54,6 +55,8 @@ func (r *Capability) Scan(__iNpUt__ any) error {
}
func NewCapability(__iNpUt__ string) (Capability, error) {
switch __iNpUt__ {
+ case string(capabilityGenAI):
+ return CapabilityGenAI, nil
case string(capabilitySemdex):
return CapabilitySemdex, nil
case string(capabilityEmailClient):
diff --git a/app/services/tag/autotagger/tagger.go b/app/services/tag/autotagger/tagger.go
index 15342c19b..3a526c49a 100644
--- a/app/services/tag/autotagger/tagger.go
+++ b/app/services/tag/autotagger/tagger.go
@@ -5,21 +5,22 @@ import (
"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fctx"
+
"github.com/Southclaws/storyden/app/resources/datagraph"
"github.com/Southclaws/storyden/app/resources/tag"
"github.com/Southclaws/storyden/app/resources/tag/tag_querier"
"github.com/Southclaws/storyden/app/resources/tag/tag_ref"
- "github.com/Southclaws/storyden/app/services/semdex"
+ "github.com/Southclaws/storyden/app/services/generative"
)
type Tagger struct {
querier *tag_querier.Querier
- tagger semdex.Tagger
+ tagger generative.Tagger
}
func New(
querier *tag_querier.Querier,
- tagger semdex.Tagger,
+ tagger generative.Tagger,
) *Tagger {
return &Tagger{
querier: querier,
diff --git a/app/services/thread/thread_semdex/indexer.go b/app/services/thread/thread_semdex/indexer.go
index bcb115dfb..b946cf7f2 100644
--- a/app/services/thread/thread_semdex/indexer.go
+++ b/app/services/thread/thread_semdex/indexer.go
@@ -17,7 +17,7 @@ func (i *semdexer) indexThread(ctx context.Context, id post.ID) error {
return fault.Wrap(err, fctx.With(ctx))
}
- err = i.indexer.Index(ctx, p)
+ err = i.semdexMutator.Index(ctx, p)
if err != nil {
return fault.Wrap(err, fctx.With(ctx))
}
@@ -31,7 +31,7 @@ func (i *semdexer) indexThread(ctx context.Context, id post.ID) error {
}
func (i *semdexer) deindexThread(ctx context.Context, id post.ID) error {
- err := i.deleter.Delete(ctx, xid.ID(id))
+ err := i.semdexMutator.Delete(ctx, xid.ID(id))
if err != nil {
return fault.Wrap(err, fctx.With(ctx))
}
diff --git a/app/services/thread/thread_semdex/reindexer.go b/app/services/thread/thread_semdex/reindexer.go
index e079c7bec..f0e8d907f 100644
--- a/app/services/thread/thread_semdex/reindexer.go
+++ b/app/services/thread/thread_semdex/reindexer.go
@@ -54,7 +54,7 @@ func (r *semdexer) reindex(ctx context.Context, reindexThreshold time.Duration,
keepIDs := dt.Map(keep, func(p *ent.Post) xid.ID { return p.ID })
discardIDs := dt.Map(discard, func(p *ent.Post) xid.ID { return p.ID })
- indexed, err := r.retriever.GetMany(ctx, uint(reindexChunk), keepIDs...)
+ indexed, err := r.semdexQuerier.GetMany(ctx, uint(reindexChunk), keepIDs...)
if err != nil {
return fault.Wrap(err, fctx.With(ctx))
}
diff --git a/app/services/thread/thread_semdex/thread_semdex.go b/app/services/thread/thread_semdex/thread_semdex.go
index dbe08e326..23fca3163 100644
--- a/app/services/thread/thread_semdex/thread_semdex.go
+++ b/app/services/thread/thread_semdex/thread_semdex.go
@@ -42,9 +42,8 @@ type semdexer struct {
threadWriter thread.Repository
indexQueue pubsub.Topic[mq.IndexThread]
deleteQueue pubsub.Topic[mq.DeleteThread]
- indexer semdex.Indexer
- deleter semdex.Deleter
- retriever semdex.Retriever
+ semdexMutator semdex.Mutator
+ semdexQuerier semdex.Querier
}
func newSemdexer(
@@ -58,9 +57,8 @@ func newSemdexer(
threadWriter thread.Repository,
indexQueue pubsub.Topic[mq.IndexThread],
deleteQueue pubsub.Topic[mq.DeleteThread],
- indexer semdex.Indexer,
- deleter semdex.Deleter,
- retriever semdex.Retriever,
+ semdexMutator semdex.Mutator,
+ semdexQuerier semdex.Querier,
) {
if cfg.SemdexProvider == "" {
return
@@ -73,9 +71,8 @@ func newSemdexer(
threadWriter: threadQuerier,
indexQueue: indexQueue,
deleteQueue: deleteQueue,
- indexer: indexer,
- deleter: deleter,
- retriever: retriever,
+ semdexMutator: semdexMutator,
+ semdexQuerier: semdexQuerier,
}
lc.Append(fx.StartHook(func(_ context.Context) error {
diff --git a/app/transports/http/openapi/server_gen.go b/app/transports/http/openapi/server_gen.go
index ee18a69b2..552942e44 100644
--- a/app/transports/http/openapi/server_gen.go
+++ b/app/transports/http/openapi/server_gen.go
@@ -111,6 +111,7 @@ const (
// Defines values for InstanceCapability.
const (
EmailClient InstanceCapability = "email_client"
+ GenAi InstanceCapability = "gen_ai"
Semdex InstanceCapability = "semdex"
SmsClient InstanceCapability = "sms_client"
)
@@ -29929,224 +29930,224 @@ var swaggerSpec = []string{
"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",
- "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",
- "AA==",
+ "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",
+ "K568JwIA",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/go.mod b/go.mod
index 1983bf532..1146e1768 100644
--- a/go.mod
+++ b/go.mod
@@ -130,6 +130,7 @@ require (
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
+ github.com/openai/openai-go v0.1.0-alpha.39 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/philippgille/chromem-go v0.7.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
@@ -140,6 +141,10 @@ require (
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect
+ github.com/tidwall/gjson v1.14.4 // indirect
+ github.com/tidwall/match v1.1.1 // indirect
+ github.com/tidwall/pretty v1.2.1 // indirect
+ github.com/tidwall/sjson v1.2.5 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
diff --git a/go.sum b/go.sum
index f3a51350d..acef9ffe0 100644
--- a/go.sum
+++ b/go.sum
@@ -395,6 +395,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
+github.com/openai/openai-go v0.1.0-alpha.39 h1:FvoNWy7BPhA0TjGOK5huRGU5sAUEx2jeubLXz34K9LE=
+github.com/openai/openai-go v0.1.0-alpha.39/go.mod h1:3SdE6BffOX9HPEQv8IL/fi3LYZ5TUpRYaqGQZbyk11A=
github.com/pb33f/libopenapi v0.18.6 h1:adxzZUnOBOAuKxFAIrtb1Qt8GA4XnDWUAxEnqiSoTh0=
github.com/pb33f/libopenapi v0.18.6/go.mod h1:qZRs2IHIcs9SjHPmQfSUCyeD3OY9JkLJQOuFxd0bYCY=
github.com/pb33f/libopenapi v0.18.7 h1:gLD4gQ88zEqv7x13SDzk3AUdpHUp9gWrP1NDwrFTy+U=
@@ -448,6 +450,10 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg=
github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
@@ -473,6 +479,16 @@ github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmc
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
+github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
+github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
diff --git a/internal/config/config.go b/internal/config/config.go
index b9bbda7df..ee5ac4570 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -47,11 +47,13 @@ type Config struct {
QueueType string `envconfig:"QUEUE_TYPE" default:"internal"`
AmqpURL string `envconfig:"AMQP_URL" default:"amqp://guest:guest@localhost:5672/"`
+ LanguageModelProvider string `envconfig:"LANGUAGE_MODEL_PROVIDER"`
+ OpenAIKey string `envconfig:"OPENAI_API_KEY"`
+
SemdexProvider string `envconfig:"SEMDEX_PROVIDER" default:""`
WeaviateURL string `envconfig:"WEAVIATE_URL"`
WeaviateToken string `envconfig:"WEAVIATE_API_TOKEN"`
WeaviateClassName string `envconfig:"WEAVIATE_CLASS_NAME"`
- OpenAIKey string `envconfig:"OPENAI_API_KEY"`
SemdexLocalPath string `envconfig:"SEMDEX_LOCAL_PATH" default:"data/semdex"`
}
diff --git a/internal/infrastructure/ai/ai.go b/internal/infrastructure/ai/ai.go
new file mode 100644
index 000000000..05f8635f3
--- /dev/null
+++ b/internal/infrastructure/ai/ai.go
@@ -0,0 +1,31 @@
+package ai
+
+import (
+ "context"
+
+ "github.com/Southclaws/storyden/internal/config"
+)
+
+type Result struct {
+ Answer string
+}
+
+type Prompter interface {
+ Prompt(ctx context.Context, input string) (*Result, error)
+}
+
+func New(cfg config.Config) (Prompter, error) {
+ switch cfg.LanguageModelProvider {
+ case "openai":
+ return newOpenAI(cfg)
+
+ default:
+ return &Disabled{}, nil
+ }
+}
+
+type Disabled struct{}
+
+func (d *Disabled) Prompt(ctx context.Context, input string) (*Result, error) {
+ return nil, nil
+}
diff --git a/internal/infrastructure/ai/openai.go b/internal/infrastructure/ai/openai.go
new file mode 100644
index 000000000..42109fd44
--- /dev/null
+++ b/internal/infrastructure/ai/openai.go
@@ -0,0 +1,41 @@
+package ai
+
+import (
+ "context"
+
+ "github.com/openai/openai-go"
+ "github.com/openai/openai-go/option"
+
+ "github.com/Southclaws/fault"
+ "github.com/Southclaws/fault/fctx"
+ "github.com/Southclaws/storyden/internal/config"
+)
+
+type OpenAI struct {
+ client *openai.Client
+}
+
+func newOpenAI(cfg config.Config) (*OpenAI, error) {
+ client := openai.NewClient(option.WithAPIKey(cfg.OpenAIKey))
+ return &OpenAI{client: client}, nil
+}
+
+func (o *OpenAI) Prompt(ctx context.Context, input string) (*Result, error) {
+ res, err := o.client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
+ Model: openai.F(openai.ChatModelChatgpt4oLatest),
+ Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
+ openai.UserMessage(input),
+ }),
+ })
+ if err != nil {
+ return nil, fault.Wrap(err, fctx.With(ctx))
+ }
+
+ if len(res.Choices) == 0 {
+ return nil, fault.New("result is empty")
+ }
+
+ return &Result{
+ Answer: res.Choices[0].Message.Content,
+ }, nil
+}
diff --git a/internal/infrastructure/infrastructure.go b/internal/infrastructure/infrastructure.go
index ff25945c5..dd6b565db 100644
--- a/internal/infrastructure/infrastructure.go
+++ b/internal/infrastructure/infrastructure.go
@@ -4,6 +4,7 @@ package infrastructure
import (
"go.uber.org/fx"
+ "github.com/Southclaws/storyden/internal/infrastructure/ai"
"github.com/Southclaws/storyden/internal/infrastructure/cache"
"github.com/Southclaws/storyden/internal/infrastructure/db"
"github.com/Southclaws/storyden/internal/infrastructure/endec/jwt"
@@ -33,6 +34,7 @@ func Build() fx.Option {
object.Build(),
frontend.Build(),
weaviate.Build(),
+ fx.Provide(ai.New),
jwt.Build(),
queue.Build(),
fx.Provide(pdf.New),
diff --git a/web/package.json b/web/package.json
index 2f5cd9f59..e8f4f934d 100644
--- a/web/package.json
+++ b/web/package.json
@@ -82,7 +82,7 @@
"@typescript-eslint/parser": "^8.8.1",
"eslint": "^8.57.1",
"eslint-config-next": "15.0.0",
- "orval": "^7.2.0",
+ "orval": "7.2.0",
"prettier": "^3.3.3",
"typescript": "^5.6.2"
},
diff --git a/web/src/api/openapi-schema/instanceCapability.ts b/web/src/api/openapi-schema/instanceCapability.ts
index d6e34186e..57bae41fa 100644
--- a/web/src/api/openapi-schema/instanceCapability.ts
+++ b/web/src/api/openapi-schema/instanceCapability.ts
@@ -13,6 +13,7 @@ export type InstanceCapability =
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const InstanceCapability = {
+ gen_ai: "gen_ai",
semdex: "semdex",
email_client: "email_client",
sms_client: "sms_client",
diff --git a/web/src/components/library/LibraryPageTagsList/LibraryPageTagsList.tsx b/web/src/components/library/LibraryPageTagsList/LibraryPageTagsList.tsx
index 0fd9f030f..5c72edfff 100644
--- a/web/src/components/library/LibraryPageTagsList/LibraryPageTagsList.tsx
+++ b/web/src/components/library/LibraryPageTagsList/LibraryPageTagsList.tsx
@@ -17,7 +17,7 @@ export type Props = {
};
export function LibraryPageTagsList(props: Props) {
- const isSuggestEnabled = useCapability(InstanceCapability.semdex);
+ const isSuggestEnabled = useCapability(InstanceCapability.gen_ai);
const { updateNode, suggestTags, revalidate } = useLibraryMutation(
props.node,
diff --git a/web/yarn.lock b/web/yarn.lock
index 0d04c567e..e61da85f4 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -972,7 +972,7 @@ __metadata:
languageName: node
linkType: hard
-"@ibm-cloud/openapi-ruleset@npm:^1.25.1":
+"@ibm-cloud/openapi-ruleset@npm:^1.14.2":
version: 1.25.1
resolution: "@ibm-cloud/openapi-ruleset@npm:1.25.1"
dependencies:
@@ -1401,35 +1401,35 @@ __metadata:
languageName: node
linkType: hard
-"@orval/angular@npm:7.3.0":
- version: 7.3.0
- resolution: "@orval/angular@npm:7.3.0"
+"@orval/angular@npm:7.2.0":
+ version: 7.2.0
+ resolution: "@orval/angular@npm:7.2.0"
dependencies:
- "@orval/core": "npm:7.3.0"
- checksum: 10c0/204de5c970eeb384005adba6d29f105db57742f0de3de413e8ed02baacac69b73dd45fe0f63946722eaf52dab4d3e67320b3e80c2aaac6134a20e1eeb304deb9
+ "@orval/core": "npm:7.2.0"
+ checksum: 10c0/9121b14cba2b6532bd549dd44ada843b15630b01b179534f09f78a426ce177c2f50fdc98a2a44f6bfa2133aae3057b65b417d48196300dfe3263caf77bb2bdfe
languageName: node
linkType: hard
-"@orval/axios@npm:7.3.0":
- version: 7.3.0
- resolution: "@orval/axios@npm:7.3.0"
+"@orval/axios@npm:7.2.0":
+ version: 7.2.0
+ resolution: "@orval/axios@npm:7.2.0"
dependencies:
- "@orval/core": "npm:7.3.0"
- checksum: 10c0/d71fd8ac5f81958eb0531e835b0cd67b06f7e570f0819484f041ddbac5d7c2a0c9aae91696f783ca0323bbb18e371e0fe98b4bf55217e688723377bcc359fb60
+ "@orval/core": "npm:7.2.0"
+ checksum: 10c0/9df63f12dcfe15f2c5c2aac59d020b48fa926e4d2a5a5291cea1a18faea1b6d3fe1ce743ad3bc7550f8641101a6ef8d7df2f5e9c5bfe989e1477f28d906266be
languageName: node
linkType: hard
-"@orval/core@npm:7.3.0":
- version: 7.3.0
- resolution: "@orval/core@npm:7.3.0"
+"@orval/core@npm:7.2.0":
+ version: 7.2.0
+ resolution: "@orval/core@npm:7.2.0"
dependencies:
"@apidevtools/swagger-parser": "npm:^10.1.0"
- "@ibm-cloud/openapi-ruleset": "npm:^1.25.1"
- acorn: "npm:^8.14.0"
- ajv: "npm:^8.17.1"
+ "@ibm-cloud/openapi-ruleset": "npm:^1.14.2"
+ acorn: "npm:^8.11.2"
+ ajv: "npm:^8.12.0"
chalk: "npm:^4.1.2"
- compare-versions: "npm:^6.1.1"
- debug: "npm:^4.3.7"
+ compare-versions: "npm:^6.1.0"
+ debug: "npm:^4.3.4"
esbuild: "npm:^0.24.0"
esutils: "npm:2.0.3"
fs-extra: "npm:^11.2.0"
@@ -1440,73 +1440,73 @@ __metadata:
lodash.uniq: "npm:^4.5.0"
lodash.uniqby: "npm:^4.7.0"
lodash.uniqwith: "npm:^4.5.0"
- micromatch: "npm:^4.0.8"
- openapi3-ts: "npm:4.4.0"
+ micromatch: "npm:^4.0.5"
+ openapi3-ts: "npm:4.2.2"
swagger2openapi: "npm:^7.0.8"
- checksum: 10c0/cd880b3f4406563efca134e4a609b8dbde46f39fb3b0ba536264beae628f7b5379f2407aab65b9e6466eacf32c478eec0e3406d39edfdb452c002d7d7de1d14f
+ checksum: 10c0/a5a70118219bef1b6922bfce77d172ddc85f9d93120ea7569222b627ea75c717a878b5ed2ab3e7842cfbb8301e8dca5ad4411c73b37fd40011c2a5675531160a
languageName: node
linkType: hard
-"@orval/fetch@npm:7.3.0":
- version: 7.3.0
- resolution: "@orval/fetch@npm:7.3.0"
+"@orval/fetch@npm:7.2.0":
+ version: 7.2.0
+ resolution: "@orval/fetch@npm:7.2.0"
dependencies:
- "@orval/core": "npm:7.3.0"
- checksum: 10c0/b57613b2ab08a36ebf89590a9927e82e47e409314864e71012a078a44ac4ea136ec60745eb85e5ac69e0ad1e56631e2b931d880d7776cffb1507df117a97503a
+ "@orval/core": "npm:7.2.0"
+ checksum: 10c0/80174ed73d8872b5d59650cea3b32bd3f8f9a3cf8a121602fa6059d99d07ada2c7afd0bd6b9f18f7501ff0ab3c699197e8fdcb341125f51ab6ca5133192d1187
languageName: node
linkType: hard
-"@orval/hono@npm:7.3.0":
- version: 7.3.0
- resolution: "@orval/hono@npm:7.3.0"
+"@orval/hono@npm:7.2.0":
+ version: 7.2.0
+ resolution: "@orval/hono@npm:7.2.0"
dependencies:
- "@orval/core": "npm:7.3.0"
- "@orval/zod": "npm:7.3.0"
+ "@orval/core": "npm:7.2.0"
+ "@orval/zod": "npm:7.2.0"
lodash.uniq: "npm:^4.5.0"
- checksum: 10c0/f9ca0063fdfbd511d966dea38de207cba5ed3bdc18c2961d0b27814cafe63aa3b0c144f4f059022e2d3f9460ff92ac7b28dca6f9513fcf76259fe7e1b8e323fa
+ checksum: 10c0/21bd1f14fe55c6edb68d01644cec0552dc03aefcf2a18ce86b8d15e4afbbc2e9b11e85cd85402a15a0489f99e9dfb1ac47d7ee419ad4a1ceebe48872ab3d384e
languageName: node
linkType: hard
-"@orval/mock@npm:7.3.0":
- version: 7.3.0
- resolution: "@orval/mock@npm:7.3.0"
+"@orval/mock@npm:7.2.0":
+ version: 7.2.0
+ resolution: "@orval/mock@npm:7.2.0"
dependencies:
- "@orval/core": "npm:7.3.0"
+ "@orval/core": "npm:7.2.0"
lodash.get: "npm:^4.4.2"
lodash.omit: "npm:^4.5.0"
openapi3-ts: "npm:^4.2.2"
- checksum: 10c0/50a70cae7047578c168b211c22461e207b6230701fca547b0d72b16d8a11a3769f3b0b1f54ef155a416a00afd21cbfcc8712b9dd0c33d397cc0c15380758b0e1
+ checksum: 10c0/94e15ec5a952e1b08f32bfa56cde7f244ab6117aa89142e7801e5b2ee0f2a17fdd3180841e9a3e5362101cfc30524191b9b5631b95a97a464e175c8a0c715258
languageName: node
linkType: hard
-"@orval/query@npm:7.3.0":
- version: 7.3.0
- resolution: "@orval/query@npm:7.3.0"
+"@orval/query@npm:7.2.0":
+ version: 7.2.0
+ resolution: "@orval/query@npm:7.2.0"
dependencies:
- "@orval/core": "npm:7.3.0"
- "@orval/fetch": "npm:7.3.0"
+ "@orval/core": "npm:7.2.0"
+ "@orval/fetch": "npm:7.2.0"
lodash.omitby: "npm:^4.6.0"
- checksum: 10c0/5ad9d1eaa14807bd112daf45da9f7ab2c7330817abd0f591bc965d11d87d83558a54a6434a0f379e859a096766158ef2d354fd919022af7142c142cc809b5728
+ checksum: 10c0/5eda8de4f26fa5764e5c2a7bdbd4460430fbd3b2a070969ff7e6e79ca9d0119cdd3d49ad87617f373030588a182d3d27ce62125870dad4788b5df3f1c318de4c
languageName: node
linkType: hard
-"@orval/swr@npm:7.3.0":
- version: 7.3.0
- resolution: "@orval/swr@npm:7.3.0"
+"@orval/swr@npm:7.2.0":
+ version: 7.2.0
+ resolution: "@orval/swr@npm:7.2.0"
dependencies:
- "@orval/core": "npm:7.3.0"
- "@orval/fetch": "npm:7.3.0"
- checksum: 10c0/1d87d5ac60a93c5ff4a19215f3b11e8e34bccd91c0c5aa63de18af03f0d5e2d66c01ed40541f89577765812b11d26f9431c19f7541eba57373a2c3656e35c75e
+ "@orval/core": "npm:7.2.0"
+ "@orval/fetch": "npm:7.2.0"
+ checksum: 10c0/a64f3cf579904c8f8816a98290a34c77f8403aad88631a256fb005214bca8d26512f734bebc76bb546954c6fe7882a8832e3ae14f6ccd9a7514dedf03e19b7b1
languageName: node
linkType: hard
-"@orval/zod@npm:7.3.0":
- version: 7.3.0
- resolution: "@orval/zod@npm:7.3.0"
+"@orval/zod@npm:7.2.0":
+ version: 7.2.0
+ resolution: "@orval/zod@npm:7.2.0"
dependencies:
- "@orval/core": "npm:7.3.0"
+ "@orval/core": "npm:7.2.0"
lodash.uniq: "npm:^4.5.0"
- checksum: 10c0/92d3e7426ddfbe73bd2d73642659090a9f626b8fc4b602ceddfbfdaf3b50039429e4f882445cf4b09a004572886d1251b95051b69f659bb8fe7b029933695f77
+ checksum: 10c0/a87321f66e3f077f3e7880923784c2f94c8feea08f966174f18ccd61771fcda0104b5b1bce9199bfb683cda66e2e4241874d7b9a80ce2521ea2d50fdad967228
languageName: node
linkType: hard
@@ -4758,7 +4758,7 @@ __metadata:
languageName: node
linkType: hard
-"acorn@npm:^8.14.0, acorn@npm:^8.9.0":
+"acorn@npm:^8.11.2, acorn@npm:^8.14.0, acorn@npm:^8.9.0":
version: 8.14.0
resolution: "acorn@npm:8.14.0"
bin:
@@ -5423,7 +5423,7 @@ __metadata:
languageName: node
linkType: hard
-"compare-versions@npm:^6.1.1":
+"compare-versions@npm:^6.1.0":
version: 6.1.1
resolution: "compare-versions@npm:6.1.1"
checksum: 10c0/415205c7627f9e4f358f571266422980c9fe2d99086be0c9a48008ef7c771f32b0fbe8e97a441ffedc3910872f917a0675fe0fe3c3b6d331cda6d8690be06338
@@ -5559,7 +5559,7 @@ __metadata:
languageName: node
linkType: hard
-"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.7":
+"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5":
version: 4.3.7
resolution: "debug@npm:4.3.7"
dependencies:
@@ -8307,7 +8307,7 @@ __metadata:
languageName: node
linkType: hard
-"micromatch@npm:^4.0.4, micromatch@npm:^4.0.8":
+"micromatch@npm:^4.0.4, micromatch@npm:^4.0.5":
version: 4.0.8
resolution: "micromatch@npm:4.0.8"
dependencies:
@@ -8872,7 +8872,7 @@ __metadata:
languageName: node
linkType: hard
-"openapi3-ts@npm:4.4.0, openapi3-ts@npm:^4.2.2":
+"openapi3-ts@npm:^4.2.2":
version: 4.4.0
resolution: "openapi3-ts@npm:4.4.0"
dependencies:
@@ -8902,20 +8902,20 @@ __metadata:
languageName: node
linkType: hard
-"orval@npm:^7.2.0":
- version: 7.3.0
- resolution: "orval@npm:7.3.0"
+"orval@npm:7.2.0":
+ version: 7.2.0
+ resolution: "orval@npm:7.2.0"
dependencies:
"@apidevtools/swagger-parser": "npm:^10.1.0"
- "@orval/angular": "npm:7.3.0"
- "@orval/axios": "npm:7.3.0"
- "@orval/core": "npm:7.3.0"
- "@orval/fetch": "npm:7.3.0"
- "@orval/hono": "npm:7.3.0"
- "@orval/mock": "npm:7.3.0"
- "@orval/query": "npm:7.3.0"
- "@orval/swr": "npm:7.3.0"
- "@orval/zod": "npm:7.3.0"
+ "@orval/angular": "npm:7.2.0"
+ "@orval/axios": "npm:7.2.0"
+ "@orval/core": "npm:7.2.0"
+ "@orval/fetch": "npm:7.2.0"
+ "@orval/hono": "npm:7.2.0"
+ "@orval/mock": "npm:7.2.0"
+ "@orval/query": "npm:7.2.0"
+ "@orval/swr": "npm:7.2.0"
+ "@orval/zod": "npm:7.2.0"
ajv: "npm:^8.12.0"
cac: "npm:^6.7.14"
chalk: "npm:^4.1.2"
@@ -8930,7 +8930,7 @@ __metadata:
tsconfck: "npm:^2.0.1"
bin:
orval: dist/bin/orval.js
- checksum: 10c0/052553afca54f6a7149dbdbb19c953f4d168c5d07c4c5fa50f5465a5454947e7f7bc0f779a0f688affe23e8f1891eb7b9f1fa9a6d8e5db866f0cef3a099ee901
+ checksum: 10c0/fb7e6fcd8feefec5367b417d4a79d42b9a57b214582761cc3bea2803754587a5d6e73a448ca6cdf5f1fabb008426524cc5d062e9d794ec7228fb1a5282263fb2
languageName: node
linkType: hard
@@ -11010,7 +11010,7 @@ __metadata:
mime-db: "npm:^1.53.0"
next: "npm:15.0.0"
nuqs: "npm:^1.20.0"
- orval: "npm:^7.2.0"
+ orval: "npm:7.2.0"
polished: "npm:^4.3.1"
prettier: "npm:^3.3.3"
react: "npm:19.0.0-rc-65a56d0e-20241020"