elixir: define model structs with YAML serialization
Swadhin Biswas committed 7 months ago
096b0b0
1 parent 778bec3
commit 096b0b0
1 file changed
+344 -0
Showing 1 changed file+344-0
▼ELIXIR/lib/contexa/models.ex
+344-0
| @@ -0,0 +1,344 @@ | ||
1 | +defmodule Contexa.Models do | |
2 | + @moduledoc """ | |
3 | + Data structures for the contexa GCC workspace. | |
4 | + | |
5 | + contexa — Git-inspired context management for LLM agents. | |
6 | + Based on arXiv:2508.00031. | |
7 | + """ | |
8 | + | |
9 | + # ── OTARecord ───────────────────────────────────────────────────── | |
10 | + | |
11 | + defmodule OTARecord do | |
12 | + @moduledoc "An Observation-Thought-Action cycle record." | |
13 | + defstruct [:step, :timestamp, :observation, :thought, :action] | |
14 | + | |
15 | + @type t :: %__MODULE__{ | |
16 | + step: non_neg_integer(), | |
17 | + timestamp: String.t(), | |
18 | + observation: String.t(), | |
19 | + thought: String.t(), | |
20 | + action: String.t() | |
21 | + } | |
22 | + end | |
23 | + | |
24 | + defmodule CommitRecord do | |
25 | + @moduledoc "A milestone checkpoint record." | |
26 | + defstruct [ | |
27 | + :commit_id, | |
28 | + :branch_name, | |
29 | + :branch_purpose, | |
30 | + :previous_progress_summary, | |
31 | + :this_commit_contribution, | |
32 | + :timestamp | |
33 | + ] | |
34 | + | |
35 | + @type t :: %__MODULE__{ | |
36 | + commit_id: String.t(), | |
37 | + branch_name: String.t(), | |
38 | + branch_purpose: String.t(), | |
39 | + previous_progress_summary: String.t(), | |
40 | + this_commit_contribution: String.t(), | |
41 | + timestamp: String.t() | |
42 | + } | |
43 | + end | |
44 | + | |
45 | + defmodule BranchMetadata do | |
46 | + @moduledoc "Branch metadata stored in metadata.yaml." | |
47 | + defstruct [ | |
48 | + :name, | |
49 | + :purpose, | |
50 | + :created_from, | |
51 | + :created_at, | |
52 | + status: "active", | |
53 | + merged_into: nil, | |
54 | + merged_at: nil | |
55 | + ] | |
56 | + | |
57 | + @type t :: %__MODULE__{ | |
58 | + name: String.t(), | |
59 | + purpose: String.t(), | |
60 | + created_from: String.t(), | |
61 | + created_at: String.t(), | |
62 | + status: String.t(), | |
63 | + merged_into: String.t() | nil, | |
64 | + merged_at: String.t() | nil | |
65 | + } | |
66 | + end | |
67 | + | |
68 | + defmodule ContextResult do | |
69 | + @moduledoc "Result of a CONTEXT query." | |
70 | + defstruct [ | |
71 | + :branch_name, | |
72 | + :k, | |
73 | + commits: [], | |
74 | + ota_records: [], | |
75 | + main_roadmap: "", | |
76 | + metadata: nil | |
77 | + ] | |
78 | + | |
79 | + @type t :: %__MODULE__{ | |
80 | + branch_name: String.t(), | |
81 | + k: non_neg_integer(), | |
82 | + commits: [CommitRecord.t()], | |
83 | + ota_records: [OTARecord.t()], | |
84 | + main_roadmap: String.t(), | |
85 | + metadata: BranchMetadata.t() | nil | |
86 | + } | |
87 | + end | |
88 | + | |
89 | + # ── Constructors ────────────────────────────────────────────────── | |
90 | + | |
91 | + @doc "Generate an 8-character hex ID." | |
92 | + @spec generate_id() :: String.t() | |
93 | + def generate_id do | |
94 | + :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower) | |
95 | + end | |
96 | + | |
97 | + @doc "Get current UTC timestamp in ISO 8601 format." | |
98 | + @spec timestamp() :: String.t() | |
99 | + def timestamp do | |
100 | + DateTime.utc_now() |> DateTime.to_iso8601() | |
101 | + end | |
102 | + | |
103 | + @doc "Create a new OTARecord." | |
104 | + @spec new_ota(non_neg_integer(), String.t(), String.t(), String.t()) :: OTARecord.t() | |
105 | + def new_ota(step, observation, thought, action) do | |
106 | + %OTARecord{ | |
107 | + step: step, | |
108 | + timestamp: timestamp(), | |
109 | + observation: observation || "", | |
110 | + thought: thought || "", | |
111 | + action: action || "" | |
112 | + } | |
113 | + end | |
114 | + | |
115 | + @doc "Create a new CommitRecord." | |
116 | + @spec new_commit(String.t(), String.t(), String.t(), String.t()) :: CommitRecord.t() | |
117 | + def new_commit(branch_name, branch_purpose, previous_summary, contribution) do | |
118 | + %CommitRecord{ | |
119 | + commit_id: generate_id(), | |
120 | + branch_name: branch_name, | |
121 | + branch_purpose: branch_purpose || "", | |
122 | + previous_progress_summary: previous_summary || "", | |
123 | + this_commit_contribution: contribution || "", | |
124 | + timestamp: timestamp() | |
125 | + } | |
126 | + end | |
127 | + | |
128 | + @doc "Create a new BranchMetadata." | |
129 | + @spec new_branch_metadata(String.t(), String.t(), String.t()) :: BranchMetadata.t() | |
130 | + def new_branch_metadata(name, purpose, created_from \\ "") do | |
131 | + %BranchMetadata{ | |
132 | + name: name, | |
133 | + purpose: purpose, | |
134 | + created_from: created_from, | |
135 | + created_at: timestamp(), | |
136 | + status: "active", | |
137 | + merged_into: nil, | |
138 | + merged_at: nil | |
139 | + } | |
140 | + end | |
141 | + | |
142 | + # ── Markdown serialization ──────────────────────────────────────── | |
143 | + | |
144 | + @doc "Render an OTARecord as markdown." | |
145 | + @spec ota_to_markdown(OTARecord.t()) :: String.t() | |
146 | + def ota_to_markdown(%OTARecord{} = rec) do | |
147 | + """ | |
148 | + ### Step #{rec.step} — #{rec.timestamp} | |
149 | + **Observation:** #{rec.observation} | |
150 | + | |
151 | + **Thought:** #{rec.thought} | |
152 | + | |
153 | + **Action:** #{rec.action} | |
154 | + | |
155 | + --- | |
156 | + """ | |
157 | + end | |
158 | + | |
159 | + @doc "Render a CommitRecord as markdown." | |
160 | + @spec commit_to_markdown(CommitRecord.t()) :: String.t() | |
161 | + def commit_to_markdown(%CommitRecord{} = rec) do | |
162 | + """ | |
163 | + ## Commit `#{rec.commit_id}` | |
164 | + **Timestamp:** #{rec.timestamp} | |
165 | + | |
166 | + **Branch Purpose:** #{rec.branch_purpose} | |
167 | + | |
168 | + **Previous Progress Summary:** #{rec.previous_progress_summary} | |
169 | + | |
170 | + **This Commit's Contribution:** #{rec.this_commit_contribution} | |
171 | + | |
172 | + --- | |
173 | + """ | |
174 | + end | |
175 | + | |
176 | + # ── YAML serialization ─────────────────────────────────────────── | |
177 | + | |
178 | + @doc "Serialize BranchMetadata to YAML string." | |
179 | + @spec metadata_to_yaml(BranchMetadata.t()) :: String.t() | |
180 | + def metadata_to_yaml(%BranchMetadata{} = meta) do | |
181 | + yaml_val = fn | |
182 | + nil -> "null" | |
183 | + "" -> "''" | |
184 | + v when is_binary(v) -> | |
185 | + if String.contains?(v, ":") or String.contains?(v, "#") do | |
186 | + "'#{String.replace(v, "'", "''")}'" | |
187 | + else | |
188 | + v | |
189 | + end | |
190 | + end | |
191 | + | |
192 | + # Keys in sorted order for cross-language compatibility | |
193 | + [ | |
194 | + "created_at: #{yaml_val.(meta.created_at)}", | |
195 | + "created_from: #{yaml_val.(meta.created_from)}", | |
196 | + "merged_at: #{yaml_val.(meta.merged_at)}", | |
197 | + "merged_into: #{yaml_val.(meta.merged_into)}", | |
198 | + "name: #{yaml_val.(meta.name)}", | |
199 | + "purpose: #{yaml_val.(meta.purpose)}", | |
200 | + "status: #{yaml_val.(meta.status)}" | |
201 | + ] | |
202 | + |> Enum.join("\n") | |
203 | + |> Kernel.<>("\n") | |
204 | + end | |
205 | + | |
206 | + @doc "Parse BranchMetadata from YAML string." | |
207 | + @spec metadata_from_yaml(String.t()) :: BranchMetadata.t() | |
208 | + def metadata_from_yaml(text) do | |
209 | + pairs = | |
210 | + text | |
211 | + |> String.split("\n", trim: true) | |
212 | + |> Enum.reduce(%{}, fn line, acc -> | |
213 | + case Regex.run(~r/^(\S+):\s*(.*)$/, line) do | |
214 | + [_, key, val] -> | |
215 | + val = val |> String.trim() | |
216 | + val = cond do | |
217 | + val == "null" -> nil | |
218 | + val == "''" -> "" | |
219 | + String.starts_with?(val, "'") and String.ends_with?(val, "'") -> | |
220 | + val |> String.slice(1..-2//1) |> String.replace("''", "'") | |
221 | + true -> val | |
222 | + end | |
223 | + Map.put(acc, key, val) | |
224 | + _ -> acc | |
225 | + end | |
226 | + end) | |
227 | + | |
228 | + %BranchMetadata{ | |
229 | + name: Map.get(pairs, "name", ""), | |
230 | + purpose: Map.get(pairs, "purpose", ""), | |
231 | + created_from: Map.get(pairs, "created_from", ""), | |
232 | + created_at: Map.get(pairs, "created_at", ""), | |
233 | + status: Map.get(pairs, "status", "active"), | |
234 | + merged_into: Map.get(pairs, "merged_into"), | |
235 | + merged_at: Map.get(pairs, "merged_at") | |
236 | + } | |
237 | + end | |
238 | + | |
239 | + # ── Markdown parsing ────────────────────────────────────────────── | |
240 | + | |
241 | + @doc "Parse commit records from commit.md content." | |
242 | + @spec parse_commits(String.t()) :: [CommitRecord.t()] | |
243 | + def parse_commits(text) do | |
244 | + text | |
245 | + |> String.split("\n---\n") | |
246 | + |> Enum.map(&String.trim/1) | |
247 | + |> Enum.filter(&(&1 != "")) | |
248 | + |> Enum.reduce([], fn block, acc -> | |
249 | + commit_id = case Regex.run(~r/## Commit `([^`]+)`/, block) do | |
250 | + [_, id] -> id | |
251 | + _ -> "" | |
252 | + end | |
253 | + | |
254 | + if commit_id != "" do | |
255 | + extract = fn pattern -> | |
256 | + case Regex.run(pattern, block) do | |
257 | + [_, val] -> String.trim(val) | |
258 | + _ -> "" | |
259 | + end | |
260 | + end | |
261 | + | |
262 | + rec = %CommitRecord{ | |
263 | + commit_id: commit_id, | |
264 | + branch_name: "", | |
265 | + timestamp: extract.(~r/\*\*Timestamp:\*\*\s*(.+)/), | |
266 | + branch_purpose: extract.(~r/\*\*Branch Purpose:\*\*\s*(.+)/), | |
267 | + previous_progress_summary: extract.(~r/\*\*Previous Progress Summary:\*\*\s*(.+)/), | |
268 | + this_commit_contribution: extract.(~r/\*\*This Commit's Contribution:\*\*\s*(.+)/) | |
269 | + } | |
270 | + acc ++ [rec] | |
271 | + else | |
272 | + acc | |
273 | + end | |
274 | + end) | |
275 | + end | |
276 | + | |
277 | + @doc "Parse OTA records from log.md content." | |
278 | + @spec parse_ota(String.t()) :: [OTARecord.t()] | |
279 | + def parse_ota(text) do | |
280 | + text | |
281 | + |> String.split("\n---\n") | |
282 | + |> Enum.map(&String.trim/1) | |
283 | + |> Enum.filter(&(&1 != "")) | |
284 | + |> Enum.reduce([], fn block, acc -> | |
285 | + {step, ts} = case Regex.run(~r/### Step (\d+)\s*[—-]\s*(.+)/, block) do | |
286 | + [_, s, t] -> {String.to_integer(s), String.trim(t)} | |
287 | + _ -> {0, ""} | |
288 | + end | |
289 | + | |
290 | + extract = fn pattern -> | |
291 | + case Regex.run(pattern, block) do | |
292 | + [_, val] -> String.trim(val) | |
293 | + _ -> "" | |
294 | + end | |
295 | + end | |
296 | + | |
297 | + obs = extract.(~r/\*\*Observation:\*\*\s*(.+)/) | |
298 | + thought = extract.(~r/\*\*Thought:\*\*\s*(.+)/) | |
299 | + action = extract.(~r/\*\*Action:\*\*\s*(.+)/) | |
300 | + | |
301 | + if obs != "" or thought != "" or action != "" do | |
302 | + rec = %OTARecord{ | |
303 | + step: step, | |
304 | + timestamp: ts, | |
305 | + observation: obs, | |
306 | + thought: thought, | |
307 | + action: action | |
308 | + } | |
309 | + acc ++ [rec] | |
310 | + else | |
311 | + acc | |
312 | + end | |
313 | + end) | |
314 | + end | |
315 | + | |
316 | + # ── Context summary ────────────────────────────────────────────── | |
317 | + | |
318 | + @doc "Render a ContextResult as a markdown summary." | |
319 | + @spec context_summary(ContextResult.t()) :: String.t() | |
320 | + def context_summary(%ContextResult{} = ctx) do | |
321 | + commit_text = ctx.commits | |
322 | + |> Enum.map(&commit_to_markdown/1) | |
323 | + |> Enum.join("\n") | |
324 | + | |
325 | + total = length(ctx.ota_records) | |
326 | + recent_ota = ctx.ota_records |> Enum.take(-5) | |
327 | + ota_text = recent_ota | |
328 | + |> Enum.map(&ota_to_markdown/1) | |
329 | + |> Enum.join("\n") | |
330 | + | |
331 | + """ | |
332 | + # CONTEXT — branch `#{ctx.branch_name}` (K=#{ctx.k}) | |
333 | + | |
334 | + ## Global Roadmap | |
335 | + #{ctx.main_roadmap} | |
336 | + | |
337 | + ## Last #{ctx.k} Commit(s) | |
338 | + #{commit_text} | |
339 | + | |
340 | + ## Recent OTA steps (#{total} total) | |
341 | + #{ota_text} | |
342 | + """ | |
343 | + end | |
344 | +end | |
0 | 345 | |