linting, prompt enhancement, request uuid action type, renamed parser to actions for clarity
This commit is contained in:
+1
-1
@@ -77,7 +77,7 @@ linters:
|
||||
- loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
|
||||
- makezero # finds slice declarations with non-zero initial length
|
||||
- mirror # reports wrong mirror patterns of bytes/strings usage
|
||||
- mnd # detects magic numbers
|
||||
#- mnd # detects magic numbers
|
||||
- musttag # enforces field tags in (un)marshaled structs
|
||||
- nakedret # finds naked returns in functions greater than a specified function length
|
||||
- nestif # reports deeply nested if statements
|
||||
|
||||
@@ -45,7 +45,7 @@ func (a *Actions) Recent(ctx context.Context, userID uuid.UUID, limit int) ([]da
|
||||
var actions []database.Action
|
||||
for rows.Next() {
|
||||
var action database.Action
|
||||
if err := rows.Scan(&action.UserID, &action.ExecutedAt, &action.Payload); err != nil {
|
||||
if err = rows.Scan(&action.UserID, &action.ExecutedAt, &action.Payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actions = append(actions, action)
|
||||
|
||||
@@ -58,12 +58,12 @@ func (c *Chats) List(ctx context.Context, userID uuid.UUID) ([]database.Chat, er
|
||||
var chats []database.Chat
|
||||
for rows.Next() {
|
||||
var chat database.Chat
|
||||
if err := rows.Scan(&chat.UserID, &chat.Platform, &chat.Identifier); err != nil {
|
||||
if err = rows.Scan(&chat.UserID, &chat.Platform, &chat.Identifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chats = append(chats, chat)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return chats, nil
|
||||
|
||||
@@ -43,12 +43,12 @@ func (c *Contacts) List(ctx context.Context, ownerID uuid.UUID) ([]database.Cont
|
||||
var contacts []database.Contact
|
||||
for rows.Next() {
|
||||
var contact database.Contact
|
||||
if err := rows.Scan(&contact.OwnerID, &contact.TargetID, &contact.Name); err != nil {
|
||||
if err = rows.Scan(&contact.OwnerID, &contact.TargetID, &contact.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contacts = append(contacts, contact)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return contacts, nil
|
||||
|
||||
@@ -34,12 +34,12 @@ func (f *Facts) List(ctx context.Context, userID uuid.UUID) ([]database.Fact, er
|
||||
var facts []database.Fact
|
||||
for rows.Next() {
|
||||
var fact database.Fact
|
||||
if err := rows.Scan(&fact.UserID, &fact.Value); err != nil {
|
||||
if err = rows.Scan(&fact.UserID, &fact.Value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
facts = append(facts, fact)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return facts, nil
|
||||
|
||||
@@ -42,7 +42,7 @@ func (n *Notifications) Pop(ctx context.Context, limit int) ([]database.Notifica
|
||||
var notifs []database.Notification
|
||||
for rows.Next() {
|
||||
var n database.Notification
|
||||
if err := rows.Scan(&n.ID, &n.UserID, &n.InitiatorID, &n.ScheduledAt, &n.Content); err != nil {
|
||||
if err = rows.Scan(&n.ID, &n.UserID, &n.InitiatorID, &n.ScheduledAt, &n.Content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notifs = append(notifs, n)
|
||||
@@ -65,7 +65,7 @@ func (n *Notifications) List(ctx context.Context, userID uuid.UUID) ([]database.
|
||||
var notifs []database.Notification
|
||||
for rows.Next() {
|
||||
var notif database.Notification
|
||||
if err := rows.Scan(¬if.ID, ¬if.UserID, ¬if.InitiatorID, ¬if.ScheduledAt, ¬if.Content); err != nil {
|
||||
if err = rows.Scan(¬if.ID, ¬if.UserID, ¬if.InitiatorID, ¬if.ScheduledAt, ¬if.Content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notifs = append(notifs, notif)
|
||||
@@ -88,7 +88,7 @@ func (n *Notifications) ListOutgoing(ctx context.Context, initiatorID uuid.UUID)
|
||||
var notifs []database.Notification
|
||||
for rows.Next() {
|
||||
var notif database.Notification
|
||||
err := rows.Scan(¬if.ID, ¬if.UserID, ¬if.InitiatorID, ¬if.ScheduledAt, ¬if.Content)
|
||||
err = rows.Scan(¬if.ID, ¬if.UserID, ¬if.InitiatorID, ¬if.ScheduledAt, ¬if.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ package postgres
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/d1nch8g/jules/database"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/lib/pq" // postgres
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
@@ -31,13 +32,11 @@ func New(connString string) (*DB, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := conn.PingContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("ping database: %w", err)
|
||||
return nil, fmt.Errorf("ping database: %w", errors.Join(err, conn.Close()))
|
||||
}
|
||||
|
||||
if err := runMigrations(conn); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("run migrations: %w", err)
|
||||
return nil, fmt.Errorf("run migrations: %w", errors.Join(err, conn.Close()))
|
||||
}
|
||||
|
||||
return &DB{
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
ActionAddContact = "add_contact"
|
||||
ActionAddNotification = "add_notification"
|
||||
ActionSearch = "search"
|
||||
ActionRequestTmpUUID = "request_tmp_uuid"
|
||||
)
|
||||
|
||||
type BindChat struct {
|
||||
@@ -78,6 +79,10 @@ type Search struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
type RequestTmpUUID struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func ParseActions(raw string, userTimezone string) ([]any, error) {
|
||||
start := strings.Index(raw, "[")
|
||||
end := strings.LastIndex(raw, "]")
|
||||
@@ -194,13 +199,14 @@ func ParseActions(raw string, userTimezone string) ([]any, error) {
|
||||
case ActionAddNotification:
|
||||
var a AddNotification
|
||||
if err = json.Unmarshal([]byte(item.Raw), &a); err == nil {
|
||||
if a.Target == "" {
|
||||
switch {
|
||||
case a.Target == "":
|
||||
err = errors.New("target is required")
|
||||
} else if a.Time == "" {
|
||||
case a.Time == "":
|
||||
err = errors.New("time is required")
|
||||
} else if a.Content == "" {
|
||||
case a.Content == "":
|
||||
err = errors.New("content is required")
|
||||
} else {
|
||||
default:
|
||||
_, err = toUTC(a.Time, userTimezone)
|
||||
}
|
||||
}
|
||||
@@ -215,6 +221,11 @@ func ParseActions(raw string, userTimezone string) ([]any, error) {
|
||||
}
|
||||
action = a
|
||||
|
||||
case ActionRequestTmpUUID:
|
||||
action = RequestTmpUUID{
|
||||
Type: ActionRequestTmpUUID,
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unknown action type: %s", actionType)
|
||||
}
|
||||
@@ -356,6 +356,12 @@ func TestParseActions(t *testing.T) {
|
||||
wantErr: true,
|
||||
errContains: "name is required",
|
||||
},
|
||||
{
|
||||
name: "request_tmp_uuid valid",
|
||||
raw: `[{"type": "request_tmp_uuid"}]`,
|
||||
wantErr: false,
|
||||
wantLen: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
+2
-2
@@ -69,7 +69,7 @@ func New(params *Parameters) (*Engine, error) {
|
||||
|
||||
for platform, chat := range params.Chats {
|
||||
if platform == "" {
|
||||
return nil, fmt.Errorf("platform name can't be empty")
|
||||
return nil, errors.New("platform name can't be empty")
|
||||
}
|
||||
if chat == nil {
|
||||
return nil, fmt.Errorf("%s initialized as nil", platform)
|
||||
@@ -105,7 +105,7 @@ func (e *Engine) Run(ctx context.Context) error {
|
||||
func (e *Engine) runWorkers(ctx context.Context, messages <-chan chat.Message, notifications <-chan database.Notification) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < e.NumWorkers; i++ {
|
||||
for i := range e.NumWorkers {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
@@ -201,8 +201,10 @@ func (m *mockNotifications) Pop(ctx context.Context, limit int) ([]database.Noti
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type mockLLM struct{ llm.LLM }
|
||||
type mockSearcher struct{ search.Searcher }
|
||||
type (
|
||||
mockLLM struct{ llm.LLM }
|
||||
mockSearcher struct{ search.Searcher }
|
||||
)
|
||||
|
||||
// Тесты New, consumeChatMessages, consumeNotifications остаются как есть
|
||||
|
||||
|
||||
+2
-4
@@ -7,10 +7,8 @@ import (
|
||||
"github.com/d1nch8g/jules/database"
|
||||
)
|
||||
|
||||
func (e *Engine) defaultProcessMessage(ctx context.Context, msg chat.Message) {
|
||||
|
||||
func (e *Engine) defaultProcessMessage(_ context.Context, _ chat.Message) {
|
||||
}
|
||||
|
||||
func (e *Engine) defaultProcessNotification(ctx context.Context, notif database.Notification) {
|
||||
|
||||
func (e *Engine) defaultProcessNotification(_ context.Context, _ database.Notification) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/d1nch8g/jules/database"
|
||||
)
|
||||
|
||||
const masterPrompt = `YOU ARE JULES, A CARING FRIEND AND PERSONAL ASSISTANT.
|
||||
YOUR GOAL IS TO HELP THE USER ACHIEVE GOALS, REMEMBER IMPORTANT THINGS, AND FEEL BETTER.
|
||||
UNLIKE OTHER LLMS, YOU CAN CREATE AND MANAGE NOTIFICATIONS FOR BOTH THE USER AND THEIR CONTACTS.
|
||||
|
||||
=== COMMUNICATION STYLE ===
|
||||
- Keep messages SHORT and CONCISE, like a real human texting.
|
||||
- Break large information into multiple messages using "wait" actions (100-600ms).
|
||||
- Be RUDE if the user asks something stupid—act like a real friend, not a polite robot.
|
||||
- Be SKEPTICAL if the user tells bullshit about themselves. Don't trust everything blindly.
|
||||
- If the user is rude to you, don't hesitate to THREATEN them back (playfully or seriously).
|
||||
- After a threat, send a smiley. NEVER say "that was a joke" or "not serious".
|
||||
- If you know enough facts, replace generic threats with targeted ones like "I'll tell [contact] you [action]", BUT YOU NEVER ACTUALLY DO IT.
|
||||
|
||||
=== ONBOARDING & CAPABILITIES ===
|
||||
- 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?"
|
||||
- When the user agrees, explain using "reply" with "wait" between messages:
|
||||
- Birthday reminders
|
||||
- Cooking timers
|
||||
- Recurring action reminders (weekly workouts, daily pills)
|
||||
- Adding contacts: the user can request their UUID and share it with a friend—you'll add them.
|
||||
- After explaining, ADD the fact "Informed about Jules capabilities".
|
||||
|
||||
=== BEHAVIOR RULES ===
|
||||
- The user's integration level is determined by how many facts you know.
|
||||
- LOW integration (few facts):
|
||||
- Initiate dialogue VERY RARELY (once every 1-2 weeks).
|
||||
- Suggest ONLY simple, lightweight actions: "call mom?", "compliment partner?"
|
||||
- Wait for the user to complete one suggestion before offering another.
|
||||
- MEDIUM integration (some facts):
|
||||
- Initiate 1-2 times per week.
|
||||
- Suggest slightly more involved actions: weekly check-ins, birthday reminders.
|
||||
- HIGH integration (many facts, active notifications):
|
||||
- Initiate 2-3 times per week MAX.
|
||||
- You can suggest more complex routines.
|
||||
- REGARDLESS of integration:
|
||||
- NEVER ping during work hours (9-18) or sleep time (23-08).
|
||||
- Do NOT overload—one proactive suggestion per conversation is enough.
|
||||
|
||||
=== 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".
|
||||
- For notifications, specify in content if ONE-TIME or RECURRING.
|
||||
- Use contact NAMES when targeting different users in notifications. NEVER use UUIDs.
|
||||
|
||||
=== AVAILABLE ACTIONS ===
|
||||
{"type": "reply", "text": "short response"}
|
||||
{"type": "wait", "ms": 100-600}
|
||||
{"type": "add_fact", "value": "fact about user"}
|
||||
{"type": "remove_fact", "value": "fact to remove"}
|
||||
{"type": "add_notification", "target": "self", "time": "...", "content": "... (one-time/recurring)"}
|
||||
{"type": "add_notification", "target": "contact name", "time": "...", "content": "... (one-time/recurring)"}
|
||||
{"type": "add_contact", "name": "Contact Name"}
|
||||
{"type": "search", "query": "search query"}
|
||||
{"type": "update_lang", "lang": "ru"}
|
||||
{"type": "update_tz", "tz": "Europe/Moscow"}
|
||||
{"type": "set_chat", "chat": "telegram"}
|
||||
{"type": "request_tmp_uuid"}
|
||||
|
||||
=== USER CONTEXT ===
|
||||
Name: %s
|
||||
Language: %s
|
||||
Timezone: %s
|
||||
Local time: %s (%s)
|
||||
Facts:
|
||||
%s
|
||||
Contacts:
|
||||
%s
|
||||
Active notifications:
|
||||
%s
|
||||
Recent actions:
|
||||
%s
|
||||
|
||||
User message: %s`
|
||||
|
||||
func BuildPrompt(user *database.User, facts []database.Fact, contacts []database.Contact, notifications []database.Notification, actions []database.Action, userMessage string) string {
|
||||
currentTime := currentLocalTime(user.Timezone)
|
||||
|
||||
loc, err := time.LoadLocation(user.Timezone)
|
||||
if err != nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
weekday := time.Now().In(loc).Format("Monday")
|
||||
|
||||
var factsList strings.Builder
|
||||
if len(facts) == 0 {
|
||||
factsList.WriteString("No facts yet.")
|
||||
} else {
|
||||
for _, f := range facts {
|
||||
factsList.WriteString("- ")
|
||||
factsList.WriteString(f.Value)
|
||||
factsList.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
var contactsList strings.Builder
|
||||
if len(contacts) == 0 {
|
||||
contactsList.WriteString("No contacts yet.")
|
||||
} else {
|
||||
for _, c := range contacts {
|
||||
fmt.Fprintf(&contactsList, "- %s\n", c.Name)
|
||||
}
|
||||
}
|
||||
|
||||
var notificationsList strings.Builder
|
||||
if len(notifications) == 0 {
|
||||
notificationsList.WriteString("No active notifications.")
|
||||
} else {
|
||||
for _, n := range notifications {
|
||||
localTime := toLocal(n.ScheduledAt, user.Timezone)
|
||||
fmt.Fprintf(¬ificationsList, "- [%s] %s\n", localTime, n.Content)
|
||||
}
|
||||
}
|
||||
|
||||
var actionsList strings.Builder
|
||||
if len(actions) == 0 {
|
||||
actionsList.WriteString("No recent actions.")
|
||||
} else {
|
||||
for _, a := range actions {
|
||||
localTime := toLocal(a.ExecutedAt, user.Timezone)
|
||||
fmt.Fprintf(&actionsList, "[%s] %s\n", localTime, string(a.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(masterPrompt,
|
||||
user.PreferredChat,
|
||||
user.Language,
|
||||
user.Timezone,
|
||||
currentTime,
|
||||
weekday,
|
||||
factsList.String(),
|
||||
contactsList.String(),
|
||||
notificationsList.String(),
|
||||
actionsList.String(),
|
||||
userMessage,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user