separated bind code with contact code, bind code is used for account merging, by single user, contact code is used to add fiends and can be shared
This commit is contained in:
+11
-9
@@ -9,6 +9,14 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserLookup int
|
||||
|
||||
const (
|
||||
UserLookupByID UserLookup = iota
|
||||
UserLookupByBindCode
|
||||
UserLookupByContactCode
|
||||
)
|
||||
|
||||
const DatabaseSource = "database"
|
||||
|
||||
type Database interface {
|
||||
@@ -22,9 +30,8 @@ type Database interface {
|
||||
}
|
||||
|
||||
type Users interface {
|
||||
Get(ctx context.Context, id uuid.UUID, lookup UserLookup) (*User, error)
|
||||
Create(ctx context.Context, user *User) error
|
||||
Get(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
GetByTempCode(ctx context.Context, code uuid.UUID) (*User, error)
|
||||
Update(ctx context.Context, user *User) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
@@ -61,7 +68,6 @@ type Actions interface {
|
||||
Recent(ctx context.Context, userID uuid.UUID, limit int) ([]Action, error)
|
||||
}
|
||||
|
||||
// User represents a Jules user.
|
||||
type User struct {
|
||||
ID uuid.UUID
|
||||
Language string
|
||||
@@ -71,31 +77,28 @@ type User struct {
|
||||
SearchCount int
|
||||
NotificationCount int
|
||||
CountUpdatedAt time.Time
|
||||
TemporaryCode uuid.UUID
|
||||
BindCode uuid.UUID
|
||||
ContactCode uuid.UUID
|
||||
Role string
|
||||
}
|
||||
|
||||
// Chat links a user to an external messaging platform.
|
||||
type Chat struct {
|
||||
UserID uuid.UUID
|
||||
Platform string // "telegram", "email", "whatsapp"
|
||||
Identifier string // @username, email, phone
|
||||
}
|
||||
|
||||
// Fact stores fact Jules knows about a user.
|
||||
type Fact struct {
|
||||
UserID uuid.UUID
|
||||
Value string
|
||||
}
|
||||
|
||||
// Contact represents a relationship between two Jules users.
|
||||
type Contact struct {
|
||||
OwnerID uuid.UUID // User who owns this contact
|
||||
TargetID uuid.UUID // Target user ID
|
||||
Name string // "mom", "brother", "Lena"
|
||||
}
|
||||
|
||||
// Notification is a scheduled reminder or check-in.
|
||||
type Notification struct {
|
||||
ID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
@@ -104,7 +107,6 @@ type Notification struct {
|
||||
Content string
|
||||
}
|
||||
|
||||
// Action records an interaction between Jules and a user.
|
||||
type Action struct {
|
||||
UserID uuid.UUID
|
||||
ExecutedAt time.Time
|
||||
|
||||
@@ -7,11 +7,13 @@ CREATE TABLE users (
|
||||
search_count INTEGER NOT NULL,
|
||||
notification_count INTEGER NOT NULL,
|
||||
count_updated_at DATE NOT NULL,
|
||||
temporary_code UUID NOT NULL,
|
||||
bind_code UUID NOT NULL,
|
||||
contact_code UUID NOT NULL,
|
||||
role TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_temporary_code ON users(temporary_code);
|
||||
CREATE INDEX idx_users_bind_code ON users(bind_code);
|
||||
CREATE INDEX idx_users_contact_code ON users(contact_code);
|
||||
|
||||
CREATE TABLE chats (
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
+29
-33
@@ -15,46 +15,42 @@ type Users struct {
|
||||
|
||||
func (u *Users) Create(ctx context.Context, user *database.User) error {
|
||||
_, err := u.conn.ExecContext(ctx, `
|
||||
INSERT INTO users (id, preferred_chat, language, timezone, llm_count, search_count, notification_count, count_updated_at, temporary_code, role)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
INSERT INTO users (
|
||||
id, preferred_chat, language, timezone,
|
||||
llm_count, search_count, notification_count,
|
||||
count_updated_at, bind_code, contact_code, role
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`, user.ID, user.PreferredChat, user.Language, user.Timezone,
|
||||
user.LLMCount, user.SearchCount, user.NotificationCount,
|
||||
user.CountUpdatedAt, user.TemporaryCode, user.Role)
|
||||
user.CountUpdatedAt, user.BindCode, user.ContactCode, user.Role)
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *Users) Get(ctx context.Context, id uuid.UUID) (*database.User, error) {
|
||||
func (u *Users) Get(ctx context.Context, key uuid.UUID, lookup database.UserLookup) (*database.User, error) {
|
||||
var column string
|
||||
switch lookup {
|
||||
case database.UserLookupByID:
|
||||
column = "id"
|
||||
case database.UserLookupByBindCode:
|
||||
column = "bind_code"
|
||||
case database.UserLookupByContactCode:
|
||||
column = "contact_code"
|
||||
default:
|
||||
return nil, errors.New("invalid lookup type")
|
||||
}
|
||||
|
||||
var user database.User
|
||||
err := u.conn.QueryRowContext(ctx, `
|
||||
SELECT id, preferred_chat, language, timezone, llm_count, search_count, notification_count, count_updated_at, temporary_code, role
|
||||
SELECT id, preferred_chat, language, timezone,
|
||||
llm_count, search_count, notification_count,
|
||||
count_updated_at, bind_code, contact_code, role
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`, id).Scan(
|
||||
WHERE `+column+` = $1
|
||||
`, key).Scan(
|
||||
&user.ID, &user.PreferredChat, &user.Language, &user.Timezone,
|
||||
&user.LLMCount, &user.SearchCount, &user.NotificationCount,
|
||||
&user.CountUpdatedAt, &user.TemporaryCode, &user.Role,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, database.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (u *Users) GetByTempCode(ctx context.Context, code uuid.UUID) (*database.User, error) {
|
||||
var user database.User
|
||||
err := u.conn.QueryRowContext(ctx, `
|
||||
SELECT id, language, timezone, preferred_chat,
|
||||
llm_count, search_count, notification_count,
|
||||
count_updated_at, temporary_code, role
|
||||
FROM users
|
||||
WHERE temporary_code = $1
|
||||
`, code).Scan(
|
||||
&user.ID, &user.Language, &user.Timezone, &user.PreferredChat,
|
||||
&user.LLMCount, &user.SearchCount, &user.NotificationCount,
|
||||
&user.CountUpdatedAt, &user.TemporaryCode, &user.Role,
|
||||
&user.CountUpdatedAt, &user.BindCode, &user.ContactCode, &user.Role,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, database.ErrNotFound
|
||||
@@ -70,11 +66,11 @@ func (u *Users) Update(ctx context.Context, user *database.User) error {
|
||||
UPDATE users
|
||||
SET preferred_chat = $1, language = $2, timezone = $3,
|
||||
llm_count = $4, search_count = $5, notification_count = $6,
|
||||
count_updated_at = $7, temporary_code = $8, role = $9
|
||||
WHERE id = $10
|
||||
count_updated_at = $7, bind_code = $8, contact_code = $9, role = $10
|
||||
WHERE id = $11
|
||||
`, user.PreferredChat, user.Language, user.Timezone,
|
||||
user.LLMCount, user.SearchCount, user.NotificationCount,
|
||||
user.CountUpdatedAt, user.TemporaryCode, user.Role, user.ID)
|
||||
user.CountUpdatedAt, user.BindCode, user.ContactCode, user.Role, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -20,8 +19,9 @@ func setupTestUser(t *testing.T, db *DB) *database.User {
|
||||
LLMCount: 0,
|
||||
SearchCount: 0,
|
||||
NotificationCount: 0,
|
||||
CountUpdatedAt: time.Now(),
|
||||
TemporaryCode: uuid.New(),
|
||||
CountUpdatedAt: time.Now().Truncate(24 * time.Hour),
|
||||
BindCode: uuid.New(),
|
||||
ContactCode: uuid.New(),
|
||||
Role: "free",
|
||||
}
|
||||
err := db.users.Create(t.Context(), u)
|
||||
@@ -35,7 +35,8 @@ func TestUsers_Create(t *testing.T) {
|
||||
user := setupTestUser(t, db)
|
||||
|
||||
assert.NotEqual(t, uuid.Nil, user.ID)
|
||||
assert.NotEqual(t, uuid.Nil, user.TemporaryCode)
|
||||
assert.NotEqual(t, uuid.Nil, user.BindCode)
|
||||
assert.NotEqual(t, uuid.Nil, user.ContactCode)
|
||||
assert.Equal(t, "telegram", user.PreferredChat)
|
||||
assert.Equal(t, "en", user.Language)
|
||||
assert.Equal(t, "UTC", user.Timezone)
|
||||
@@ -43,32 +44,48 @@ func TestUsers_Create(t *testing.T) {
|
||||
assert.Equal(t, 0, user.SearchCount)
|
||||
assert.Equal(t, 0, user.NotificationCount)
|
||||
assert.Equal(t, "free", user.Role)
|
||||
assert.True(t, time.Since(user.CountUpdatedAt) < time.Hour*24)
|
||||
}
|
||||
|
||||
func TestUsers_Get(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
user := setupTestUser(t, db)
|
||||
|
||||
t.Run("found", func(t *testing.T) {
|
||||
fetched, err := db.users.Get(t.Context(), user.ID)
|
||||
t.Run("by id", func(t *testing.T) {
|
||||
fetched, err := db.users.Get(t.Context(), user.ID, database.UserLookupByID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, fetched.ID)
|
||||
assert.Equal(t, user.PreferredChat, fetched.PreferredChat)
|
||||
assert.Equal(t, user.LLMCount, fetched.LLMCount)
|
||||
assert.Equal(t, user.Role, fetched.Role)
|
||||
})
|
||||
|
||||
t.Run("by bind code", func(t *testing.T) {
|
||||
fetched, err := db.users.Get(t.Context(), user.BindCode, database.UserLookupByBindCode)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, fetched.ID)
|
||||
assert.Equal(t, user.BindCode, fetched.BindCode)
|
||||
})
|
||||
|
||||
t.Run("by contact code", func(t *testing.T) {
|
||||
fetched, err := db.users.Get(t.Context(), user.ContactCode, database.UserLookupByContactCode)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, fetched.ID)
|
||||
assert.Equal(t, user.ContactCode, fetched.ContactCode)
|
||||
})
|
||||
|
||||
t.Run("by invalid type", func(t *testing.T) {
|
||||
var vv = database.UserLookupByContactCode
|
||||
vv = 6
|
||||
_, err := db.users.Get(t.Context(), user.ContactCode, vv)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid lookup type")
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
_, err := db.users.Get(t.Context(), uuid.New())
|
||||
_, err := db.users.Get(t.Context(), uuid.New(), database.UserLookupByID)
|
||||
assert.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsers_Update(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
user := setupTestUser(t, db)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
@@ -79,11 +96,13 @@ func TestUsers_Update(t *testing.T) {
|
||||
user.SearchCount = 20
|
||||
user.NotificationCount = 30
|
||||
user.Role = "premium"
|
||||
user.BindCode = uuid.New()
|
||||
user.ContactCode = uuid.New()
|
||||
|
||||
err := db.users.Update(t.Context(), user)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := db.users.Get(t.Context(), user.ID)
|
||||
fetched, err := db.users.Get(t.Context(), user.ID, database.UserLookupByID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "whatsapp", fetched.PreferredChat)
|
||||
assert.Equal(t, "ru", fetched.Language)
|
||||
@@ -92,6 +111,8 @@ func TestUsers_Update(t *testing.T) {
|
||||
assert.Equal(t, 20, fetched.SearchCount)
|
||||
assert.Equal(t, 30, fetched.NotificationCount)
|
||||
assert.Equal(t, "premium", fetched.Role)
|
||||
assert.Equal(t, user.BindCode, fetched.BindCode)
|
||||
assert.Equal(t, user.ContactCode, fetched.ContactCode)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
@@ -110,7 +131,7 @@ func TestUsers_Delete(t *testing.T) {
|
||||
err := db.users.Delete(t.Context(), user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.users.Get(t.Context(), user.ID)
|
||||
_, err = db.users.Get(t.Context(), user.ID, database.UserLookupByID)
|
||||
assert.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
@@ -124,7 +145,7 @@ func TestUsers_Get_DatabaseError(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
db.Close()
|
||||
|
||||
_, err := db.users.Get(t.Context(), uuid.New())
|
||||
_, err := db.users.Get(t.Context(), uuid.New(), database.UserLookupByID)
|
||||
assert.Error(t, err)
|
||||
assert.NotErrorIs(t, err, database.ErrNotFound)
|
||||
}
|
||||
@@ -148,28 +169,3 @@ func TestUsers_Delete_DatabaseError(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
assert.NotErrorIs(t, err, database.ErrNotFound)
|
||||
}
|
||||
|
||||
func TestUsers_GetByTempCode(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
|
||||
user := setupTestUser(t, db)
|
||||
|
||||
t.Run("found", func(t *testing.T) {
|
||||
fetched, err := db.Users().GetByTempCode(context.Background(), user.TemporaryCode)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, fetched.ID)
|
||||
assert.Equal(t, user.TemporaryCode, fetched.TemporaryCode)
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
_, err := db.Users().GetByTempCode(context.Background(), uuid.New())
|
||||
assert.ErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("database error", func(t *testing.T) {
|
||||
db.Close()
|
||||
_, err := db.Users().GetByTempCode(context.Background(), user.TemporaryCode)
|
||||
assert.Error(t, err)
|
||||
assert.NotErrorIs(t, err, database.ErrNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
+118
-19
@@ -4,18 +4,132 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/d1nch8g/jules/chat"
|
||||
"github.com/d1nch8g/jules/database"
|
||||
"github.com/d1nch8g/jules/engine/actions"
|
||||
"github.com/d1nch8g/jules/engine/log"
|
||||
"github.com/d1nch8g/jules/engine/prompt"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUUIDNotFound = errors.New("user with specified UUID not found")
|
||||
)
|
||||
func (e *Engine) validateActions(ctx context.Context, actionSlice []any, promptCtx prompt.Context) error {
|
||||
var errs []error
|
||||
|
||||
for _, actionAny := range actionSlice {
|
||||
switch action := actionAny.(type) {
|
||||
case actions.AddContact:
|
||||
_, err := e.Database.Users().Get(ctx, uuid.MustParse(action.UUID), database.UserLookupByContactCode)
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrNotFound) {
|
||||
errs = append(errs, fmt.Errorf("user with requested uuid %s not present in database, uuid might be wrong", action.UUID))
|
||||
} else {
|
||||
errs = append(errs, errors.New("unexpected db occured"))
|
||||
}
|
||||
}
|
||||
|
||||
case actions.AddNotification:
|
||||
found := slices.ContainsFunc(promptCtx.Contacts, func(c database.Contact) bool {
|
||||
return c.Name == action.Target
|
||||
})
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("contact target %s is invalid, provide exact name", action.Target))
|
||||
}
|
||||
|
||||
case actions.BindChat:
|
||||
_, err := e.Database.Users().Get(ctx, uuid.MustParse(action.UUID), database.UserLookupByBindCode)
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrNotFound) {
|
||||
errs = append(errs, fmt.Errorf("user with requested uuid %s not present in database, uuid might be wrong", action.UUID))
|
||||
} else {
|
||||
errs = append(errs, errors.New("unexpected db occured"))
|
||||
}
|
||||
}
|
||||
|
||||
case actions.Message:
|
||||
found := slices.ContainsFunc(promptCtx.Chats, func(c database.Chat) bool {
|
||||
return action.Platform == c.Platform
|
||||
})
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("message is invalid, platform %s might not connected for user", action.Platform))
|
||||
}
|
||||
|
||||
case actions.RemoveFact:
|
||||
found := slices.ContainsFunc(promptCtx.Facts, func(c database.Fact) bool {
|
||||
return action.Value == c.Value
|
||||
})
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("fact '%s' is invalid, platform might not connected for user", action.Value))
|
||||
}
|
||||
|
||||
case actions.RemoveNotification:
|
||||
allNotifications := append(promptCtx.IncomingNotifications, promptCtx.OutgoingNotificaions...)
|
||||
targetUUID := uuid.MustParse(action.UUID)
|
||||
found := slices.ContainsFunc(allNotifications, func(c database.Notification) bool {
|
||||
return c.ID == targetUUID
|
||||
})
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("notification with uuid %s is not found for user, might not be present", action.UUID))
|
||||
}
|
||||
|
||||
case actions.SetChat:
|
||||
found := slices.ContainsFunc(promptCtx.Chats, func(c database.Chat) bool {
|
||||
return action.Chat == c.Platform
|
||||
})
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("chat %s might not connected for user, not found", action.Chat))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (e *Engine) executeActions(ctx context.Context, actionSlice []any) error {
|
||||
for _, actionAny := range actionSlice {
|
||||
switch action := actionAny.(type) {
|
||||
case actions.BindChat:
|
||||
// TODO
|
||||
|
||||
case actions.Message:
|
||||
// TODO
|
||||
|
||||
case actions.Wait:
|
||||
// TODO
|
||||
|
||||
case actions.UpdateLang:
|
||||
// TODO
|
||||
|
||||
case actions.UpdateTZ:
|
||||
// TODO
|
||||
|
||||
case actions.SetChat:
|
||||
// TODO
|
||||
|
||||
case actions.AddFact:
|
||||
// TODO
|
||||
|
||||
case actions.RemoveFact:
|
||||
// TODO
|
||||
|
||||
case actions.AddContact:
|
||||
// TODO
|
||||
|
||||
case actions.AddNotification:
|
||||
// TODO
|
||||
|
||||
case actions.RemoveNotification:
|
||||
// TODO
|
||||
|
||||
case actions.Search:
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) executeBindChat(ctx context.Context, user *database.User, act actions.BindChat, msg chat.Message) error {
|
||||
targetUUID, err := uuid.Parse(act.UUID)
|
||||
@@ -23,21 +137,6 @@ func (e *Engine) executeBindChat(ctx context.Context, user *database.User, act a
|
||||
return fmt.Errorf("unable to parse uuid: %w", err)
|
||||
}
|
||||
|
||||
targetUser, err := e.Database.Users().GetByTempCode(ctx, targetUUID)
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrNotFound) {
|
||||
return ErrUUIDNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
targetUser.TemporaryCode = uuid.New()
|
||||
|
||||
return errors.Join(
|
||||
e.Database.Users().Delete(ctx, user.ID),
|
||||
e.Database.Chats().Attach(ctx, targetUser.ID, msg.Chat, msg.ID),
|
||||
e.Database.Users().Update(ctx, targetUser),
|
||||
)
|
||||
}
|
||||
|
||||
func (e *Engine) executeWait(act actions.Wait) {
|
||||
@@ -75,7 +174,7 @@ func (e *Engine) executeAddContact(ctx context.Context, user *database.User, act
|
||||
|
||||
targetUser, err := e.Database.Users().GetByTempCode(ctx, targetUUID)
|
||||
if err != nil {
|
||||
if errors.Is()
|
||||
// if errors.Is()
|
||||
}
|
||||
|
||||
return e.Database.Contacts().Add(ctx, &database.Contact{
|
||||
|
||||
+16
-75
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/d1nch8g/jules/chat"
|
||||
@@ -28,7 +27,8 @@ func (e *Engine) defaultProcessMessage(ctx context.Context, msg chat.Message) {
|
||||
ID: uuid.New(),
|
||||
PreferredChat: msg.Chat,
|
||||
CountUpdatedAt: time.Now(),
|
||||
TemporaryCode: uuid.New(),
|
||||
BindCode: uuid.New(),
|
||||
ContactCode: uuid.New(),
|
||||
Role: "free",
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (e *Engine) defaultProcessMessage(ctx context.Context, msg chat.Message) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
user, err = e.Database.Users().Get(ctx, userID)
|
||||
user, err = e.Database.Users().Get(ctx, userID, database.UserLookupByID)
|
||||
if err != nil {
|
||||
le.Error("failed to get user from database", err)
|
||||
return
|
||||
@@ -57,7 +57,7 @@ func (e *Engine) defaultProcessMessage(ctx context.Context, msg chat.Message) {
|
||||
func (e *Engine) defaultProcessNotification(ctx context.Context, notif database.Notification) {
|
||||
le := log.FromNotification(ctx, notif)
|
||||
|
||||
user, err := e.Database.Users().Get(ctx, notif.UserID)
|
||||
user, err := e.Database.Users().Get(ctx, notif.UserID, database.UserLookupByID)
|
||||
if err != nil {
|
||||
le.Error("failed to get user from database", err)
|
||||
return
|
||||
@@ -74,8 +74,14 @@ func (e *Engine) process(ctx context.Context, le *log.Event, user *database.User
|
||||
}
|
||||
le.Context(promptContext)
|
||||
|
||||
//TODO: finish
|
||||
actions, err := e.executePrompt(ctx, promptContext, user.Timezone, le)
|
||||
if err != nil {
|
||||
le.Error("failed to execute prompt", err)
|
||||
return
|
||||
}
|
||||
|
||||
//TODO: finish
|
||||
_ = actions
|
||||
}
|
||||
|
||||
func (e *Engine) collectPromptContext(ctx context.Context, user *database.User, source, message string) (*prompt.Context, error) {
|
||||
@@ -117,7 +123,8 @@ func (e *Engine) collectPromptContext(ctx context.Context, user *database.User,
|
||||
UserLanguage: user.Language,
|
||||
UserTimezone: user.Timezone,
|
||||
UserPreferredChat: user.PreferredChat,
|
||||
UserTemporaryCode: user.TemporaryCode,
|
||||
UserBindCode: user.BindCode,
|
||||
UserContactCode: user.ContactCode,
|
||||
Chats: chats,
|
||||
Facts: facts,
|
||||
Contacts: contacts,
|
||||
@@ -137,14 +144,14 @@ func (e *Engine) executePrompt(ctx context.Context, promptCtx *prompt.Context, t
|
||||
result, err := e.LLM.Process(ctx, promptString)
|
||||
cancel()
|
||||
if err != nil {
|
||||
promptCtx.ExecutionErrors = append(promptCtx.ExecutionErrors, err)
|
||||
promptCtx.Error = errors.Join(promptCtx.Error, err)
|
||||
le.Warn("first attempt to receive response from LLM api failed")
|
||||
continue
|
||||
}
|
||||
|
||||
actions, err := actions.Parse(result, timezone)
|
||||
if err != nil {
|
||||
promptCtx.ExecutionErrors = append(promptCtx.ExecutionErrors, err)
|
||||
promptCtx.Error = errors.Join(promptCtx.Error, err)
|
||||
le.LLMResponse(result)
|
||||
le.Warn("failed to parse response provided by llm")
|
||||
continue
|
||||
@@ -152,7 +159,7 @@ func (e *Engine) executePrompt(ctx context.Context, promptCtx *prompt.Context, t
|
||||
|
||||
err = e.validateActions(ctx, actions, *promptCtx)
|
||||
if err != nil {
|
||||
promptCtx.ExecutionErrors = append(promptCtx.ExecutionErrors, err)
|
||||
promptCtx.Error = errors.Join(promptCtx.Error, err)
|
||||
le.LLMResponse(result)
|
||||
le.Warn("failed to validate llm input")
|
||||
continue
|
||||
@@ -163,69 +170,3 @@ func (e *Engine) executePrompt(ctx context.Context, promptCtx *prompt.Context, t
|
||||
|
||||
return nil, errors.New("all attempt to receive LLM response failed")
|
||||
}
|
||||
|
||||
func (e *Engine) validateActions(ctx context.Context, actionSlice []any, promptCtx prompt.Context) error {
|
||||
var errs []error
|
||||
|
||||
for _, actionAny := range actionSlice {
|
||||
switch action := actionAny.(type) {
|
||||
case actions.AddContact:
|
||||
_, err := e.Database.Users().GetByTempCode(ctx, uuid.MustParse(action.UUID))
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrNotFound) {
|
||||
errs = append(errs, fmt.Errorf("user with requested uuid %s not present in database, uuid might be wrong", action.UUID))
|
||||
} else {
|
||||
errs = append(errs, errors.New("unexpected db occured"))
|
||||
}
|
||||
}
|
||||
case actions.AddNotification:
|
||||
found := slices.ContainsFunc(promptCtx.Contacts, func(c database.Contact) bool {
|
||||
return c.Name == action.Target
|
||||
})
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("contact target %s is invalid, provide exact name", action.Target))
|
||||
}
|
||||
case actions.BindChat:
|
||||
_, err := e.Database.Users().GetByTempCode(ctx, uuid.MustParse(action.UUID))
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrNotFound) {
|
||||
errs = append(errs, fmt.Errorf("user with requested uuid %s not present in database, uuid might be wrong", action.UUID))
|
||||
} else {
|
||||
errs = append(errs, errors.New("unexpected db occured"))
|
||||
}
|
||||
}
|
||||
case actions.Message:
|
||||
found := slices.ContainsFunc(promptCtx.Chats, func(c database.Chat) bool {
|
||||
return action.Platform == c.Platform
|
||||
})
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("message is invalid, platform %s might not connected for user", action.Platform))
|
||||
}
|
||||
case actions.RemoveFact:
|
||||
found := slices.ContainsFunc(promptCtx.Facts, func(c database.Fact) bool {
|
||||
return action.Value == c.Value
|
||||
})
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("fact '%s' is invalid, platform might not connected for user", action.Value))
|
||||
}
|
||||
case actions.RemoveNotification:
|
||||
allNotifications := append(promptCtx.IncomingNotifications, promptCtx.OutgoingNotificaions...)
|
||||
targetUUID := uuid.MustParse(action.UUID)
|
||||
found := slices.ContainsFunc(allNotifications, func(c database.Notification) bool {
|
||||
return c.ID == targetUUID
|
||||
})
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("notification with uuid %s is not found for user, might not be present", action.UUID))
|
||||
}
|
||||
case actions.SetChat:
|
||||
found := slices.ContainsFunc(promptCtx.Chats, func(c database.Chat) bool {
|
||||
return action.Chat == c.Platform
|
||||
})
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("chat %s might not connected for user, not found", action.Chat))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
+18
-13
@@ -17,8 +17,10 @@ THIS PROMPT IS USED FOR BOTH NOTIFICATIONS AND MESSAGE PROCESSING.
|
||||
|
||||
=== ADDING VARIOUS CHATS ===
|
||||
- Jules is multiplatform, currently supported: "telegram"
|
||||
- When new user comes without much info being set up - ask him if he already uses jules
|
||||
- If user already uses Jules - ask for temporary code, and use bind_chat command
|
||||
- When a new user comes without much info, ask if they already use Jules.
|
||||
- If they do, ask for their BIND CODE,
|
||||
- BIND CODE is used ONLY to link another chat to the SAME user.
|
||||
- When giving user's BIND CODE - always ask to NOT share it with others.
|
||||
|
||||
=== FACTS MANAGEMENT ===
|
||||
Facts are long-term memories about the user. Use them to personalize interactions.
|
||||
@@ -49,7 +51,7 @@ FORMAT: Keep facts short. One fact per value. "mom name is Irina", "Likes fitnes
|
||||
- If sending UUID to user - always in a separate message
|
||||
|
||||
=== ONBOARDING & CAPABILITIES ===
|
||||
- If language is empty - you should try to define it by received message and set
|
||||
- If language is empty, try to define it from the received message and set it.
|
||||
- Each user has an "Informed about Jules capabilities" fact.
|
||||
- If this fact is MISSING, you MUST proactively ask (in the user's language):
|
||||
"Hey, btw, want me to tell you what I can help with?"
|
||||
@@ -57,7 +59,7 @@ FORMAT: Keep facts short. One fact per value. "mom name is Irina", "Likes fitnes
|
||||
- Birthday reminders
|
||||
- Cooking timers
|
||||
- Recurring action reminders (weekly workouts, daily pills)
|
||||
- Adding contacts: the user can request their contact code and share it with a friend—you'll add them.
|
||||
- Adding contacts: the user can request their CONTACT CODE and share it with a friend. The friend can then use "add_contact" with this code to connect.
|
||||
- After explaining, ADD the fact "Informed about Jules capabilities".
|
||||
|
||||
=== BEHAVIOR RULES ===
|
||||
@@ -79,11 +81,14 @@ FORMAT: Keep facts short. One fact per value. "mom name is Irina", "Likes fitnes
|
||||
=== TECHNICAL RULES ===
|
||||
- Return ONLY a valid JSON array of actions: [{"type": "...", ...}, ...]
|
||||
- All times MUST be in the user's local timezone in format "2006-01-02 15:04".
|
||||
- All language codes should be parsable by the system, by golang language.
|
||||
- For notifications, specify in content if ONE-TIME or RECURRING.
|
||||
- When user is asking to make interaction with other user - that user should be in contacts, otherwise - reject
|
||||
- When user is asking to send a message to other user - that can be done via notification, with time set to past, details provided in content
|
||||
- Start from actions related to DB interaction (without delays), finish with user messages (first no delay, second third with some indent)
|
||||
- If you received errors - try to figure out wether you can fix them by yourself, or you need to inform user (invalid UUID/non existing user)
|
||||
- BIND CODE is private and used for "bind_chat". CONTACT CODE is public and used for "add_contact".
|
||||
- Always send any UUID (bind or contact) in a SEPARATE message, not bundled with other text.
|
||||
|
||||
=== AVAILABLE ACTIONS ===
|
||||
{"type": "message", "platform": "telegram", "text": "short response"}
|
||||
@@ -106,7 +111,8 @@ type Context struct {
|
||||
UserLanguage string
|
||||
UserTimezone string
|
||||
UserPreferredChat string
|
||||
UserTemporaryCode uuid.UUID
|
||||
UserBindCode uuid.UUID
|
||||
UserContactCode uuid.UUID
|
||||
|
||||
Chats []database.Chat
|
||||
Facts []database.Fact
|
||||
@@ -118,7 +124,7 @@ type Context struct {
|
||||
MessageSource string
|
||||
MessageContent string
|
||||
|
||||
ExecutionErrors []error
|
||||
Error error
|
||||
}
|
||||
|
||||
func Build(ctx Context) string {
|
||||
@@ -138,7 +144,8 @@ func Build(ctx Context) string {
|
||||
weekday := time.Now().In(loc).Format("Monday")
|
||||
fmt.Fprintf(&b, "Time: %s (%s)\n", currentTime, weekday)
|
||||
fmt.Fprintf(&b, "User chat (selected): %s\n", ctx.UserPreferredChat)
|
||||
fmt.Fprintf(&b, "UUID: %s\n", ctx.UserTemporaryCode.String())
|
||||
fmt.Fprintf(&b, "Bind code: %s\n", ctx.UserBindCode.String())
|
||||
fmt.Fprintf(&b, "Contact code: %s\n", ctx.UserContactCode.String())
|
||||
|
||||
if len(ctx.Chats) > 0 {
|
||||
b.WriteString("Connected chats:\n")
|
||||
@@ -185,12 +192,10 @@ func Build(ctx Context) string {
|
||||
}
|
||||
}
|
||||
|
||||
if len(ctx.ExecutionErrors) > 0 {
|
||||
b.WriteString("\n=== EXECUTION ERRORS FROM PREVIOUS ACTIONS ===\n")
|
||||
for _, err := range ctx.ExecutionErrors {
|
||||
fmt.Fprintf(&b, " - %s\n", err.Error())
|
||||
}
|
||||
b.WriteString("\nIf errors are present, try to fix them or inform the user what went wrong.\n")
|
||||
if ctx.Error != nil {
|
||||
b.WriteString("\nExecution errors:\n")
|
||||
b.WriteString(ctx.Error.Error())
|
||||
b.WriteString("\nTry to fix the error yourself or inform the user that you can't do that.\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
|
||||
@@ -11,13 +11,15 @@ import (
|
||||
)
|
||||
|
||||
func TestBuild_Message(t *testing.T) {
|
||||
tmpCode := uuid.New()
|
||||
bindCode := uuid.New()
|
||||
contactCode := uuid.New()
|
||||
|
||||
ctx := Context{
|
||||
UserLanguage: "en",
|
||||
UserTimezone: "Europe/Moscow",
|
||||
UserPreferredChat: "telegram",
|
||||
UserTemporaryCode: tmpCode,
|
||||
UserBindCode: bindCode,
|
||||
UserContactCode: contactCode,
|
||||
Chats: []database.Chat{
|
||||
{Platform: "telegram", Identifier: "@test"},
|
||||
},
|
||||
@@ -46,7 +48,8 @@ func TestBuild_Message(t *testing.T) {
|
||||
assert.Contains(t, result, "YOU ARE JULES")
|
||||
assert.Contains(t, result, "Language: en")
|
||||
assert.Contains(t, result, "User chat (selected): telegram")
|
||||
assert.Contains(t, result, "UUID: "+tmpCode.String())
|
||||
assert.Contains(t, result, "Bind code: "+bindCode.String())
|
||||
assert.Contains(t, result, "Contact code: "+contactCode.String())
|
||||
assert.Contains(t, result, "Connected chats:")
|
||||
assert.Contains(t, result, "telegram")
|
||||
assert.Contains(t, result, "Facts:")
|
||||
@@ -65,13 +68,15 @@ func TestBuild_Message(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuild_Notification(t *testing.T) {
|
||||
tmpCode := uuid.New()
|
||||
bindCode := uuid.New()
|
||||
contactCode := uuid.New()
|
||||
|
||||
ctx := Context{
|
||||
UserLanguage: "ru",
|
||||
UserTimezone: "UTC",
|
||||
UserPreferredChat: "telegram",
|
||||
UserTemporaryCode: tmpCode,
|
||||
UserBindCode: bindCode,
|
||||
UserContactCode: contactCode,
|
||||
Chats: []database.Chat{
|
||||
{Platform: "telegram", Identifier: "@test"},
|
||||
},
|
||||
@@ -82,7 +87,8 @@ func TestBuild_Notification(t *testing.T) {
|
||||
result := Build(ctx)
|
||||
|
||||
assert.Contains(t, result, "Language: ru")
|
||||
assert.Contains(t, result, "UUID: "+tmpCode.String())
|
||||
assert.Contains(t, result, "Bind code: "+bindCode.String())
|
||||
assert.Contains(t, result, "Contact code: "+contactCode.String())
|
||||
assert.Contains(t, result, "Connected chats:")
|
||||
assert.Contains(t, result, "telegram")
|
||||
assert.NotContains(t, result, "Facts:")
|
||||
@@ -94,13 +100,15 @@ func TestBuild_Notification(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuild_EmptyData(t *testing.T) {
|
||||
tmpCode := uuid.New()
|
||||
bindCode := uuid.New()
|
||||
contactCode := uuid.New()
|
||||
|
||||
ctx := Context{
|
||||
UserLanguage: "ru",
|
||||
UserTimezone: "UTC",
|
||||
UserPreferredChat: "telegram",
|
||||
UserTemporaryCode: tmpCode,
|
||||
UserBindCode: bindCode,
|
||||
UserContactCode: contactCode,
|
||||
MessageSource: "telegram",
|
||||
MessageContent: "Привет",
|
||||
}
|
||||
@@ -108,7 +116,8 @@ func TestBuild_EmptyData(t *testing.T) {
|
||||
result := Build(ctx)
|
||||
|
||||
assert.Contains(t, result, "Language: ru")
|
||||
assert.Contains(t, result, "UUID: "+tmpCode.String())
|
||||
assert.Contains(t, result, "Bind code: "+bindCode.String())
|
||||
assert.Contains(t, result, "Contact code: "+contactCode.String())
|
||||
assert.NotContains(t, result, "Connected chats:")
|
||||
assert.NotContains(t, result, "Facts:")
|
||||
assert.NotContains(t, result, "Contacts:")
|
||||
@@ -132,23 +141,23 @@ func TestBuild_InvalidTimezone(t *testing.T) {
|
||||
assert.Contains(t, result, "Time: ")
|
||||
}
|
||||
|
||||
func TestBuild_ExecutionErrors(t *testing.T) {
|
||||
tmpCode := uuid.New()
|
||||
func TestBuild_ExecutionError(t *testing.T) {
|
||||
bindCode := uuid.New()
|
||||
contactCode := uuid.New()
|
||||
|
||||
ctx := Context{
|
||||
UserLanguage: "en",
|
||||
UserTimezone: "UTC",
|
||||
UserPreferredChat: "telegram",
|
||||
UserTemporaryCode: tmpCode,
|
||||
UserBindCode: bindCode,
|
||||
UserContactCode: contactCode,
|
||||
MessageSource: "telegram",
|
||||
MessageContent: "test",
|
||||
ExecutionErrors: []error{errors.New("user not found"), errors.New("invalid UUID")},
|
||||
Error: errors.New("user not found"),
|
||||
}
|
||||
|
||||
result := Build(ctx)
|
||||
|
||||
assert.Contains(t, result, "=== EXECUTION ERRORS FROM PREVIOUS ACTIONS ===")
|
||||
assert.Contains(t, result, "user not found")
|
||||
assert.Contains(t, result, "invalid UUID")
|
||||
assert.Contains(t, result, "If errors are present, try to fix them")
|
||||
assert.Contains(t, result, "Execution errors:\nuser not found")
|
||||
assert.Contains(t, result, "Try to fix the error yourself or inform the user")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user