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:
d1nch8g
2026-04-19 20:46:17 +03:00
parent 7e2ee63f2c
commit bfc9c1b4f5
8 changed files with 259 additions and 209 deletions
+11 -9
View File
@@ -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
View File
@@ -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
}
+37 -41
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")
+26 -17
View File
@@ -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")
}