linting, prompt enhancement, request uuid action type, renamed parser to actions for clarity

This commit is contained in:
d1nch8g
2026-04-19 01:20:52 +03:00
parent 9b17ab716d
commit bf84a4b902
13 changed files with 192 additions and 28 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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(&notif.ID, &notif.UserID, &notif.InitiatorID, &notif.ScheduledAt, &notif.Content); err != nil {
if err = rows.Scan(&notif.ID, &notif.UserID, &notif.InitiatorID, &notif.ScheduledAt, &notif.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(&notif.ID, &notif.UserID, &notif.InitiatorID, &notif.ScheduledAt, &notif.Content)
err = rows.Scan(&notif.ID, &notif.UserID, &notif.InitiatorID, &notif.ScheduledAt, &notif.Content)
if err != nil {
return nil, err
}
+4 -5
View File
@@ -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{
+15 -4
View File
@@ -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
View File
@@ -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()
+4 -2
View File
@@ -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
View File
@@ -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) {
}
+148
View File
@@ -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(&notificationsList, "- [%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,
)
}