File size: 4,918 Bytes
0810902
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""Interactive chat with Piko-9b, with streaming output.

    python examples/inference_cli.py
    python examples/inference_cli.py --quantization none --temperature 0.7

Commands inside the session:
    /image <path>   attach an image to the next message
    /system <text>  replace the system prompt and reset the conversation
    /reset          clear the conversation
    /exit           quit
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path
from threading import Thread
from typing import Any

import torch
from _common import add_common_arguments, generation_kwargs, load_model

DEFAULT_SYSTEM = "You are Piko-9, an AI assistant. Be accurate, direct, and concise."


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    add_common_arguments(parser)
    parser.add_argument("--system", default=DEFAULT_SYSTEM)
    args = parser.parse_args()

    model, processor = load_model(args.model, args.quantization, args.dtype, args.revision)

    try:
        from transformers import TextIteratorStreamer
    except ImportError:
        sys.exit("TextIteratorStreamer unavailable; upgrade transformers.")

    system = args.system
    history: list[dict[str, Any]] = []
    pending_image: str | None = None

    print("Piko-9b ready. /image <path>, /system <text>, /reset, /exit\n")

    while True:
        try:
            line = input(">>> ").strip()
        except (EOFError, KeyboardInterrupt):
            print()
            break

        if not line:
            continue
        if line in ("/exit", "/quit"):
            break
        if line == "/reset":
            history.clear()
            pending_image = None
            print("[conversation cleared]\n")
            continue
        if line.startswith("/system "):
            system = line[len("/system ") :].strip()
            history.clear()
            print("[system prompt set, conversation cleared]\n")
            continue
        if line.startswith("/image "):
            candidate = Path(line[len("/image ") :].strip()).expanduser()
            if not candidate.is_file():
                print(f"[no such file: {candidate}]\n")
                continue
            pending_image = str(candidate.resolve())
            print(f"[attached {candidate.name}; it will go with your next message]\n")
            continue

        content: list[dict[str, str]] = []
        if pending_image:
            content.append({"type": "image", "url": pending_image})
        content.append({"type": "text", "text": line})
        history.append({"role": "user", "content": content})
        pending_image = None

        messages = ([{"role": "system", "content": system}] if system else []) + history

        try:
            inputs = processor.apply_chat_template(
                messages,
                add_generation_prompt=True,
                tokenize=True,
                return_dict=True,
                return_tensors="pt",
            ).to(model.device)
        except ImportError as exc:
            if "orchvision" in str(exc):
                print("[image input needs torchvision: pip install torchvision]\n")
                history.pop()
                continue
            raise

        streamer = TextIteratorStreamer(
            processor.tokenizer, skip_prompt=True, skip_special_tokens=True
        )
        thread = Thread(
            target=_generate,
            args=(model, inputs, streamer, generation_kwargs(args)),
            daemon=True,
        )
        thread.start()

        pieces: list[str] = []
        in_reasoning = False
        for piece in streamer:
            pieces.append(piece)
            joined = "".join(pieces)
            if not args.show_reasoning:
                # Suppress the <think>...</think> span unless asked for.
                if "<think>" in joined and "</think>" not in joined:
                    if not in_reasoning:
                        print("[thinking…]", end="", flush=True)
                        in_reasoning = True
                    continue
                if in_reasoning and "</think>" in joined:
                    in_reasoning = False
                    print("\r" + " " * 12 + "\r", end="", flush=True)
                    piece = joined.rsplit("</think>", 1)[1]
            print(piece, end="", flush=True)
        thread.join()
        print("\n")

        history.append(
            {"role": "assistant", "content": [{"type": "text", "text": "".join(pieces)}]}
        )


def _generate(model: Any, inputs: Any, streamer: Any, kwargs: dict[str, Any]) -> None:
    try:
        with torch.inference_mode():
            model.generate(**inputs, streamer=streamer, **kwargs)
    except torch.cuda.OutOfMemoryError:
        print("\n[CUDA out of memory — try /reset, a shorter prompt, or 4-bit]", flush=True)


if __name__ == "__main__":
    main()