Class: Ask::CodingHarness::Store

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/coding_harness/store.rb

Overview

Persistence for conversations, backed by ask-state-providers (SQLite).

A conversation is a plain Hash with string keys:

id, title, directory, archived, messages, created_at, updated_at

Messages are { "role" => "user"|"assistant", "content" => String, "created_at" => String } hashes. The store is the single owner of conversation records; the server and runner only read/write through it.

Constant Summary collapse

CONVERSATIONS_KEY =
"__conversations__"
WORKSPACES_KEY =
"__workspaces__"
MAX_CONVERSATIONS =
500
MAX_WORKSPACES =
100

Instance Method Summary collapse

Constructor Details

#initialize(db_path:) ⇒ Store

Returns a new instance of Store.

Parameters:

  • db_path (String)

    path to the SQLite database file



23
24
25
26
27
# File 'lib/ask/coding_harness/store.rb', line 23

def initialize(db_path:)
  @db_path = db_path
  dir = File.dirname(db_path)
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
end

Instance Method Details

#build(directory: nil, title: "New conversation", agent: nil) ⇒ Object

A fresh conversation record. Not persisted until #save.

Parameters:

  • agent (String, nil) (defaults to: nil)

    declarative agent name (ask-agent convention) or nil for the default agent



32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/ask/coding_harness/store.rb', line 32

def build(directory: nil, title: "New conversation", agent: nil)
  now = Time.now.iso8601
  {
    "id" => SecureRandom.uuid,
    "title" => title,
    "directory" => directory,
    "agent" => agent,
    "archived" => false,
    "messages" => [],
    "created_at" => now,
    "updated_at" => now
  }
end

#closeObject



110
111
112
113
# File 'lib/ask/coding_harness/store.rb', line 110

def close
  @db&.close
  @db = nil
end

#delete(id) ⇒ Object



64
65
66
67
# File 'lib/ask/coding_harness/store.rb', line 64

def delete(id)
  db.delete("conv:#{id}")
  db.list_remove(CONVERSATIONS_KEY, id)
end

#list(archived: false) ⇒ Object

Conversation summaries (no messages), sorted by most recent first.



70
71
72
73
74
75
76
77
78
79
# File 'lib/ask/coding_harness/store.rb', line 70

def list(archived: false)
  ids = db.list_range(CONVERSATIONS_KEY, 0, -1)
  summaries = ids.filter_map do |id|
    data = db.get("conv:#{id}")
    next unless data
    next if data["archived"] == true && !archived
    summary(data)
  end
  summaries.sort_by { |c| c["updated_at"].to_s }.reverse
end

#list_for_directory(directory, archived: false) ⇒ Object

Summaries for one workspace directory, most recent first.



82
83
84
# File 'lib/ask/coding_harness/store.rb', line 82

def list_for_directory(directory, archived: false)
  list(archived: archived).select { |c| c["directory"] == directory }
end

#load(id) ⇒ Object

Load a conversation by id, or nil when missing.



59
60
61
62
# File 'lib/ask/coding_harness/store.rb', line 59

def load(id)
  data = db.get("conv:#{id}")
  data&.transform_keys(&:to_s)
end

#projects(archived: false) ⇒ Object

Workspace directories that have conversations, with counts.



87
88
89
90
91
92
# File 'lib/ask/coding_harness/store.rb', line 87

def projects(archived: false)
  list(archived: archived).each_with_object(Hash.new(0)) do |c, counts|
    dir = c["directory"]
    counts[dir] += 1 if dir
  end.map { |dir, count| { "directory" => dir, "name" => File.basename(dir), "conversation_count" => count } }
end

#register_workspace(path) ⇒ Object

Register a workspace path (idempotent). Workspaces the server has explicitly opened, even before any conversation exists.



98
99
100
101
102
103
# File 'lib/ask/coding_harness/store.rb', line 98

def register_workspace(path)
  dir = File.expand_path(path)
  existing = db.list_range(WORKSPACES_KEY, 0, -1)
  db.list_append(WORKSPACES_KEY, dir, max_length: MAX_WORKSPACES) unless existing.include?(dir)
  dir
end

#save(conv) ⇒ Object

Persist a conversation, bumping updated_at and guessing a title when the conversation is still untitled and has messages.



48
49
50
51
52
53
54
55
56
# File 'lib/ask/coding_harness/store.rb', line 48

def save(conv)
  conv["updated_at"] = Time.now.iso8601
  if conv["title"] == "New conversation" && conv["messages"].length >= 1
    conv["title"] = guess_title(conv["messages"])
  end
  db.set("conv:#{conv["id"]}", conv)
  index_conversation(conv["id"])
  conv
end

#workspacesObject

Explicitly registered workspace paths.



106
107
108
# File 'lib/ask/coding_harness/store.rb', line 106

def workspaces
  db.list_range(WORKSPACES_KEY, 0, -1)
end