jac-coder 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. jac_coder/__init__.jac +0 -0
  2. jac_coder/api.jac +82 -0
  3. jac_coder/cli_entry.py +25 -0
  4. jac_coder/config.jac +36 -0
  5. jac_coder/context.jac +17 -0
  6. jac_coder/data/examples/ai_agent.md +90 -0
  7. jac_coder/data/examples/blog_app.md +386 -0
  8. jac_coder/data/examples/core_patterns.md +321 -0
  9. jac_coder/data/examples/todo_app.md +321 -0
  10. jac_coder/data/reference/ai.md +131 -0
  11. jac_coder/data/reference/backend.md +215 -0
  12. jac_coder/data/reference/frontend.md +271 -0
  13. jac_coder/data/reference/osp.md +229 -0
  14. jac_coder/data/reference/pitfalls.md +141 -0
  15. jac_coder/data/reference/syntax.md +159 -0
  16. jac_coder/data/rules/core_jac.md +559 -0
  17. jac_coder/data/rules/fullstack.md +362 -0
  18. jac_coder/data/rules/workflow.md +88 -0
  19. jac_coder/events.jac +110 -0
  20. jac_coder/impl/api.impl.jac +399 -0
  21. jac_coder/impl/config.impl.jac +163 -0
  22. jac_coder/impl/context.impl.jac +117 -0
  23. jac_coder/impl/mcp_manager.impl.jac +380 -0
  24. jac_coder/impl/memory.impl.jac +247 -0
  25. jac_coder/impl/nodes.impl.jac +259 -0
  26. jac_coder/impl/permission.impl.jac +62 -0
  27. jac_coder/impl/walkers.impl.jac +298 -0
  28. jac_coder/mcp_manager.jac +35 -0
  29. jac_coder/memory.jac +15 -0
  30. jac_coder/nodes.jac +306 -0
  31. jac_coder/permission.jac +19 -0
  32. jac_coder/serve_entry.jac +30 -0
  33. jac_coder/server.jac +324 -0
  34. jac_coder/tool/__init__.jac +17 -0
  35. jac_coder/tool/checked.jac +10 -0
  36. jac_coder/tool/delegation.jac +23 -0
  37. jac_coder/tool/filesystem.jac +25 -0
  38. jac_coder/tool/git.jac +18 -0
  39. jac_coder/tool/guarded.jac +23 -0
  40. jac_coder/tool/impl/checked.impl.jac +38 -0
  41. jac_coder/tool/impl/delegation.impl.jac +157 -0
  42. jac_coder/tool/impl/filesystem.impl.jac +781 -0
  43. jac_coder/tool/impl/git.impl.jac +115 -0
  44. jac_coder/tool/impl/guarded.impl.jac +72 -0
  45. jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
  46. jac_coder/tool/impl/jac_docs.impl.jac +136 -0
  47. jac_coder/tool/impl/jac_tools.impl.jac +79 -0
  48. jac_coder/tool/impl/mcp.impl.jac +32 -0
  49. jac_coder/tool/impl/preview.impl.jac +233 -0
  50. jac_coder/tool/impl/question.impl.jac +29 -0
  51. jac_coder/tool/impl/scaffold.impl.jac +231 -0
  52. jac_coder/tool/impl/search.impl.jac +85 -0
  53. jac_coder/tool/impl/shell.impl.jac +89 -0
  54. jac_coder/tool/impl/task.impl.jac +12 -0
  55. jac_coder/tool/impl/think.impl.jac +4 -0
  56. jac_coder/tool/impl/todo.impl.jac +58 -0
  57. jac_coder/tool/impl/validate.impl.jac +236 -0
  58. jac_coder/tool/impl/web.impl.jac +91 -0
  59. jac_coder/tool/jac_analyzer.jac +21 -0
  60. jac_coder/tool/jac_docs.jac +9 -0
  61. jac_coder/tool/jac_tools.jac +11 -0
  62. jac_coder/tool/mcp.jac +17 -0
  63. jac_coder/tool/preview.jac +31 -0
  64. jac_coder/tool/question.jac +7 -0
  65. jac_coder/tool/scaffold.jac +10 -0
  66. jac_coder/tool/search.jac +14 -0
  67. jac_coder/tool/shell.jac +12 -0
  68. jac_coder/tool/task.jac +9 -0
  69. jac_coder/tool/think.jac +5 -0
  70. jac_coder/tool/todo.jac +12 -0
  71. jac_coder/tool/validate.jac +11 -0
  72. jac_coder/tool/vision.jac +17 -0
  73. jac_coder/tool/web.jac +10 -0
  74. jac_coder/util/__init__.jac +18 -0
  75. jac_coder/util/colors.jac +20 -0
  76. jac_coder/util/impl/sandbox.impl.jac +38 -0
  77. jac_coder/util/impl/tool_output.impl.jac +208 -0
  78. jac_coder/util/sandbox.jac +8 -0
  79. jac_coder/util/tool_output.jac +29 -0
  80. jac_coder/walkers.jac +67 -0
  81. jac_coder-0.1.0.dist-info/METADATA +9 -0
  82. jac_coder-0.1.0.dist-info/RECORD +85 -0
  83. jac_coder-0.1.0.dist-info/WHEEL +5 -0
  84. jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
  85. jac_coder-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,136 @@
1
+ glob _docs_index: list = [];
2
+
3
+
4
+ def _build_docs_index() -> list {
5
+ index: list = [];
6
+ data_dir = get_data_dir();
7
+
8
+ sources: list = [];
9
+ # Reference docs (split by topic)
10
+ ref_dir = os.path.join(data_dir, "reference");
11
+ if os.path.isdir(ref_dir) {
12
+ for fname in sorted(os.listdir(ref_dir)) {
13
+ if fname.endswith(".md") {
14
+ label = fname.replace(".md", "");
15
+ sources.append((label, os.path.join(ref_dir, fname)));
16
+ }
17
+ }
18
+ }
19
+ # Example docs (split by project)
20
+ ex_dir = os.path.join(data_dir, "examples");
21
+ if os.path.isdir(ex_dir) {
22
+ for fname in sorted(os.listdir(ex_dir)) {
23
+ if fname.endswith(".md") {
24
+ label = "example:" + fname.replace(".md", "");
25
+ sources.append((label, os.path.join(ex_dir, fname)));
26
+ }
27
+ }
28
+ }
29
+ if not sources {
30
+ import sys;
31
+ sys.stderr.write(f"[jac_docs] WARNING: no docs found in {data_dir}\n");
32
+ }
33
+ for (source_name, filepath) in sources {
34
+ try {
35
+ with open(filepath, "r") as f {
36
+ content = f.read();
37
+ }
38
+ } except (FileNotFoundError, OSError) {
39
+ continue;
40
+ }
41
+ sections = re.split(r'\n(?=## )', content);
42
+ section_num = 0;
43
+ for section in sections {
44
+ section = section.strip();
45
+ if not section.startswith("## ") {
46
+ continue;
47
+ }
48
+ section_num += 1;
49
+ lines = section.split("\n", 1);
50
+ title = lines[0].replace("## ", "", 1).strip();
51
+ body = lines[1].strip() if len(lines) > 1 else "";
52
+ raw_text = (title + " " + body).lower();
53
+ keyword_set = set(re.findall(r'[a-z0-9_]+', raw_text));
54
+ index.append(
55
+ {
56
+ "title": title,
57
+ "source": source_name,
58
+ "section_num": section_num,
59
+ "body": body,
60
+ "keywords": keyword_set
61
+ }
62
+ );
63
+ }
64
+ }
65
+ return index;
66
+ }
67
+
68
+
69
+ def _ensure_index() -> list {
70
+ if not _docs_index {
71
+ idx = _build_docs_index();
72
+ # Mutate in place since glob reassignment may not work in impl
73
+ _docs_index.clear();
74
+ _docs_index.extend(idx);
75
+ }
76
+ return _docs_index;
77
+ }
78
+
79
+
80
+ impl jac_docs(query: str, max_results: int = 3) -> str {
81
+ tool_status("jac_docs", query);
82
+ if not query or not query.strip() {
83
+ return "Error: query is required. Example: jac_docs('imports') or jac_docs('frontend effects lifecycle').";
84
+ }
85
+
86
+ index = _ensure_index();
87
+ if not index {
88
+ return "Error: Jac documentation index is empty — data files not found.";
89
+ }
90
+
91
+ query_terms = set(re.findall(r'[a-z0-9_]+', query.lower()));
92
+ if not query_terms {
93
+ return "Error: no searchable terms in query.";
94
+ }
95
+
96
+ scored: list = [];
97
+ for section in index {
98
+ title_lower = section["title"].lower();
99
+ title_hits = 0;
100
+ body_hits = 0;
101
+ for term in query_terms {
102
+ if term in title_lower {
103
+ title_hits += 1;
104
+ }
105
+ if term in section["keywords"] {
106
+ body_hits += 1;
107
+ }
108
+ }
109
+ score = (title_hits * 3) + body_hits;
110
+ if score > 0 {
111
+ scored.append({"section": section, "score": score});
112
+ }
113
+ }
114
+
115
+ if not scored {
116
+ return f"No matching sections found for: '{query}'. Try broader terms like 'syntax', 'walker', 'frontend', 'imports', 'llm', 'api'.";
117
+ }
118
+
119
+ scored.sort(key=lambda x : x["score"], reverse=True);
120
+ top = scored[:max_results];
121
+
122
+ parts: list = [];
123
+ matched_titles: list = [];
124
+ for entry in top {
125
+ s = entry["section"];
126
+ parts.append(f"## {s['title']} (from {s['source']})\n\n{s['body']}");
127
+ matched_titles.append(s["title"]);
128
+ }
129
+
130
+ titles_str = ", ".join(matched_titles);
131
+ tool_end(f"-> {titles_str}");
132
+
133
+ result = "\n\n---\n\n".join(parts);
134
+ (result, _) = truncate_output(result);
135
+ return result;
136
+ }
@@ -0,0 +1,79 @@
1
+ impl jac_check(file: str) -> str {
2
+ (file, err) = sandbox_path(file);
3
+ if err {
4
+ return err;
5
+ }
6
+ tool_status("jac_check", file);
7
+ if not file {
8
+ return "Error: file path is required";
9
+ }
10
+
11
+ try {
12
+ result = subprocess.run(
13
+ ["jac", "check", file], capture_output=True, text=True, timeout=30
14
+ );
15
+ output = ((result.stdout or "") + (result.stderr or "")).strip();
16
+ if not output {
17
+ tool_end("-> clean");
18
+ return f"No errors found in {file}";
19
+ }
20
+ err_count = len(
21
+ [
22
+ l
23
+ for l in output.splitlines()
24
+ if "error" in l.lower()
25
+ ]
26
+ );
27
+ tool_end(
28
+ f"-> {err_count} errors" if err_count > 0 else "-> clean",
29
+ error=err_count > 0
30
+ );
31
+ (output, _) = truncate_output(output);
32
+ return output;
33
+ } except subprocess.TimeoutExpired {
34
+ tool_end("-> timeout", error=True);
35
+ return "Error: jac check timed out after 30 seconds";
36
+ } except Exception as e {
37
+ tool_end("-> error", error=True);
38
+ return f"Error: {str(e)}";
39
+ }
40
+ }
41
+
42
+
43
+ impl jac_run(file: str, args: str = "") -> str {
44
+ (file, err) = sandbox_path(file);
45
+ if err {
46
+ return err;
47
+ }
48
+ tool_status("jac_run", file);
49
+ if not file {
50
+ return "Error: file path is required";
51
+ }
52
+
53
+ cmd = ["jac", "run", file];
54
+ if args {
55
+ cmd += args.split();
56
+ }
57
+
58
+ try {
59
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60);
60
+ output = ((result.stdout or "") + (result.stderr or "")).strip();
61
+ if not output {
62
+ return "(no output)";
63
+ }
64
+ if result.returncode != 0 {
65
+ output = f"Exit code: {result.returncode}\n{output}";
66
+ tool_end(f"-> exit {result.returncode}", error=True);
67
+ } else {
68
+ tool_end("-> ok");
69
+ }
70
+ (output, _) = truncate_output(output);
71
+ return output;
72
+ } except subprocess.TimeoutExpired {
73
+ tool_end("-> timeout", error=True);
74
+ return "Error: jac run timed out after 60 seconds";
75
+ } except Exception as e {
76
+ tool_end("-> error", error=True);
77
+ return f"Error: {str(e)}";
78
+ }
79
+ }
@@ -0,0 +1,32 @@
1
+ """Implementation of the mcp_call agent tool."""
2
+
3
+
4
+ impl mcp_call(server_name: str, tool_name: str, arguments_json: str = "{}") -> str {
5
+ tool_status("mcp_call", f"{server_name}.{tool_name}");
6
+
7
+ if not server_name or not tool_name {
8
+ tool_end("-> error: missing server_name or tool_name", error=True);
9
+ return "Error: server_name and tool_name are required.";
10
+ }
11
+
12
+ import json as _json;
13
+ try {
14
+ args: dict = _json.loads(arguments_json) if arguments_json else {};
15
+ } except Exception as e {
16
+ tool_end(f"-> error: invalid JSON — {e}", error=True);
17
+ return f"Error: arguments_json is invalid JSON — {e}";
18
+ }
19
+
20
+ try {
21
+ result: str = str(mcp_call_tool(server_name, tool_name, args));
22
+ } except Exception as e {
23
+ tool_end(f"-> error: {e}", error=True);
24
+ return f"Error: MCP call failed — {e}";
25
+ }
26
+
27
+ preview = result[:80].replace("\n", " ");
28
+ tool_end(f"-> {preview}");
29
+
30
+ (output, _) = truncate_output(result);
31
+ return output;
32
+ }
@@ -0,0 +1,233 @@
1
+ import os;
2
+ import json;
3
+ import base64;
4
+ import threading;
5
+ import httpx;
6
+ import from byllm.lib { Image }
7
+ import from jac_coder.tool.vision { analyze_screenshot, compare_screenshots }
8
+ import from jac_coder.util.tool_output { truncate_output, tool_status, tool_end }
9
+
10
+ # Thread-local storage for per-request state (preview URL, last screenshot).
11
+ # Avoids process-global os.environ which is unsafe with concurrent users.
12
+ glob _tl = threading.local();
13
+
14
+
15
+ """Get the preview URL from thread-local (set at turn start), falling back to env var.
16
+ The env var fallback is a convenience for local single-user dev only;
17
+ in production, set_preview_url() is called per-thread by jaccoder_adapter."""
18
+ def _get_preview_url() -> str {
19
+ return getattr(_tl, "preview_url", "") or os.environ.get("JACCODER_PREVIEW_URL", "");
20
+ }
21
+
22
+
23
+ """Set the preview URL for the current thread/request."""
24
+ impl set_preview_url(url: str) -> None {
25
+ _tl.preview_url = url;
26
+ }
27
+
28
+
29
+ """Call the MCP screenshot tool and return a dict with image data.
30
+ Returns {"success": True, "image_b64": ..., "mime_type": ...} on success,
31
+ or {"success": False, "error": ...} on failure.
32
+ delay is in SECONDS (converted to ms for MCP).
33
+ """
34
+ def _take_screenshot(url: str, viewport: str, delay: int) -> dict {
35
+
36
+ mcp_url = os.environ.get("JAC_MCP_SERVER_URL", "https://mcp.jaseci.org");
37
+
38
+ # Localhost preview can't be reached by a remote MCP server
39
+ if "localhost" in url and "localhost" not in mcp_url {
40
+ return {
41
+ "success": False,
42
+ "error": (
43
+ "Cannot screenshot localhost URL from remote MCP server. "
44
+ "Either: (1) set JAC_MCP_SERVER_URL to a local MCP server, "
45
+ "or (2) set JAC_IDE_PREVIEW_PUBLIC_URL_TEMPLATE so the preview "
46
+ "gets a public URL the remote MCP can reach."
47
+ )
48
+ };
49
+ }
50
+
51
+ # Convert delay from seconds to milliseconds for MCP
52
+ delay_ms = min(delay * 1000, 5000);
53
+
54
+ try {
55
+ with httpx.Client(timeout=60.0) as client {
56
+ resp = client.post(
57
+ f"{mcp_url}/mcp",
58
+ json={
59
+ "jsonrpc": "2.0",
60
+ "id": 1,
61
+ "method": "tools/call",
62
+ "params": {
63
+ "name": "take_screenshot",
64
+ "arguments": {
65
+ "url": url,
66
+ "viewport": viewport,
67
+ "format": "jpeg",
68
+ "full_page": False,
69
+ "delay": delay_ms
70
+ }
71
+ }
72
+ }
73
+ );
74
+ data = resp.json();
75
+ }
76
+ } except Exception as e {
77
+ return {"success": False, "error": f"MCP request failed: {e}"};
78
+ }
79
+
80
+ result = data.get("result", {});
81
+ if result.get("isError") {
82
+ error_text = result.get("content", [{}])[0].get("text", "Unknown MCP error");
83
+ return {"success": False, "error": error_text};
84
+ }
85
+
86
+ content_list = result.get("content", []);
87
+ if not content_list {
88
+ return {"success": False, "error": "Empty response from MCP"};
89
+ }
90
+
91
+ content = content_list[0];
92
+ if content.get("type") == "image" {
93
+ return {
94
+ "success": True,
95
+ "image_b64": content["data"],
96
+ "mime_type": content.get("mimeType", "image/jpeg")
97
+ };
98
+ }
99
+
100
+ return {"success": False, "error": content.get("text", "Unexpected response type")};
101
+ }
102
+
103
+
104
+ impl capture_preview(
105
+ viewport: str = "desktop",
106
+ delay: int = 2
107
+ ) -> str {
108
+ tool_status("capture_preview", f"viewport={viewport}");
109
+
110
+ preview_url = _get_preview_url();
111
+ if not preview_url {
112
+ tool_end("-> no preview URL", error=True);
113
+ return (
114
+ "Error: No live preview URL available. "
115
+ "The preview must be running in JacBuilder for this tool to work. "
116
+ "Set JACCODER_PREVIEW_URL environment variable or start the preview."
117
+ );
118
+ }
119
+
120
+ # Take screenshot via MCP
121
+ result = _take_screenshot(preview_url, viewport, delay);
122
+ if not result["success"] {
123
+ tool_end(f"-> screenshot failed: {str(result['error'])}", error=True);
124
+ return f"Error: Screenshot failed — {str(result['error'])}";
125
+ }
126
+
127
+ image_b64: str = str(result["image_b64"]);
128
+ mime_type: str = str(result["mime_type"]);
129
+ data_url = f"data:{mime_type};base64,{image_b64}";
130
+
131
+ # Store for before/after comparison in evaluate_preview (thread-local, not env)
132
+ _tl.last_screenshot = data_url;
133
+
134
+ # Brief vision analysis via byllm — keep it light (300 tokens) to save cost.
135
+ # The detailed evaluation belongs in evaluate_preview.
136
+ analysis = analyze_screenshot(
137
+ Image(data_url),
138
+ "Briefly describe this web app UI screenshot in 3-5 sentences. "
139
+ "Cover: layout structure, key visible elements, colors/theme, "
140
+ "and any obvious visual issues (broken layout, blank areas, overflow). "
141
+ "Be concise."
142
+ );
143
+
144
+ (output, _) = truncate_output(analysis);
145
+ tool_end(f"-> analyzed {viewport} view");
146
+ return f"Preview Screenshot Analysis ({viewport} viewport, URL: {preview_url}):\n\n{output}";
147
+ }
148
+
149
+
150
+ impl evaluate_preview(
151
+ intent: str,
152
+ viewport: str = "desktop",
153
+ delay: int = 2
154
+ ) -> str {
155
+ tool_status("evaluate_preview", intent[:80] + "..." if len(intent) > 80 else intent);
156
+
157
+ if not intent {
158
+ tool_end("-> no intent", error=True);
159
+ return "Error: intent description is required. Describe what the UI should look like.";
160
+ }
161
+
162
+ preview_url = _get_preview_url();
163
+ if not preview_url {
164
+ tool_end("-> no preview URL", error=True);
165
+ return (
166
+ "Error: No live preview URL available. "
167
+ "The preview must be running in JacBuilder for this tool to work."
168
+ );
169
+ }
170
+
171
+ # Take screenshot via MCP
172
+ result = _take_screenshot(preview_url, viewport, delay);
173
+ if not result["success"] {
174
+ tool_end(f"-> screenshot failed: {str(result['error'])}", error=True);
175
+ return f"Error: Screenshot failed — {str(result['error'])}";
176
+ }
177
+
178
+ current_b64: str = str(result["image_b64"]);
179
+ mime_type: str = str(result["mime_type"]);
180
+ current_data_url = f"data:{mime_type};base64,{current_b64}";
181
+
182
+ # Check for a previous screenshot for before/after comparison (thread-local)
183
+ before_data_url = getattr(_tl, "last_screenshot", "");
184
+
185
+ # Update stored screenshot (thread-local)
186
+ _tl.last_screenshot = current_data_url;
187
+
188
+ if before_data_url and before_data_url != current_data_url {
189
+ # Before/after comparison mode via byllm
190
+ prompt = (
191
+ f"Compare these TWO UI screenshots. The first image is BEFORE changes, "
192
+ f"the second image is AFTER changes.\n\n"
193
+ f"User's intended design: {intent}\n\n"
194
+ f"Evaluate:\n"
195
+ f"1. VERDICT: Does the AFTER screenshot match the user's intent? (PASS / PARTIAL / FAIL)\n"
196
+ f"2. SCORE: Rate the match 0-100\n"
197
+ f"3. MATCHES: What parts correctly match the intended design?\n"
198
+ f"4. MISMATCHES: What differs from the intended design?\n"
199
+ f"5. REGRESSIONS: Did any previously working elements break?\n"
200
+ f"6. FIXES: If not PASS, list specific CSS/layout changes needed in the .cl.jac code\n\n"
201
+ f"Be specific with element names, colors, and positions."
202
+ );
203
+
204
+ analysis = compare_screenshots(
205
+ Image(before_data_url),
206
+ Image(current_data_url),
207
+ prompt
208
+ );
209
+
210
+ (output, _) = truncate_output(analysis);
211
+ tool_end(f"-> compared before/after");
212
+ return f"Preview Evaluation — Before/After Comparison ({viewport}):\n\n{output}";
213
+ } else {
214
+ # Single-image evaluation mode via byllm
215
+ analysis = analyze_screenshot(
216
+ Image(current_data_url),
217
+ f"Evaluate this UI screenshot against the following intended design:\n\n"
218
+ f"INTENDED DESIGN: {intent}\n\n"
219
+ f"Evaluate:\n"
220
+ f"1. VERDICT: Does this UI match the intent? (PASS / PARTIAL / FAIL)\n"
221
+ f"2. SCORE: Rate the match 0-100\n"
222
+ f"3. MATCHES: What parts correctly match the intended design?\n"
223
+ f"4. MISMATCHES: What differs from the intended design?\n"
224
+ f"5. ISSUES: Any visual bugs (broken layout, overflow, missing elements)?\n"
225
+ f"6. FIXES: If not PASS, list specific CSS/layout changes needed in the .cl.jac code\n\n"
226
+ f"Be specific with element names, colors, and positions."
227
+ );
228
+
229
+ (output, _) = truncate_output(analysis);
230
+ tool_end(f"-> evaluated against intent");
231
+ return f"Preview Evaluation ({viewport}):\n\n{output}";
232
+ }
233
+ }
@@ -0,0 +1,29 @@
1
+ impl ask_question(question: str, options: str = "") -> str {
2
+ if not question {
3
+ return "Error: question is required";
4
+ }
5
+
6
+ if os.environ.get("JACCODER_WEB_MODE", "") {
7
+ return "Cannot ask user interactively in web mode. Proceed with your best judgment.";
8
+ }
9
+
10
+ print(f"\n{YELLOW}[Question]{RESET} {question}");
11
+
12
+ if options {
13
+ choices = [o.strip() for o in options.split(",")];
14
+ for (i, opt) in enumerate(choices) {
15
+ print(f" {i + 1}. {opt}");
16
+ }
17
+ answer = input("Your choice (number or text): ").strip();
18
+ try {
19
+ idx = int(answer) - 1;
20
+ if 0 <= idx < len(choices) {
21
+ answer = choices[idx];
22
+ }
23
+ } except ValueError { }
24
+ } else {
25
+ answer = input("Your answer: ").strip();
26
+ }
27
+
28
+ return f"User answered: {answer}";
29
+ }