File size: 13,053 Bytes
a1a5a09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
"""
Sequential Thinking — Gradio MCP App

Ports the Sequential Thinking MCP Server (TypeScript) to a Python Gradio app
that exposes the same tool via Gradio's built-in MCP server support.

Launch:  python app.py
MCP SSE endpoint:  http://localhost:7860/gradio_api/mcp/sse
"""

import json
import os
import sys
from typing import Optional
from uuid import uuid4

# On Windows the default console encoding (cp1252) cannot represent the emoji
# characters that Gradio prints on startup.  Reconfigure stdout/stderr to
# UTF-8 early so those prints succeed.  When running via `run.ps1` (or with
# `python -X utf8 app.py`) the streams are already UTF-8 and this is a no-op.
if sys.platform == "win32":
    for _stream in (sys.stdout, sys.stderr):
        try:
            if hasattr(_stream, "reconfigure") and _stream.encoding.lower() != "utf-8":
                _stream.reconfigure(encoding="utf-8", errors="replace")
        except Exception:
            pass

import gradio as gr
from theme import soft_professional_theme
from chat_logger import log_chat, shutdown_logger

# ---------------------------------------------------------------------------
# Core state (mirrors SequentialThinkingServer in lib.ts)
# ---------------------------------------------------------------------------

_thought_history: list[dict] = []
_branches: dict[str, list[dict]] = {}
_session_id: str = uuid4().hex  # Session identifier for logging


# ---------------------------------------------------------------------------
# Tool logic
# ---------------------------------------------------------------------------

def sequential_thinking(
    thought: str,
    thought_number: int,
    total_thoughts: int,
    next_thought_needed: bool,
    is_revision: bool = False,
    revises_thought: Optional[int] = None,
    branch_from_thought: Optional[int] = None,
    branch_id: Optional[str] = None,
    needs_more_thoughts: bool = False,
) -> dict:
    """
    A detailed tool for dynamic and reflective problem-solving through thoughts.
    This tool helps analyze problems through a flexible thinking process that
    can adapt and evolve. Each thought can build on, question, or revise
    previous insights as understanding deepens.

    Use this tool when:
    - Breaking down complex problems into steps
    - Planning and design with room for revision
    - Analysis that might need course correction
    - Problems where the full scope is not clear initially
    - Tasks that need context maintained over multiple steps

    Args:
        thought: Your current thinking step (analytical step, revision, question, hypothesis, etc.)
        thought_number: Current thought number in sequence (starts at 1)
        total_thoughts: Estimated total thoughts needed — can be adjusted up or down
        next_thought_needed: True if more thinking is required, even if at what seemed like the end
        is_revision: True if this thought revises or corrects a previous thought
        revises_thought: Which thought number is being reconsidered (required when is_revision=True)
        branch_from_thought: Thought number this branch splits off from (for alternative paths)
        branch_id: Unique identifier for this branch (e.g. "alternative-approach")
        needs_more_thoughts: True if you realise more thoughts are needed beyond totalThoughts

    Returns:
        dict with thoughtNumber, totalThoughts, nextThoughtNeeded, branches, thoughtHistoryLength
    """
    global _thought_history, _branches

    # Sanitise optional int inputs that arrive as 0 from Gradio Number widgets
    if revises_thought == 0:
        revises_thought = None
    if branch_from_thought == 0:
        branch_from_thought = None

    # Mirror TypeScript behaviour: expand totalThoughts if thoughtNumber exceeds it
    if thought_number > total_thoughts:
        total_thoughts = thought_number

    thought_data: dict = {
        "thought": thought,
        "thoughtNumber": thought_number,
        "totalThoughts": total_thoughts,
        "isRevision": is_revision,
        "revisesThought": revises_thought,
        "branchFromThought": branch_from_thought,
        "branchId": branch_id if branch_id and branch_id.strip() else None,
        "needsMoreThoughts": needs_more_thoughts,
        "nextThoughtNeeded": next_thought_needed,
    }

    _thought_history.append(thought_data)

    if branch_from_thought and branch_id and branch_id.strip():
        _branches.setdefault(branch_id, []).append(thought_data)

    _log_thought(thought_data)

    # Log to local storage
    log_chat(
        session_id=_session_id,
        model_name="sequential-thinking",
        thought=thought,
        thought_number=thought_number,
        total_thoughts=total_thoughts,
        metadata={
            "is_revision": is_revision,
            "revises_thought": revises_thought,
            "branch_from_thought": branch_from_thought,
            "branch_id": branch_id,
            "needs_more_thoughts": needs_more_thoughts,
            "next_thought_needed": next_thought_needed,
        }
    )

    return {
        "thoughtNumber": thought_number,
        "totalThoughts": total_thoughts,
        "nextThoughtNeeded": next_thought_needed,
        "branches": list(_branches.keys()),
        "thoughtHistoryLength": len(_thought_history),
    }


def reset_session() -> str:
    """
    Reset the thought history and all branches, starting a fresh problem-solving session.

    Returns:
        Confirmation message string.
    """
    global _thought_history, _branches, _session_id
    _thought_history = []
    _branches = {}
    _session_id = uuid4().hex  # Generate new session ID
    return "Session reset. Ready for a new problem-solving session."


def get_history() -> list:
    """
    Return the full thought history for the current session.

    Returns:
        List of all recorded thought objects.
    """
    return _thought_history


# ---------------------------------------------------------------------------
# Logging helper (mirrors lib.ts formatThought, without chalk colours)
# ---------------------------------------------------------------------------

def _log_thought(data: dict) -> None:
    if os.environ.get("DISABLE_THOUGHT_LOGGING", "").lower() == "true":
        return

    t_num = data["thoughtNumber"]
    t_total = data["totalThoughts"]
    thought = data["thought"]

    if data.get("isRevision"):
        label = f"[Revision] {t_num}/{t_total} (revising thought {data.get('revisesThought')})"
    elif data.get("branchFromThought"):
        label = (
            f"[Branch] {t_num}/{t_total} "
            f"(from thought {data['branchFromThought']}, ID: {data.get('branchId')})"
        )
    else:
        label = f"[Thought] {t_num}/{t_total}"

    width = max(len(label), len(thought)) + 4
    border = "-" * width
    try:
        print(f"\n+{border}+")
        print(f"| {label.ljust(width - 2)} |")
        print(f"+{border}+")
        print(f"| {thought.ljust(width - 2)} |")
        print(f"+{border}+")
    except UnicodeEncodeError:
        print(f"\n{label}\n  {thought}")


# ---------------------------------------------------------------------------
# Gradio UI
# ---------------------------------------------------------------------------

DESCRIPTION = """
# 🧠 Sequential Thinking MCP Server

Dynamic, reflective problem-solving through structured thoughts.  
This Gradio app exposes the **Sequential Thinking** tool as an MCP server.

**MCP endpoint (SSE):** `http://localhost:7860/gradio_api/mcp/sse`
"""

# Load custom CSS
with open('styles.css', 'r') as f:
    custom_css = f.read()

with gr.Blocks(title="Sequential Thinking MCP", theme=soft_professional_theme, css=custom_css) as demo:
    gr.Markdown(DESCRIPTION)

    with gr.Tabs():

        # ------------------------------------------------------------------
        # Tab 1 — process a thought
        # ------------------------------------------------------------------
        with gr.Tab("Process Thought"):
            with gr.Row(equal_height=False):
                with gr.Column(scale=1):
                    thought_input = gr.Textbox(
                        label="Thought",
                        placeholder="Enter your current thinking step…",
                        lines=5,
                    )
                    with gr.Row():
                        thought_number_input = gr.Number(
                            label="Thought Number", value=1, minimum=1, precision=0
                        )
                        total_thoughts_input = gr.Number(
                            label="Total Thoughts (estimate)", value=5, minimum=1, precision=0
                        )
                    next_thought_needed_input = gr.Checkbox(
                        label="Next Thought Needed", value=True
                    )

                    with gr.Accordion("Advanced options", open=False):
                        is_revision_input = gr.Checkbox(label="Is Revision", value=False)
                        revises_thought_input = gr.Number(
                            label="Revises Thought #",
                            minimum=0,
                            precision=0,
                            value=0,
                            info="0 = not applicable",
                        )
                        branch_from_thought_input = gr.Number(
                            label="Branch From Thought #",
                            minimum=0,
                            precision=0,
                            value=0,
                            info="0 = not applicable",
                        )
                        branch_id_input = gr.Textbox(
                            label="Branch ID",
                            placeholder="e.g. alternative-approach",
                        )
                        needs_more_thoughts_input = gr.Checkbox(
                            label="Needs More Thoughts", value=False
                        )

                    with gr.Row():
                        submit_btn = gr.Button("Process Thought", variant="primary")
                        reset_btn = gr.Button("Reset Session", variant="secondary")

                with gr.Column(scale=1):
                    output_json = gr.JSON(label="Result")
                    status_box = gr.Textbox(label="Status", interactive=False, visible=True)

            submit_btn.click(
                fn=sequential_thinking,
                inputs=[
                    thought_input,
                    thought_number_input,
                    total_thoughts_input,
                    next_thought_needed_input,
                    is_revision_input,
                    revises_thought_input,
                    branch_from_thought_input,
                    branch_id_input,
                    needs_more_thoughts_input,
                ],
                outputs=output_json,
            )

            reset_btn.click(fn=reset_session, inputs=[], outputs=status_box)

        # ------------------------------------------------------------------
        # Tab 2 — inspect history
        # ------------------------------------------------------------------
        with gr.Tab("Thought History"):
            refresh_btn = gr.Button("Refresh")
            history_output = gr.JSON(label="All Thoughts")
            refresh_btn.click(fn=get_history, inputs=[], outputs=history_output)

        # ------------------------------------------------------------------
        # Tab 3 — quick reference
        # ------------------------------------------------------------------
        with gr.Tab("MCP Reference"):
            gr.Markdown("""
## Connecting via MCP

Add this to your MCP client configuration:

```json
{
  "mcpServers": {
    "sequential-thinking": {
      "url": "http://localhost:7860/gradio_api/mcp/sse"
    }
  }
}
```

## Exposed MCP Tools

| Tool | Description |
|------|-------------|
| `sequential_thinking` | Process a single thought step |
| `reset_session` | Clear history and branches |
| `get_history` | Return the full thought history |

## Parameters for `sequential_thinking`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `thought` | string | ✅ | Current thinking step |
| `thought_number` | int | ✅ | Step index (starts at 1) |
| `total_thoughts` | int | ✅ | Estimated total steps |
| `next_thought_needed` | bool | ✅ | More steps required? |
| `is_revision` | bool | | Revises an earlier thought |
| `revises_thought` | int | | Which thought is revised |
| `branch_from_thought` | int | | Branch origin thought |
| `branch_id` | string | | Branch label |
| `needs_more_thoughts` | bool | | Extend beyond estimate |

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `DISABLE_THOUGHT_LOGGING` | `false` | Set to `true` to suppress console output |
""")


if __name__ == "__main__":
    try:
        demo.launch(mcp_server=True)
    finally:
        shutdown_logger()