permerix 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.
- permerix/__init__.py +48 -0
- permerix/__main__.py +92 -0
- permerix/_versi.py +3 -0
- permerix/galat.py +68 -0
- permerix/jenis.py +93 -0
- permerix/klien.py +223 -0
- permerix-0.1.0.dist-info/METADATA +188 -0
- permerix-0.1.0.dist-info/RECORD +12 -0
- permerix-0.1.0.dist-info/WHEEL +5 -0
- permerix-0.1.0.dist-info/entry_points.txt +2 -0
- permerix-0.1.0.dist-info/licenses/LICENSE +21 -0
- permerix-0.1.0.dist-info/top_level.txt +1 -0
permerix/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Permerix - pustaka Python resmi.
|
|
3
|
+
|
|
4
|
+
Permerix adalah nama layanannya; Nusa adalah nama model di dalamnya -
|
|
5
|
+
hubungannya seperti Claude dengan Opus.
|
|
6
|
+
|
|
7
|
+
from permerix import Permerix
|
|
8
|
+
|
|
9
|
+
klien = Permerix(kunci="prx-live-xxxx")
|
|
10
|
+
print(klien.tanya("apa itu kalimat efektif"))
|
|
11
|
+
|
|
12
|
+
Mengalir:
|
|
13
|
+
|
|
14
|
+
for potong in klien.percakapan.alirkan("halo"):
|
|
15
|
+
print(potong.isi, end="", flush=True)
|
|
16
|
+
|
|
17
|
+
Tanpa dependensi apa pun - hanya pustaka bawaan Python.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from ._versi import __version__
|
|
21
|
+
from .galat import (
|
|
22
|
+
GalatJaringan,
|
|
23
|
+
GalatKunci,
|
|
24
|
+
GalatLayanan,
|
|
25
|
+
GalatPermerix,
|
|
26
|
+
KreditHabis,
|
|
27
|
+
ModelDitolak,
|
|
28
|
+
TerlaluCepat,
|
|
29
|
+
)
|
|
30
|
+
from .jenis import Balasan, Pemakaian, Potong
|
|
31
|
+
from .klien import ALAMAT_RESMI, MODEL_BAWAAN, Permerix
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Permerix",
|
|
35
|
+
"Balasan",
|
|
36
|
+
"Potong",
|
|
37
|
+
"Pemakaian",
|
|
38
|
+
"GalatPermerix",
|
|
39
|
+
"GalatKunci",
|
|
40
|
+
"KreditHabis",
|
|
41
|
+
"TerlaluCepat",
|
|
42
|
+
"ModelDitolak",
|
|
43
|
+
"GalatJaringan",
|
|
44
|
+
"GalatLayanan",
|
|
45
|
+
"ALAMAT_RESMI",
|
|
46
|
+
"MODEL_BAWAAN",
|
|
47
|
+
"__version__",
|
|
48
|
+
]
|
permerix/__main__.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Antarmuka baris perintah: python -m permerix
|
|
3
|
+
|
|
4
|
+
python -m permerix "apa itu kalimat efektif"
|
|
5
|
+
python -m permerix --alir "ceritakan tentang pantun"
|
|
6
|
+
python -m permerix --kredit
|
|
7
|
+
python -m permerix --model
|
|
8
|
+
|
|
9
|
+
Kunci diambil dari PERMERIX_API_KEY, atau lewat --kunci.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from ._versi import __version__
|
|
18
|
+
from .galat import GalatPermerix, KreditHabis, TerlaluCepat
|
|
19
|
+
from .klien import MODEL_BAWAAN, Permerix
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main(argv: list[str] | None = None) -> int:
|
|
23
|
+
urai = argparse.ArgumentParser(
|
|
24
|
+
prog="permerix",
|
|
25
|
+
description="Berbicara dengan Permerix dari baris perintah.")
|
|
26
|
+
urai.add_argument("pesan", nargs="*", help="pesan yang ingin dikirim")
|
|
27
|
+
urai.add_argument("--kunci", help="kunci API (bawaan: PERMERIX_API_KEY)")
|
|
28
|
+
urai.add_argument("--alamat", help="alamat layanan (bawaan: bawaan pustaka)")
|
|
29
|
+
urai.add_argument("--model", dest="model_dipakai", default=MODEL_BAWAAN,
|
|
30
|
+
help=f"model yang dipakai (bawaan: {MODEL_BAWAAN})")
|
|
31
|
+
urai.add_argument("--alir", action="store_true",
|
|
32
|
+
help="tampilkan jawaban sepotong demi sepotong")
|
|
33
|
+
urai.add_argument("--kredit", action="store_true",
|
|
34
|
+
help="tampilkan sisa kredit lalu keluar")
|
|
35
|
+
urai.add_argument("--daftar-model", action="store_true",
|
|
36
|
+
help="tampilkan model yang tersedia lalu keluar")
|
|
37
|
+
urai.add_argument("--versi", action="version",
|
|
38
|
+
version=f"permerix {__version__}")
|
|
39
|
+
pilihan = urai.parse_args(argv)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
klien = Permerix(kunci=pilihan.kunci, alamat=pilihan.alamat)
|
|
43
|
+
|
|
44
|
+
if pilihan.kredit:
|
|
45
|
+
data = klien.kredit()
|
|
46
|
+
sisa = "tanpa batas" if data.get("tanpa_batas") else data.get("kredit")
|
|
47
|
+
print(f"Peran : {data.get('peran')}")
|
|
48
|
+
print(f"Kredit : {sisa}")
|
|
49
|
+
print(f"Pesan : {data.get('pesan')}")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
if pilihan.daftar_model:
|
|
53
|
+
for m in klien.model():
|
|
54
|
+
print(f"{m['id']:<12} {m.get('nama', '')} [{m.get('status', '')}]")
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
pesan = " ".join(pilihan.pesan).strip()
|
|
58
|
+
if not pesan:
|
|
59
|
+
urai.print_help()
|
|
60
|
+
return 2
|
|
61
|
+
|
|
62
|
+
if pilihan.alir:
|
|
63
|
+
for potong in klien.percakapan.alirkan(pesan, model=pilihan.model_dipakai):
|
|
64
|
+
if potong.selesai:
|
|
65
|
+
print()
|
|
66
|
+
if potong.pemakaian:
|
|
67
|
+
print(f"[{potong.pemakaian}]", file=sys.stderr)
|
|
68
|
+
else:
|
|
69
|
+
print(potong.isi, end="", flush=True)
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
balasan = klien.percakapan.kirim(pesan, model=pilihan.model_dipakai)
|
|
73
|
+
print(balasan.isi)
|
|
74
|
+
print(f"[{balasan.pemakaian}]", file=sys.stderr)
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
except KreditHabis as e:
|
|
78
|
+
print(f"Kredit habis: {e.pesan}", file=sys.stderr)
|
|
79
|
+
return 3
|
|
80
|
+
except TerlaluCepat as e:
|
|
81
|
+
print(f"Terlalu cepat: {e.pesan}", file=sys.stderr)
|
|
82
|
+
return 4
|
|
83
|
+
except GalatPermerix as e:
|
|
84
|
+
print(f"Gagal: {e}", file=sys.stderr)
|
|
85
|
+
return 1
|
|
86
|
+
except KeyboardInterrupt:
|
|
87
|
+
print(file=sys.stderr)
|
|
88
|
+
return 130
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
sys.exit(main())
|
permerix/_versi.py
ADDED
permerix/galat.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kesalahan yang bisa dilempar pustaka Permerix.
|
|
3
|
+
|
|
4
|
+
Semuanya turunan GalatPermerix, jadi satu blok `except GalatPermerix`
|
|
5
|
+
sudah cukup untuk menangkap apa pun. Yang ingin membedakan penyebabnya
|
|
6
|
+
bisa menangkap turunan yang lebih khusus.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GalatPermerix(Exception):
|
|
13
|
+
"""Induk semua kesalahan Permerix."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, pesan: str, kode: int = 0, jenis: str = "") -> None:
|
|
16
|
+
super().__init__(pesan)
|
|
17
|
+
self.pesan = pesan
|
|
18
|
+
self.kode = kode # status HTTP, 0 bila belum sampai server
|
|
19
|
+
self.jenis = jenis # penanda dari server, mis. "galat_kunci"
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return f"[{self.kode}] {self.pesan}" if self.kode else self.pesan
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GalatKunci(GalatPermerix):
|
|
26
|
+
"""Kunci API tidak ada, salah, atau sudah dicabut. (401/403)"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class KreditHabis(GalatPermerix):
|
|
30
|
+
"""Kredit pengguna sudah nol. Isi ulang lewat bot Telegram. (402)"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TerlaluCepat(GalatPermerix):
|
|
34
|
+
"""Melewati batas laju. Tunggu sebentar lalu coba lagi. (429)"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, pesan: str, kode: int = 429, jenis: str = "",
|
|
37
|
+
tunggu: int = 0) -> None:
|
|
38
|
+
super().__init__(pesan, kode, jenis)
|
|
39
|
+
self.tunggu = tunggu # perkiraan detik sebelum boleh mencoba lagi
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ModelDitolak(GalatPermerix):
|
|
43
|
+
"""Model tidak dikenal, belum dirilis, atau perlu peran lebih tinggi. (403)"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GalatJaringan(GalatPermerix):
|
|
47
|
+
"""Server tidak bisa dihubungi sama sekali."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GalatLayanan(GalatPermerix):
|
|
51
|
+
"""Server menjawab dengan kesalahan di sisinya. (5xx)"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def dari_tanggapan(kode: int, pesan: str, jenis: str = "") -> GalatPermerix:
|
|
55
|
+
"""Pilih jenis kesalahan yang paling tepat dari status HTTP."""
|
|
56
|
+
if kode == 402:
|
|
57
|
+
return KreditHabis(pesan, kode, jenis)
|
|
58
|
+
if kode == 429:
|
|
59
|
+
return TerlaluCepat(pesan, kode, jenis)
|
|
60
|
+
if kode in (401,) or jenis == "galat_kunci":
|
|
61
|
+
return GalatKunci(pesan, kode, jenis)
|
|
62
|
+
if kode == 403:
|
|
63
|
+
# 403 bisa berarti kunci admin dicabut ATAU model tidak boleh dipakai.
|
|
64
|
+
return (ModelDitolak(pesan, kode, jenis) if jenis == "model_ditolak"
|
|
65
|
+
else GalatKunci(pesan, kode, jenis))
|
|
66
|
+
if kode >= 500:
|
|
67
|
+
return GalatLayanan(pesan, kode, jenis)
|
|
68
|
+
return GalatPermerix(pesan, kode, jenis)
|
permerix/jenis.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Bentuk data yang dikembalikan pustaka: Balasan, Potong, Pemakaian."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Pemakaian:
|
|
11
|
+
"""Berapa kredit yang terpakai untuk satu permintaan."""
|
|
12
|
+
|
|
13
|
+
kredit: int = 0
|
|
14
|
+
rincian: str = ""
|
|
15
|
+
sisa_kredit: Optional[int] = None # None berarti tanpa batas (owner)
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def tanpa_batas(self) -> bool:
|
|
19
|
+
return self.sisa_kredit is None
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def dari(cls, data: dict[str, Any] | None) -> "Pemakaian":
|
|
23
|
+
data = data or {}
|
|
24
|
+
return cls(
|
|
25
|
+
kredit=int(data.get("kredit") or 0),
|
|
26
|
+
rincian=str(data.get("rincian") or ""),
|
|
27
|
+
sisa_kredit=data.get("sisa_kredit"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
sisa = "tanpa batas" if self.tanpa_batas else str(self.sisa_kredit)
|
|
32
|
+
return f"{self.kredit} kredit (sisa {sisa})"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Balasan:
|
|
37
|
+
"""Jawaban utuh dari satu percakapan."""
|
|
38
|
+
|
|
39
|
+
isi: str = ""
|
|
40
|
+
model: str = ""
|
|
41
|
+
bertanya_balik: bool = False
|
|
42
|
+
pemakaian: Pemakaian = field(default_factory=Pemakaian)
|
|
43
|
+
mentah: dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def dari(cls, data: dict[str, Any]) -> "Balasan":
|
|
47
|
+
# Terima dua bentuk: gaya Permerix dan gaya serupa OpenAI, supaya
|
|
48
|
+
# pustaka ini tetap bekerja bila pemanggil memakai jalur /completions.
|
|
49
|
+
balasan = data.get("balasan")
|
|
50
|
+
if isinstance(balasan, dict):
|
|
51
|
+
isi = str(balasan.get("isi") or "")
|
|
52
|
+
else:
|
|
53
|
+
pilihan = (data.get("choices") or [{}])[0]
|
|
54
|
+
isi = str((pilihan.get("message") or {}).get("content") or "")
|
|
55
|
+
|
|
56
|
+
return cls(
|
|
57
|
+
isi=isi,
|
|
58
|
+
model=str(data.get("model") or ""),
|
|
59
|
+
bertanya_balik=bool(data.get("bertanya_balik")),
|
|
60
|
+
pemakaian=Pemakaian.dari(data.get("pemakaian") or data.get("usage")),
|
|
61
|
+
mentah=data,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def kredit(self) -> int:
|
|
66
|
+
return self.pemakaian.kredit
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def sisa_kredit(self) -> Optional[int]:
|
|
70
|
+
return self.pemakaian.sisa_kredit
|
|
71
|
+
|
|
72
|
+
def __str__(self) -> str:
|
|
73
|
+
return self.isi
|
|
74
|
+
|
|
75
|
+
def __repr__(self) -> str:
|
|
76
|
+
cuplik = self.isi[:48] + ("..." if len(self.isi) > 48 else "")
|
|
77
|
+
return f"<Balasan {self.pemakaian}: {cuplik!r}>"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Potong:
|
|
82
|
+
"""Satu kepingan jawaban saat memakai alir=True."""
|
|
83
|
+
|
|
84
|
+
isi: str = ""
|
|
85
|
+
selesai: bool = False
|
|
86
|
+
pemakaian: Optional[Pemakaian] = None
|
|
87
|
+
|
|
88
|
+
def __str__(self) -> str:
|
|
89
|
+
return self.isi
|
|
90
|
+
|
|
91
|
+
def __bool__(self) -> bool:
|
|
92
|
+
# Supaya `if potong:` berarti "ada isinya", bukan sekadar "objek ada".
|
|
93
|
+
return bool(self.isi)
|
permerix/klien.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Klien Permerix.
|
|
3
|
+
|
|
4
|
+
from permerix import Permerix
|
|
5
|
+
|
|
6
|
+
klien = Permerix(kunci="prx-live-xxxx")
|
|
7
|
+
print(klien.percakapan.kirim("apa itu kalimat efektif").isi)
|
|
8
|
+
|
|
9
|
+
Hanya memakai pustaka bawaan Python - tidak ada dependensi sama sekali,
|
|
10
|
+
jadi `pip install permerix` tidak menarik paket lain apa pun.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
from typing import Any, Iterator, Optional, Sequence, Union
|
|
21
|
+
|
|
22
|
+
from ._versi import __version__
|
|
23
|
+
from .galat import GalatJaringan, GalatPermerix, dari_tanggapan
|
|
24
|
+
from .jenis import Balasan, Pemakaian, Potong
|
|
25
|
+
|
|
26
|
+
# ===========================================================================
|
|
27
|
+
# ALAMAT LAYANAN
|
|
28
|
+
#
|
|
29
|
+
# Nilai di bawah ini yang dipakai bila pemanggil tidak menyebutkan alamat.
|
|
30
|
+
# Beginilah pustaka seperti Groq bekerja: alamatnya tertanam di pustaka,
|
|
31
|
+
# sehingga pemakainya cukup menulis Permerix(kunci="...").
|
|
32
|
+
#
|
|
33
|
+
# SEBELUM MENERBITKAN KE PyPI, ganti dengan alamat layananmu yang sebenarnya
|
|
34
|
+
# (sebaiknya domain ber-HTTPS, bukan IP mentah).
|
|
35
|
+
# ===========================================================================
|
|
36
|
+
ALAMAT_RESMI = "http://localhost:8080"
|
|
37
|
+
|
|
38
|
+
MODEL_BAWAAN = "nusa-0.2"
|
|
39
|
+
TENGGANG_BAWAAN = 180.0
|
|
40
|
+
COBA_ULANG = 2 # berapa kali mencoba lagi bila jaringan gagal
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _alamat_bawaan() -> str:
|
|
44
|
+
"""
|
|
45
|
+
Alamat yang dipakai bila tidak disebutkan.
|
|
46
|
+
|
|
47
|
+
Dibaca saat klien DIBUAT, bukan saat modul diimpor: kalau dibaca sekali di
|
|
48
|
+
awal, PERMERIX_API_URL yang dipasang belakangan - misalnya oleh skrip yang
|
|
49
|
+
memuat .env sesudah import - tidak akan pernah terpakai.
|
|
50
|
+
"""
|
|
51
|
+
return os.environ.get("PERMERIX_API_URL") or ALAMAT_RESMI
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
Pesan = Union[str, Sequence[dict]]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _bakukan_pesan(pesan: Pesan) -> list[dict]:
|
|
58
|
+
"""Terima string biasa maupun daftar pesan bergaya percakapan."""
|
|
59
|
+
if isinstance(pesan, str):
|
|
60
|
+
return [{"peran": "pengguna", "isi": pesan}]
|
|
61
|
+
if isinstance(pesan, dict):
|
|
62
|
+
return [pesan]
|
|
63
|
+
return list(pesan)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _Percakapan:
|
|
67
|
+
"""Bagian percakapan dari klien: klien.percakapan.kirim(...)."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, klien: "Permerix") -> None:
|
|
70
|
+
self._klien = klien
|
|
71
|
+
|
|
72
|
+
def kirim(self, pesan: Pesan, model: str = MODEL_BAWAAN,
|
|
73
|
+
**tambahan: Any) -> Balasan:
|
|
74
|
+
"""Kirim percakapan lalu tunggu jawaban utuh."""
|
|
75
|
+
badan = {"model": model, "pesan": _bakukan_pesan(pesan), "alir": False}
|
|
76
|
+
badan.update(tambahan)
|
|
77
|
+
return Balasan.dari(self._klien._minta("POST", "/v1/chat", badan))
|
|
78
|
+
|
|
79
|
+
def alirkan(self, pesan: Pesan, model: str = MODEL_BAWAAN,
|
|
80
|
+
**tambahan: Any) -> Iterator[Potong]:
|
|
81
|
+
"""Kirim percakapan lalu terima jawaban sepotong demi sepotong."""
|
|
82
|
+
badan = {"model": model, "pesan": _bakukan_pesan(pesan), "alir": True}
|
|
83
|
+
badan.update(tambahan)
|
|
84
|
+
return self._klien._alir("/v1/chat", badan)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Permerix:
|
|
88
|
+
"""
|
|
89
|
+
Klien layanan Permerix.
|
|
90
|
+
|
|
91
|
+
kunci : kunci API dari bot Telegram. Bila kosong, diambil dari peubah
|
|
92
|
+
lingkungan PERMERIX_API_KEY.
|
|
93
|
+
alamat : alamat layanan. Bila kosong, memakai PERMERIX_API_URL atau
|
|
94
|
+
alamat yang tertanam di pustaka.
|
|
95
|
+
tenggang: batas waktu menunggu jawaban, dalam detik.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, kunci: Optional[str] = None,
|
|
99
|
+
alamat: Optional[str] = None,
|
|
100
|
+
tenggang: float = TENGGANG_BAWAAN,
|
|
101
|
+
coba_ulang: int = COBA_ULANG) -> None:
|
|
102
|
+
self.kunci = (kunci or os.environ.get("PERMERIX_API_KEY") or "").strip()
|
|
103
|
+
if not self.kunci:
|
|
104
|
+
raise GalatPermerix(
|
|
105
|
+
"kunci API belum diisi. Ambil lewat bot Telegram, lalu "
|
|
106
|
+
"berikan dengan Permerix(kunci=\"prx-live-...\") atau pasang "
|
|
107
|
+
"peubah lingkungan PERMERIX_API_KEY.")
|
|
108
|
+
|
|
109
|
+
self.alamat = (alamat or _alamat_bawaan()).rstrip("/")
|
|
110
|
+
self.tenggang = tenggang
|
|
111
|
+
self.coba_ulang = max(0, int(coba_ulang))
|
|
112
|
+
self.percakapan = _Percakapan(self)
|
|
113
|
+
|
|
114
|
+
# ------------------------------------------------------------- jaringan
|
|
115
|
+
def _permintaan(self, metode: str, jalur: str,
|
|
116
|
+
badan: Optional[dict] = None) -> urllib.request.Request:
|
|
117
|
+
data = json.dumps(badan).encode("utf-8") if badan is not None else None
|
|
118
|
+
return urllib.request.Request(
|
|
119
|
+
self.alamat + jalur, data=data, method=metode,
|
|
120
|
+
headers={
|
|
121
|
+
"Authorization": f"Bearer {self.kunci}",
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
"Accept": "application/json",
|
|
124
|
+
"User-Agent": f"permerix-python/{__version__}",
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
def _buka(self, metode: str, jalur: str, badan: Optional[dict] = None):
|
|
128
|
+
"""Buka sambungan, ubah kegagalan HTTP jadi kesalahan yang jelas."""
|
|
129
|
+
galat_akhir: Optional[Exception] = None
|
|
130
|
+
|
|
131
|
+
for percobaan in range(self.coba_ulang + 1):
|
|
132
|
+
try:
|
|
133
|
+
return urllib.request.urlopen(
|
|
134
|
+
self._permintaan(metode, jalur, badan), timeout=self.tenggang)
|
|
135
|
+
|
|
136
|
+
except urllib.error.HTTPError as e:
|
|
137
|
+
pesan, jenis = f"HTTP {e.code}", ""
|
|
138
|
+
try:
|
|
139
|
+
isi = json.loads(e.read().decode("utf-8"))
|
|
140
|
+
keterangan = isi.get("galat") or {}
|
|
141
|
+
pesan = keterangan.get("pesan", pesan)
|
|
142
|
+
jenis = keterangan.get("jenis", "")
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
# Kesalahan HTTP tidak diulang: server sudah menjawab dengan
|
|
146
|
+
# tegas, mengulanginya hanya membuang kredit dan waktu.
|
|
147
|
+
raise dari_tanggapan(e.code, pesan, jenis) from None
|
|
148
|
+
|
|
149
|
+
except urllib.error.URLError as e:
|
|
150
|
+
galat_akhir = e
|
|
151
|
+
except (TimeoutError, OSError) as e:
|
|
152
|
+
galat_akhir = e
|
|
153
|
+
|
|
154
|
+
# Hanya kegagalan jaringan yang diulang, dengan jeda menaik.
|
|
155
|
+
if percobaan < self.coba_ulang:
|
|
156
|
+
time.sleep(0.5 * (percobaan + 1))
|
|
157
|
+
|
|
158
|
+
alasan = getattr(galat_akhir, "reason", galat_akhir)
|
|
159
|
+
raise GalatJaringan(
|
|
160
|
+
f"tidak bisa menghubungi {self.alamat}: {alasan}") from None
|
|
161
|
+
|
|
162
|
+
def _minta(self, metode: str, jalur: str,
|
|
163
|
+
badan: Optional[dict] = None) -> dict:
|
|
164
|
+
with self._buka(metode, jalur, badan) as tanggapan:
|
|
165
|
+
teks = tanggapan.read().decode("utf-8")
|
|
166
|
+
try:
|
|
167
|
+
return json.loads(teks)
|
|
168
|
+
except json.JSONDecodeError:
|
|
169
|
+
raise GalatPermerix(
|
|
170
|
+
f"tanggapan tidak terbaca: {teks[:120]}") from None
|
|
171
|
+
|
|
172
|
+
def _alir(self, jalur: str, badan: dict) -> Iterator[Potong]:
|
|
173
|
+
# Aliran tidak diulang: sebagian jawaban mungkin sudah terkirim, dan
|
|
174
|
+
# mengulangnya akan menghasilkan jawaban ganda.
|
|
175
|
+
klien_alir = Permerix.__new__(Permerix)
|
|
176
|
+
klien_alir.__dict__.update(self.__dict__)
|
|
177
|
+
klien_alir.coba_ulang = 0
|
|
178
|
+
|
|
179
|
+
with klien_alir._buka("POST", jalur, badan) as tanggapan:
|
|
180
|
+
for baris_bita in tanggapan:
|
|
181
|
+
baris = baris_bita.decode("utf-8").strip()
|
|
182
|
+
if not baris.startswith("data:"):
|
|
183
|
+
continue
|
|
184
|
+
muatan = baris[5:].strip()
|
|
185
|
+
if muatan == "[DONE]":
|
|
186
|
+
return
|
|
187
|
+
try:
|
|
188
|
+
data = json.loads(muatan)
|
|
189
|
+
except json.JSONDecodeError:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if data.get("selesai"):
|
|
193
|
+
yield Potong(selesai=True,
|
|
194
|
+
pemakaian=Pemakaian.dari(data.get("pemakaian")))
|
|
195
|
+
return
|
|
196
|
+
# Dua bentuk: gaya Permerix dan gaya serupa OpenAI.
|
|
197
|
+
isi = (data.get("potong") or {}).get("isi")
|
|
198
|
+
if isi is None:
|
|
199
|
+
pilihan = (data.get("choices") or [{}])[0]
|
|
200
|
+
isi = (pilihan.get("delta") or {}).get("content")
|
|
201
|
+
if isi:
|
|
202
|
+
yield Potong(isi=isi)
|
|
203
|
+
|
|
204
|
+
# --------------------------------------------------------------- guna
|
|
205
|
+
def model(self) -> list[dict]:
|
|
206
|
+
"""Daftar model yang boleh dipakai kunci ini."""
|
|
207
|
+
return self._minta("GET", "/v1/model").get("data", [])
|
|
208
|
+
|
|
209
|
+
def kredit(self) -> dict:
|
|
210
|
+
"""Sisa kredit dan ringkasan pemakaian."""
|
|
211
|
+
return self._minta("GET", "/v1/kredit")
|
|
212
|
+
|
|
213
|
+
def sehat(self) -> dict:
|
|
214
|
+
"""Keadaan layanan. Tidak memerlukan kunci yang sah."""
|
|
215
|
+
return self._minta("GET", "/sehat")
|
|
216
|
+
|
|
217
|
+
# ------------------------------------------------------------- ringkas
|
|
218
|
+
def tanya(self, pesan: Pesan, model: str = MODEL_BAWAAN, **tambahan) -> str:
|
|
219
|
+
"""Jalan pintas: kirim satu pesan, kembalikan teks jawabannya saja."""
|
|
220
|
+
return self.percakapan.kirim(pesan, model=model, **tambahan).isi
|
|
221
|
+
|
|
222
|
+
def __repr__(self) -> str:
|
|
223
|
+
return f"<Permerix {self.alamat} kunci=...{self.kunci[-6:]}>"
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: permerix
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pustaka Python resmi untuk layanan Permerix (model Nusa).
|
|
5
|
+
Author: Daffa Farid
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Daffa7382466/permerix
|
|
8
|
+
Project-URL: Repository, https://github.com/Daffa7382466/permerix
|
|
9
|
+
Project-URL: Issues, https://github.com/Daffa7382466/permerix/issues
|
|
10
|
+
Keywords: permerix,nusa,ai,bahasa-indonesia,nlp,sdk
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Natural Language :: Indonesian
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Permerix
|
|
30
|
+
|
|
31
|
+
Pustaka Python resmi untuk layanan **Permerix**.
|
|
32
|
+
|
|
33
|
+
**Permerix** adalah nama layanannya; **Nusa** adalah nama model di dalamnya —
|
|
34
|
+
hubungannya seperti Claude dengan Opus.
|
|
35
|
+
|
|
36
|
+
Nusa adalah AI berbahasa Indonesia yang dibangun dari nol: mesin n-gram dan
|
|
37
|
+
peleburan graf kata, tanpa jaringan saraf. Ia menyusun kalimatnya sendiri dan
|
|
38
|
+
bisa mencari tahu ke internet ketika tidak tahu.
|
|
39
|
+
|
|
40
|
+
**Tanpa dependensi.** `pip install permerix` tidak menarik paket lain apa pun —
|
|
41
|
+
pustaka ini hanya memakai modul bawaan Python.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Pasang
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install permerix
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Pakai
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from permerix import Permerix
|
|
55
|
+
|
|
56
|
+
klien = Permerix(kunci="prx-live-xxxx")
|
|
57
|
+
|
|
58
|
+
print(klien.tanya("apa itu kalimat efektif"))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Alamat layanan sudah tertanam di pustaka, jadi kamu tidak perlu mengaturnya.
|
|
62
|
+
|
|
63
|
+
### Jawaban lengkap
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
balasan = klien.percakapan.kirim("apa itu pantun")
|
|
67
|
+
|
|
68
|
+
print(balasan.isi)
|
|
69
|
+
print(balasan.kredit) # kredit yang terpakai
|
|
70
|
+
print(balasan.sisa_kredit) # sisa; None berarti tanpa batas
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Mengalir
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
for potong in klien.percakapan.alirkan("ceritakan tentang pantun"):
|
|
77
|
+
print(potong.isi, end="", flush=True)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Cek kredit dan model
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
klien.kredit() # {'kredit': 98, 'peran': 'user', 'pesan': 2, ...}
|
|
84
|
+
klien.model() # [{'id': 'nusa-0.2', 'nama': 'Nusa 0.2', ...}]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Dari baris perintah
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export PERMERIX_API_KEY=prx-live-xxxx
|
|
93
|
+
|
|
94
|
+
permerix "apa itu kalimat efektif"
|
|
95
|
+
permerix --alir "ceritakan tentang pantun"
|
|
96
|
+
permerix --kredit
|
|
97
|
+
permerix --daftar-model
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Atau tanpa memasang perintahnya: `python -m permerix "halo"`
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Mengambil kunci
|
|
105
|
+
|
|
106
|
+
Kunci API diambil lewat **bot Telegram** Permerix:
|
|
107
|
+
|
|
108
|
+
1. Buka botnya, tekan `/start`
|
|
109
|
+
2. Tekan tombol **Key** → **Buat Kunci**
|
|
110
|
+
3. Salin kunci `prx-live-...`
|
|
111
|
+
|
|
112
|
+
Pengguna baru mendapat **100 kredit gratis**. Satu pesan biasanya 1 kredit,
|
|
113
|
+
naik sampai 5 bila Nusa perlu mencari ke internet.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Pengaturan
|
|
118
|
+
|
|
119
|
+
| Peubah lingkungan | Guna |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `PERMERIX_API_KEY` | kunci API, supaya tidak perlu ditulis di kode |
|
|
122
|
+
| `PERMERIX_API_URL` | alamat layanan; menimpa alamat bawaan pustaka |
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# Semua pengaturan bisa juga diberikan langsung
|
|
126
|
+
klien = Permerix(
|
|
127
|
+
kunci="prx-live-xxxx",
|
|
128
|
+
alamat="https://api.contoh.com", # opsional
|
|
129
|
+
tenggang=180, # detik
|
|
130
|
+
coba_ulang=2, # ulangi bila jaringan gagal
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Menangani kesalahan
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from permerix import Permerix, KreditHabis, TerlaluCepat, GalatPermerix
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
print(klien.tanya("halo"))
|
|
143
|
+
except KreditHabis:
|
|
144
|
+
print("Kredit habis, isi ulang lewat bot Telegram.")
|
|
145
|
+
except TerlaluCepat as e:
|
|
146
|
+
print(f"Terlalu cepat, tunggu sebentar. {e.pesan}")
|
|
147
|
+
except GalatPermerix as e:
|
|
148
|
+
print(f"Gagal ({e.kode}): {e.pesan}")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Semua kesalahan turunan `GalatPermerix`, jadi satu blok `except` sudah cukup.
|
|
152
|
+
|
|
153
|
+
| Kesalahan | Kapan |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `GalatKunci` | kunci salah, tidak ada, atau sudah dicabut |
|
|
156
|
+
| `KreditHabis` | kredit sudah nol |
|
|
157
|
+
| `TerlaluCepat` | melewati batas laju |
|
|
158
|
+
| `ModelDitolak` | model belum dirilis atau perlu peran lebih tinggi |
|
|
159
|
+
| `GalatJaringan` | server tidak bisa dihubungi |
|
|
160
|
+
| `GalatLayanan` | server bermasalah di sisinya |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Model
|
|
165
|
+
|
|
166
|
+
| Model | Keterangan |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `nusa-0.2` | model bawaan, sudah dirilis |
|
|
169
|
+
|
|
170
|
+
Model berikutnya akan dibuka lebih dulu untuk pengguna Premium.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Batas jujur
|
|
175
|
+
|
|
176
|
+
Nusa **bukan model bahasa besar**. Ia menyusun ulang kalimat yang pernah
|
|
177
|
+
dibacanya memakai n-gram dan peleburan graf kata, tanpa jaringan saraf dan
|
|
178
|
+
tanpa penalaran. Kalimatnya kadang masih pincang, terutama untuk pertanyaan
|
|
179
|
+
yang menuntut hafalan atau perhitungan berantai.
|
|
180
|
+
|
|
181
|
+
Yang dijaga: ia tidak menyalin mentah hasil pencarian, tidak membalik makna
|
|
182
|
+
kalimat, dan mengaku tidak tahu daripada mengarang.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Lisensi
|
|
187
|
+
|
|
188
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
permerix/__init__.py,sha256=5QQBbb0wH72WbSUzHj0bz36thkbykgMMaBcFs5K-oLU,1007
|
|
2
|
+
permerix/__main__.py,sha256=ZhGzBG-eK-eriktMNCZEDoE7za-ZvCsUSdLvi77Weyo,3262
|
|
3
|
+
permerix/_versi.py,sha256=mDHSli6XuEeB10AeNPcHxag91AkPk6U2w_YHi75nB_M,83
|
|
4
|
+
permerix/galat.py,sha256=Q55ObSkJzTfQhXkbzoo7zM7oIB_90wpQdFCzvUOQBFU,2325
|
|
5
|
+
permerix/jenis.py,sha256=4svGHV28z_l3ElA0EVotIWga2QvVUxubwAaN3zuIFNU,2720
|
|
6
|
+
permerix/klien.py,sha256=bdoXbePE8XiWeNYTJ1q8_01X3vy8fg6_K6qXANDEPNo,8836
|
|
7
|
+
permerix-0.1.0.dist-info/licenses/LICENSE,sha256=F8vZWx_BVS1FTXBdjt0gKJeaft3x4ESrrGHSyxzXWoA,1068
|
|
8
|
+
permerix-0.1.0.dist-info/METADATA,sha256=qk8pE7m872HNQl_CnbILui8hifr6xzVDENwMAKA9oRA,5155
|
|
9
|
+
permerix-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
10
|
+
permerix-0.1.0.dist-info/entry_points.txt,sha256=l2QTDmucbKYK3YpoWVmk_mprBgV36TAr1B39826BDfw,52
|
|
11
|
+
permerix-0.1.0.dist-info/top_level.txt,sha256=ftdmdT3EIiKd20puyvtG93NzV-7hI9fXkCm6I2hsBFY,9
|
|
12
|
+
permerix-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daffa Farid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
permerix
|