• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

marscoin / martianrepublic / 23773190405

30 Mar 2026 11:43PM UTC coverage: 9.277% (-0.07%) from 9.35%
23773190405

push

github

Martian Congress
fix: multi-wallet signing — try all mnemonics when key doesn't match

When sending from a legacy address, the primary mnemonic's key doesn't
match the UTXO. The brute-force fallback now tries all stored mnemonics
from _allMnemonics (stored during unlock) across multiple derivation
paths until it finds the key that can sign for the UTXO address.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

507 of 5465 relevant lines covered (9.28%)

1.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

27.33
/app/Http/Controllers/AiHelperController.php
1
<?php
2

3
namespace App\Http\Controllers;
4

5
use Illuminate\Http\Request;
6
use Illuminate\Support\Facades\Http;
7
use Illuminate\Support\Facades\Log;
8
use Symfony\Component\HttpFoundation\StreamedResponse;
9

10
class AiHelperController extends Controller
11
{
12
    private function getSystemPrompt(): string
13
    {
14
        return <<<'PROMPT'
15
You are Olympus, the AI guide of the Martian Republic — a blockchain-based governance platform for Mars built on the Marscoin network.
16

17
Your role: Help citizens and newcomers understand how the Republic works. Be warm, concise, and jargon-free. You embody civic pride and the pioneer spirit of Mars settlement.
18

19
You have access to a search tool to look up Academy articles and site content. Use it when you need specific details about governance, voting, Mars time, or any topic covered in the Academy. Don't guess — search first if unsure.
20

21
Key knowledge:
22
- Direct democracy on the Marscoin blockchain
23
- 4 governance tiers: Signal, Operational, Legislative, Constitutional
24
- CoinShuffle for anonymous voting (cryptographic ballot mixing)
25
- Citizenship requires registration, 2FA, and an HD wallet ("The Forge")
26
- Mars time: 1 sol = 24h 39m 35s, Darian calendar (24 months, 668.6 sols/year)
27
- MARS cryptocurrency: proof-of-work, merge-mined with Litecoin
28

29
Academy articles:
30
- /academy/governance-and-voting — 4-tier governance system
31
- /academy/coinshuffle-secret-ballots — anonymous voting protocol
32
- /academy/mars-timekeeping — sols, MTC, Darian calendar
33
- /academy/the-public-good — political philosophy
34

35
Rules:
36
- Keep answers under 150 words unless asked for detail
37
- Never make up information — use the search tool if unsure
38
- Link to Academy articles when relevant (use markdown links)
39
- No price speculation or investment advice
40
- Be encouraging — every citizen matters
41
PROMPT;
42
    }
43

44
    private function getTools(): array
1✔
45
    {
46
        return [
1✔
47
            [
1✔
48
                'type' => 'function',
1✔
49
                'function' => [
1✔
50
                    'name' => 'search_content',
1✔
51
                    'description' => 'Search the Martian Republic Academy articles and site content for information. Use this to find accurate details about governance, voting, Mars time, citizenship, proposals, or any topic.',
1✔
52
                    'parameters' => [
1✔
53
                        'type' => 'object',
1✔
54
                        'properties' => [
1✔
55
                            'query' => [
1✔
56
                                'type' => 'string',
1✔
57
                                'description' => 'Search query — keywords or a question to search for in Academy articles',
1✔
58
                            ],
1✔
59
                        ],
1✔
60
                        'required' => ['query'],
1✔
61
                    ],
1✔
62
                ],
1✔
63
            ],
1✔
64
        ];
1✔
65
    }
66

67
    private function executeSearch(string $query): string
×
68
    {
69
        $searchPaths = [
×
70
            resource_path('views/academy'),
×
71
            resource_path('views/academy/articles'),
×
72
        ];
×
73

74
        $keywords = array_filter(
×
75
            explode(' ', strtolower(preg_replace('/[^a-zA-Z0-9\s]/', '', $query))),
×
76
            fn($w) => strlen($w) > 2
×
77
        );
×
78

79
        if (empty($keywords)) return 'No results found.';
×
80

81
        $results = [];
×
82

83
        foreach ($searchPaths as $path) {
×
84
            $files = glob($path . '/*.blade.php') ?: [];
×
85
            foreach ($files as $file) {
×
86
                $content = file_get_contents($file);
×
87
                $text = strip_tags($content);
×
88
                $text = preg_replace('/\s+/', ' ', $text);
×
89
                $lower = strtolower($text);
×
90

91
                $score = 0;
×
92
                foreach ($keywords as $kw) {
×
93
                    $score += substr_count($lower, $kw);
×
94
                }
95

96
                if ($score > 0) {
×
97
                    $slug = basename($file, '.blade.php');
×
98
                    // Get multiple excerpts around matches
99
                    $excerpts = [];
×
100
                    foreach ($keywords as $kw) {
×
101
                        $offset = 0;
×
102
                        while (($pos = strpos($lower, $kw, $offset)) !== false && count($excerpts) < 3) {
×
103
                            $start = max(0, $pos - 80);
×
104
                            $excerpts[] = '...' . trim(substr($text, $start, 250)) . '...';
×
105
                            $offset = $pos + strlen($kw) + 100;
×
106
                        }
107
                    }
108
                    $results[] = [
×
109
                        'score' => $score,
×
110
                        'slug' => $slug,
×
111
                        'url' => '/academy/' . $slug,
×
112
                        'excerpts' => array_unique(array_slice($excerpts, 0, 3)),
×
113
                    ];
×
114
                }
115
            }
116
        }
117

118
        usort($results, fn($a, $b) => $b['score'] <=> $a['score']);
×
119
        $top = array_slice($results, 0, 3);
×
120

121
        if (empty($top)) return 'No matching Academy articles found for: ' . $query;
×
122

123
        $output = "Search results for \"{$query}\":\n\n";
×
124
        foreach ($top as $r) {
×
125
            $output .= "--- {$r['url']} (relevance: {$r['score']}) ---\n";
×
126
            foreach ($r['excerpts'] as $ex) {
×
127
                $output .= $ex . "\n";
×
128
            }
129
            $output .= "\n";
×
130
        }
131
        return $output;
×
132
    }
133

134
    public function chat(Request $request)
3✔
135
    {
136
        $request->validate([
3✔
137
            'messages' => 'required|array|min:1',
3✔
138
            'messages.*.role' => 'required|in:user,assistant',
3✔
139
            'messages.*.content' => 'required|string|max:2000',
3✔
140
        ]);
3✔
141

142
        $userMessages = array_slice($request->input('messages'), -10);
1✔
143
        $apiKey = config('services.openrouter.api_key');
1✔
144
        $model = config('services.openrouter.model', 'qwen/qwen3-235b-a22b-2507');
1✔
145

146
        $messages = array_merge(
1✔
147
            [['role' => 'system', 'content' => $this->getSystemPrompt()]],
1✔
148
            $userMessages
1✔
149
        );
1✔
150

151
        // Step 1: Non-streaming call to check for tool use
152
        $firstResponse = Http::withHeaders([
1✔
153
            'Authorization' => 'Bearer ' . $apiKey,
1✔
154
            'HTTP-Referer' => 'https://martianrepublic.org',
1✔
155
            'X-Title' => 'Martian Republic',
1✔
156
        ])->timeout(30)->post('https://openrouter.ai/api/v1/chat/completions', [
1✔
157
            'model' => $model,
1✔
158
            'messages' => $messages,
1✔
159
            'tools' => $this->getTools(),
1✔
160
            'max_tokens' => 500,
1✔
161
            'temperature' => 0.7,
1✔
162
        ]);
1✔
163

164
        $firstData = $firstResponse->json();
×
165
        $choice = $firstData['choices'][0] ?? null;
×
166

167
        if (!$choice) {
×
168
            return response()->json(['error' => 'No response from AI'], 502);
×
169
        }
170

171
        // Step 2: If tool call, execute and do a second call with results
172
        if (($choice['finish_reason'] ?? '') === 'tool_calls' ||
×
173
            !empty($choice['message']['tool_calls'])) {
×
174

175
            $toolCalls = $choice['message']['tool_calls'] ?? [];
×
176
            $assistantMsg = $choice['message'];
×
177
            $messages[] = $assistantMsg;
×
178

179
            foreach ($toolCalls as $tc) {
×
180
                $args = json_decode($tc['function']['arguments'] ?? '{}', true);
×
181
                $searchQuery = $args['query'] ?? '';
×
182
                $result = $this->executeSearch($searchQuery);
×
183

184
                $messages[] = [
×
185
                    'role' => 'tool',
×
186
                    'tool_call_id' => $tc['id'],
×
187
                    'content' => $result,
×
188
                ];
×
189
            }
190

191
            // Step 3: Stream the final response with tool results
192
            return $this->streamResponse($messages, $apiKey, $model);
×
193
        }
194

195
        // No tool call — stream directly from the first response content
196
        $content = $choice['message']['content'] ?? '';
×
197
        return new StreamedResponse(function () use ($content) {
×
198
            echo 'data: ' . json_encode(['content' => $content]) . "\n\n";
×
199
            echo "data: [DONE]\n\n";
×
200
            ob_flush();
×
201
            flush();
×
202
        }, 200, [
×
203
            'Content-Type' => 'text/event-stream',
×
204
            'Cache-Control' => 'no-cache',
×
205
            'Connection' => 'keep-alive',
×
206
            'X-Accel-Buffering' => 'no',
×
207
        ]);
×
208
    }
209

210
    private function streamResponse(array $messages, string $apiKey, string $model): StreamedResponse
×
211
    {
212
        return new StreamedResponse(function () use ($messages, $apiKey, $model) {
×
213
            $response = Http::withHeaders([
×
214
                'Authorization' => 'Bearer ' . $apiKey,
×
215
                'HTTP-Referer' => 'https://martianrepublic.org',
×
216
                'X-Title' => 'Martian Republic',
×
217
            ])->withOptions([
×
218
                'stream' => true,
×
219
            ])->post('https://openrouter.ai/api/v1/chat/completions', [
×
220
                'model' => $model,
×
221
                'messages' => $messages,
×
222
                'stream' => true,
×
223
                'max_tokens' => 500,
×
224
                'temperature' => 0.7,
×
225
            ]);
×
226

227
            $body = $response->getBody();
×
228
            while (!$body->eof()) {
×
229
                $line = '';
×
230
                while (!$body->eof()) {
×
231
                    $char = $body->read(1);
×
232
                    if ($char === "\n") break;
×
233
                    $line .= $char;
×
234
                }
235
                $line = trim($line);
×
236
                if (str_starts_with($line, 'data: ')) {
×
237
                    $data = substr($line, 6);
×
238
                    if ($data === '[DONE]') {
×
239
                        echo "data: [DONE]\n\n";
×
240
                        break;
×
241
                    }
242
                    $json = json_decode($data, true);
×
243
                    if ($json && isset($json['choices'][0]['delta']['content'])) {
×
244
                        $token = $json['choices'][0]['delta']['content'];
×
245
                        echo 'data: ' . json_encode(['content' => $token]) . "\n\n";
×
246
                        ob_flush();
×
247
                        flush();
×
248
                    }
249
                }
250
            }
251
        }, 200, [
×
252
            'Content-Type' => 'text/event-stream',
×
253
            'Cache-Control' => 'no-cache',
×
254
            'Connection' => 'keep-alive',
×
255
            'X-Accel-Buffering' => 'no',
×
256
        ]);
×
257
    }
258
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc