├── characterai ├── types │ ├── __init__.py │ ├── user.py │ ├── recent.py │ ├── other.py │ ├── chat2.py │ ├── account.py │ ├── character.py │ └── chat1.py ├── errors.py ├── pycai │ ├── __init__.py │ ├── methods │ │ ├── __init__.py │ │ ├── users.py │ │ ├── recent.py │ │ ├── chats.py │ │ ├── other.py │ │ ├── utils.py │ │ ├── chat1.py │ │ ├── account.py │ │ ├── characters.py │ │ └── chat2.py │ └── client.py ├── aiocai │ ├── __init__.py │ ├── methods │ │ ├── __init__.py │ │ ├── users.py │ │ ├── recent.py │ │ ├── chats.py │ │ ├── other.py │ │ ├── utils.py │ │ ├── chat1.py │ │ ├── account.py │ │ ├── characters.py │ │ └── chat2.py │ └── client.py ├── __init__.py └── auth.py ├── docs ├── _static │ ├── theme.js │ ├── search.svg │ └── style.css ├── _templates │ └── autosummary │ │ ├── class.rst │ │ └── method.rst ├── images │ ├── logo.png │ ├── title.png │ └── full_logo.png ├── client.rst ├── errors.rst ├── support.rst ├── Makefile ├── make.bat ├── qna.rst ├── types │ └── index.rst ├── auth.rst ├── methods │ └── index.rst ├── index.rst ├── changelog.rst ├── conf.py └── starting.rst ├── requirements.txt ├── .github └── FUNDING.yml ├── examples ├── sync │ ├── login.py │ ├── auth.py │ ├── chat1.py │ └── chat2.py └── async │ ├── login.py │ ├── auth.py │ ├── chat1.py │ └── chat2.py ├── .readthedocs.yml ├── setup.py ├── LICENSE ├── README.md └── .gitignore /characterai/types/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/theme.js: -------------------------------------------------------------------------------- 1 | document.body.dataset.theme = 'dark'; -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pydantic>=2.7.1 2 | websockets 3 | curl_cffi 4 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{name | underline}} 2 | 3 | .. autoclass:: {{fullname}}() -------------------------------------------------------------------------------- /docs/_templates/autosummary/method.rst: -------------------------------------------------------------------------------- 1 | {{name | underline}} 2 | 3 | .. automethod:: {{fullname}} -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kramcat/CharacterAI/HEAD/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kramcat/CharacterAI/HEAD/docs/images/title.png -------------------------------------------------------------------------------- /docs/images/full_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kramcat/CharacterAI/HEAD/docs/images/full_logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['qiwi.com/n/KRAMCAT'] 4 | -------------------------------------------------------------------------------- /characterai/errors.py: -------------------------------------------------------------------------------- 1 | class CAIError(Exception): 2 | ... 3 | 4 | class ServerError(CAIError): 5 | ... 6 | 7 | class AuthError(CAIError): 8 | ... 9 | 10 | class NotFoundError(CAIError): 11 | ... 12 | 13 | class JSONError(CAIError): 14 | ... -------------------------------------------------------------------------------- /examples/sync/login.py: -------------------------------------------------------------------------------- 1 | from characterai import pycai, sendCode, authUser 2 | 3 | email = input('Enter your email: ') 4 | 5 | code = sendCode(email) 6 | 7 | link = input('Enter the link: ') 8 | 9 | token = authUser(link, email) 10 | 11 | print(f'YOUR TOKEN: {token}') 12 | -------------------------------------------------------------------------------- /characterai/pycai/__init__.py: -------------------------------------------------------------------------------- 1 | # ____ _________ ____ 2 | # / __ \__ __/ ____/ | / _/ 3 | # / /_/ / / / / / / /| | / / 4 | # / ____/ /_/ / /___/ ___ |_/ / 5 | # /_/ \__, /\____/_/ |_/___/ 6 | # /____/ 7 | 8 | from .client import pycai -------------------------------------------------------------------------------- /characterai/aiocai/__init__.py: -------------------------------------------------------------------------------- 1 | # ___ _ _________ ____ 2 | # ___ _ _________ ____ 3 | # / | (_)___ / ____/ | / _/ 4 | # / /| | / / __ \/ / / /| | / / 5 | # / ___ |/ / /_/ / /___/ ___ |_/ / 6 | # /_/ |_/_/\____/\____/_/ |_/___/ 7 | 8 | from .client import aiocai -------------------------------------------------------------------------------- /docs/_static/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | jobs: 8 | post_install: 9 | - pip install sphinx-inline-tabs sphinx-copybutton furo sphinxcontrib-towncrier pydantic curl_cffi websockets 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | formats: all 15 | -------------------------------------------------------------------------------- /examples/sync/auth.py: -------------------------------------------------------------------------------- 1 | from characterai import pycai 2 | 3 | # Usually 4 | client = pycai.Client('TOKEN') 5 | 6 | print(client.get_me()) 7 | 8 | client.close() 9 | 10 | # Via context manager 11 | with pycai.Client('TOKEN') as client: 12 | print(client.get_me()) 13 | 14 | # Via the function 15 | print(pycai.get_me(token='TOKEN')) -------------------------------------------------------------------------------- /examples/async/login.py: -------------------------------------------------------------------------------- 1 | from characterai import aiocai, sendCode, authUser 2 | import asyncio 3 | 4 | async def main(): 5 | email = input('Enter your email: ') 6 | 7 | code = sendCode(email) 8 | 9 | link = input('Enter the link: ') 10 | 11 | token = authUser(link, email) 12 | 13 | print(f'YOUR TOKEN: {token}') 14 | 15 | asyncio.run(main()) 16 | -------------------------------------------------------------------------------- /characterai/aiocai/methods/__init__.py: -------------------------------------------------------------------------------- 1 | from .users import Users 2 | from .characters import Characters 3 | from .account import Account 4 | from .other import Other 5 | from .recent import Recent 6 | from .chats import Chats 7 | from .chat2 import ChatV2 8 | 9 | class Methods( 10 | Users, Characters, 11 | Account, Recent, 12 | Chats, Other, ChatV2 13 | ): 14 | ... -------------------------------------------------------------------------------- /characterai/pycai/methods/__init__.py: -------------------------------------------------------------------------------- 1 | from .users import Users 2 | from .characters import Characters 3 | from .account import Account 4 | from .other import Other 5 | from .recent import Recent 6 | from .chats import Chats 7 | from .chat2 import ChatV2 8 | 9 | class Methods( 10 | Users, Characters, 11 | Account, Recent, 12 | Chats, Other, ChatV2 13 | ): 14 | ... -------------------------------------------------------------------------------- /examples/sync/chat1.py: -------------------------------------------------------------------------------- 1 | from characterai import pycai 2 | 3 | token = 'YOUR TOKEN' 4 | 5 | client = pycai.Client(token) 6 | 7 | char = input('CHAR: ') 8 | 9 | new = client.chat1.new_chat(char) 10 | 11 | while True: 12 | text = input('YOU: ') 13 | 14 | message = client.chat1.send_message( 15 | new.id, new.tgt, text 16 | ) 17 | 18 | print(f'{message.author}: {message.text}') -------------------------------------------------------------------------------- /docs/client.rst: -------------------------------------------------------------------------------- 1 | ###### 2 | Client 3 | ###### 4 | 5 | The main class for working with the library 6 | 7 | It can also work as a context manager 8 | 9 | .. code-block:: python 10 | 11 | async with aiocai.Client('TOKEN') as client: 12 | await client.get_me() 13 | 14 | .. autoclass:: characterai.aiocai.client.aiocai.Client() 15 | 16 | .. autofunction:: characterai.aiocai.client.aiocai.Client.close -------------------------------------------------------------------------------- /characterai/__init__.py: -------------------------------------------------------------------------------- 1 | # DISCLAIMER: 2 | # This is not an official library and is not coordinated with developers who may not like it. 3 | # You may use the library for any purpose and modify it as you wish, 4 | # but you must be sure to include the name KRAMCAT as the original author 5 | 6 | __version__ = '1.0.0a1' 7 | 8 | from .aiocai.client import aiocai 9 | from .pycai.client import pycai 10 | from .auth import sendCode, authUser, authGuest -------------------------------------------------------------------------------- /examples/async/auth.py: -------------------------------------------------------------------------------- 1 | from characterai import aiocai 2 | import asyncio 3 | 4 | async def main(): 5 | # Usually 6 | client = aiocai.Client('TOKEN') 7 | 8 | print(await client.get_me()) 9 | 10 | await client.close() 11 | 12 | # Via context manager 13 | async with aiocai.Client('TOKEN') as client: 14 | print(await client.get_me()) 15 | 16 | # Via the function 17 | print(await aiocai.get_me(token='TOKEN')) 18 | 19 | asyncio.run(main()) -------------------------------------------------------------------------------- /docs/errors.rst: -------------------------------------------------------------------------------- 1 | ###### 2 | Errors 3 | ###### 4 | 5 | ServerError 6 | =========== 7 | 8 | Any error on the server side 9 | 10 | AuthError 11 | ========= 12 | 13 | It means that you have the wrong token 14 | 15 | NotFoundError 16 | ============= 17 | 18 | The user's public account was not found 19 | 20 | JSONError 21 | ========= 22 | 23 | The server response contains a response that cannot be decoded in JSON. These can be either individual characters or the entire text, which may not be JSON -------------------------------------------------------------------------------- /docs/support.rst: -------------------------------------------------------------------------------- 1 | ####### 2 | Support 3 | ####### 4 | 5 | If you like this library and documentation, you can support my project through cryptocurrency 6 | 7 | Thank you so much for all donations, it really supports my work with the library and future projects 8 | 9 | | TON - ``UQBlGz8aw5tWaocR8gPppQe6SgTx-kkh5keInKtEzVOqPhdY`` 10 | | BTC - ``bc1qghtyl43jd6xr66wwtrxkpe04sglqlwgcp04yl9`` 11 | | ETH - ``0x1489B0DDCE07C029040331e4c66F5aA94D7B4d4e`` 12 | | USDT (TRC20) - ``TJpvALv9YiL2khFBb7xfWrUDpvL5nYFs8u`` -------------------------------------------------------------------------------- /examples/async/chat1.py: -------------------------------------------------------------------------------- 1 | from characterai import aiocai 2 | import asyncio 3 | 4 | token = 'YOUR TOKEN' 5 | 6 | async def main(): 7 | client = aiocai.Client(token) 8 | 9 | char = input('CHAR: ') 10 | 11 | new = await client.chat1.new_chat(char) 12 | 13 | while True: 14 | text = input('YOU: ') 15 | 16 | message = await client.chat1.send_message( 17 | new.id, new.tgt, text 18 | ) 19 | 20 | print(f'{message.author}: {message.text}') 21 | 22 | asyncio.run(main()) -------------------------------------------------------------------------------- /examples/sync/chat2.py: -------------------------------------------------------------------------------- 1 | from characterai import pycai 2 | 3 | char = input('CHAR ID: ') 4 | 5 | client = pycai.Client('TOKEN') 6 | 7 | me = client.get_me() 8 | 9 | with client.connect() as chat: 10 | new, answer = chat.new_chat( 11 | char, me.id 12 | ) 13 | 14 | print(f'{answer.name}: {answer.text}') 15 | 16 | while True: 17 | text = input('YOU: ') 18 | 19 | message = chat.send_message( 20 | char, new.chat_id, text 21 | ) 22 | 23 | print(f'{message.name}: {message.text}') -------------------------------------------------------------------------------- /examples/async/chat2.py: -------------------------------------------------------------------------------- 1 | from characterai import aiocai 2 | import asyncio 3 | 4 | async def main(): 5 | char = input('CHAR ID: ') 6 | 7 | client = aiocai.Client('TOKEN') 8 | 9 | me = await client.get_me() 10 | 11 | async with await client.connect() as chat: 12 | new, answer = await chat.new_chat( 13 | char, me.id 14 | ) 15 | 16 | print(f'{answer.name}: {answer.text}') 17 | 18 | while True: 19 | text = input('YOU: ') 20 | 21 | message = await chat.send_message( 22 | char, new.chat_id, text 23 | ) 24 | 25 | print(f'{message.name}: {message.text}') 26 | 27 | asyncio.run(main()) -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /characterai/pycai/methods/users.py: -------------------------------------------------------------------------------- 1 | from ...errors import NotFoundError 2 | from ...types import user 3 | 4 | from .utils import caimethod 5 | 6 | class Users: 7 | @caimethod 8 | def get_user( 9 | self, username: str, *, token: str = None 10 | ): 11 | """User info by nickname 12 | 13 | EXAMPLE:: 14 | 15 | await client.get_user('USERNAME') 16 | 17 | Args: 18 | username (``str``): 19 | User nickname 20 | 21 | Returns: 22 | :obj:`~characterai.types.user.User` 23 | """ 24 | data = self.request( 25 | 'chat/user/public/', token=token, 26 | data={'username': username} 27 | ) 28 | 29 | if data['public_user'] == []: 30 | raise NotFoundError( 31 | f'User {username} not found.' 32 | ) 33 | 34 | return user.User.model_validate( 35 | data['public_user'] 36 | ) -------------------------------------------------------------------------------- /characterai/aiocai/methods/users.py: -------------------------------------------------------------------------------- 1 | from ...errors import NotFoundError 2 | from ...types import user 3 | 4 | from .utils import caimethod 5 | 6 | class Users: 7 | @caimethod 8 | async def get_user( 9 | self, username: str, *, token: str = None 10 | ): 11 | """User info by nickname 12 | 13 | EXAMPLE:: 14 | 15 | await client.get_user('USERNAME') 16 | 17 | Args: 18 | username (``str``): 19 | User nickname 20 | 21 | Returns: 22 | :obj:`~characterai.types.user.User` 23 | """ 24 | data = await self.request( 25 | 'chat/user/public/', token=token, 26 | data={'username': username} 27 | ) 28 | 29 | if data['public_user'] == []: 30 | raise NotFoundError( 31 | f'User {username} not found.' 32 | ) 33 | 34 | return user.User.model_validate( 35 | data['public_user'] 36 | ) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('README.md', encoding='utf-8') as f: 4 | readme = f.read() 5 | 6 | setup( 7 | name='characterai', 8 | version='1.0.0', 9 | description='An unofficial API for Character AI for Python', 10 | keywords='ai wrapper api library', 11 | long_description=readme, 12 | long_description_content_type='text/markdown', 13 | url='https://github.com/kramcat/characterai', 14 | author='kramcat', 15 | license='MIT', 16 | install_requires=['pydantic', 'curl_cffi', 'websockets'], 17 | packages=find_packages(include=['characterai*']), 18 | project_urls={ 19 | 'Community': 'https://discord.gg/ZHJe3tXQkf', 20 | 'Source': 'https://github.com/kramcat/characterai', 21 | 'Documentation': 'https://docs.kram.cat', 22 | }, 23 | classifiers=[ 24 | 'Programming Language :: Python :: 3.10', 25 | 'License :: OSI Approved :: MIT License', 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mark 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. 22 | -------------------------------------------------------------------------------- /characterai/types/user.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List 3 | 4 | from .other import Avatar 5 | from .character import CharShort 6 | 7 | class User(BaseModel, Avatar): 8 | """User info 9 | 10 | Parameters: 11 | characters (List of :obj:`~characterai.types.character.CharShort`): 12 | List of user's public characters 13 | 14 | username (``str``): 15 | Public username 16 | 17 | name (``str``): 18 | Public name 19 | 20 | num_following (``int``): 21 | Number of users subscribed to the account 22 | 23 | num_followers (``int``): 24 | Number of users who are subscribed to the account 25 | 26 | avatar_file_name (``str``, *optional*): 27 | Path to the avatar on the server 28 | 29 | subscription_type (``str``): 30 | Type of c.ai subscription 31 | 32 | bio (``str``, *optional*): 33 | Account desctiption 34 | 35 | creator_info (``str``, *optional*): 36 | Author information. don't know what kind 37 | 38 | avatar (:obj:`~characterai.types.other.Avatar`): 39 | Avatar info 40 | """ 41 | characters: List[CharShort] 42 | username: str 43 | name: str 44 | num_following: int 45 | num_followers: int 46 | avatar_file_name: str | None 47 | subscription_type: str 48 | bio: str | None 49 | creator_info: str | None -------------------------------------------------------------------------------- /docs/qna.rst: -------------------------------------------------------------------------------- 1 | #################### 2 | Answers to Questions 3 | #################### 4 | 5 | What's ``author_id``? 6 | ===================== 7 | 8 | This is your account ID, needed to create a new chat in chat2. It can be found in :obj:`~characterai.aiocai.methods.account.Account.Account.get_me()`. 9 | 10 | What's ``tgt``? 11 | =============== 12 | 13 | This is an old character ID type, needed for chat1. You can get it in :obj:`~characterai.types.character.Character.Character.get_char` under ``identifier``. 14 | 15 | What's the difference between chat1 and chat2? 16 | ============================================== 17 | 18 | For the average user there are no significant differences except that you can't send pictures in chat2 (actually you can, the library allows it, but not on the site) 19 | 20 | But on the server side there are huge differences. First of all, chat1 works via HTTPS requests, while chat2 works via WebSockets. Secondly, the requests and their responses are very different, and accordingly their logic is very different 21 | 22 | chat2 is currently the newest version of chat2, hardly worth waiting for chat3 23 | 24 | .. note:: 25 | 26 | You can migrate chat1 to chat2 with :obj:`~characterai.aiocai.methods.chats.Chats.migrate` 27 | 28 | What's the difference between AioCAI and PyCAI? 29 | =============================================== 30 | 31 | AioCAI is the asynchronous version, PyCAI is the synchronous version 32 | 33 | Both versions support the same types and methods, just in PyCAI you don't need to use ``asyncio`` and write ``await`` and ``async`` -------------------------------------------------------------------------------- /characterai/pycai/client.py: -------------------------------------------------------------------------------- 1 | from .methods import Methods 2 | from .methods.chat1 import ChatV1 3 | from .methods.chat2 import WSConnect 4 | from .methods.utils import Request 5 | 6 | from curl_cffi.requests import Session 7 | 8 | class pycai(Methods, Request): 9 | chat1 = ChatV1() 10 | connect = WSConnect(start=False) 11 | 12 | class Client(Methods, Request): 13 | """CharacterAI client 14 | 15 | Args: 16 | token (``str``): 17 | Account auth token 18 | 19 | identifier (``str``): 20 | Which browser version to impersonate in the session 21 | 22 | **kwargs (``Any``): 23 | Supports all arguments from curl_cffi `Session `_ 24 | 25 | """ 26 | def __init__( 27 | self, token: str = None, 28 | identifier: str ='chrome120', 29 | **kwargs 30 | ): 31 | self.token = token 32 | self.session = Session( 33 | impersonate=identifier, 34 | headers={ 35 | 'Authorization': f'Token {token}' 36 | }, 37 | **kwargs 38 | ) 39 | 40 | self.chat1 = ChatV1(self.session, token) 41 | self.connect = WSConnect(token, start=False) 42 | 43 | def __enter__(self): 44 | return self 45 | 46 | def __exit__(self, *args): 47 | self.session.close() 48 | 49 | def close(self): 50 | """If you won't be using the client in the future, please close it""" 51 | self.session.close() -------------------------------------------------------------------------------- /characterai/pycai/methods/recent.py: -------------------------------------------------------------------------------- 1 | from ...types import character, recent 2 | from .utils import caimethod, validate 3 | 4 | class Recent: 5 | @caimethod 6 | def get_recent_chats(self, *, token: str = None): 7 | """Recent characters chatted with 8 | 9 | EXAMPLE:: 10 | 11 | await client.get_recent_chats() 12 | 13 | Returns: 14 | List of :obj:`~characterai.types.character.CharShort` 15 | """ 16 | data = self.request( 17 | 'chat/characters/recent/', 18 | token=token 19 | ) 20 | 21 | return validate( 22 | character.CharShort, 23 | data['characters'] 24 | ) 25 | 26 | @caimethod 27 | def get_recent_rooms(self, *, token: str = None): 28 | """Recent rooms 29 | 30 | EXAMPLE:: 31 | 32 | await client.get_recent_rooms() 33 | 34 | Returns: 35 | List of :obj:`~characterai.types.recent.Room` 36 | """ 37 | data = self.request( 38 | 'chat/rooms/recent/', 39 | token=token 40 | ) 41 | 42 | return validate( 43 | recent.Room, data['rooms'] 44 | ) 45 | 46 | @caimethod 47 | def get_recent(self, *, token: str = None): 48 | """Recent chats 49 | 50 | EXAMPLE:: 51 | 52 | await client.get_recent() 53 | 54 | Returns: 55 | List of :obj:`~characterai.types.recent.Chat` 56 | """ 57 | data = self.request( 58 | 'chats/recent/', neo=True, 59 | token=token 60 | ) 61 | 62 | return validate( 63 | recent.Chat, data['chats'] 64 | ) -------------------------------------------------------------------------------- /characterai/aiocai/methods/recent.py: -------------------------------------------------------------------------------- 1 | from ...types import character, recent 2 | from .utils import caimethod, validate 3 | 4 | class Recent: 5 | @caimethod 6 | async def get_recent_chats(self, *, token: str = None): 7 | """Recent characters chatted with 8 | 9 | EXAMPLE:: 10 | 11 | await client.get_recent_chats() 12 | 13 | Returns: 14 | List of :obj:`~characterai.types.character.CharShort` 15 | """ 16 | data = await self.request( 17 | 'chat/characters/recent/', 18 | token=token 19 | ) 20 | 21 | return validate( 22 | character.CharShort, 23 | data['characters'] 24 | ) 25 | 26 | @caimethod 27 | async def get_recent_rooms(self, *, token: str = None): 28 | """Recent rooms 29 | 30 | EXAMPLE:: 31 | 32 | await client.get_recent_rooms() 33 | 34 | Returns: 35 | List of :obj:`~characterai.types.recent.Room` 36 | """ 37 | data = await self.request( 38 | 'chat/rooms/recent/', 39 | token=token 40 | ) 41 | 42 | return validate( 43 | recent.Room, data['rooms'] 44 | ) 45 | 46 | @caimethod 47 | async def get_recent(self, *, token: str = None): 48 | """Recent chats 49 | 50 | EXAMPLE:: 51 | 52 | await client.get_recent() 53 | 54 | Returns: 55 | List of :obj:`~characterai.types.recent.Chat` 56 | """ 57 | data = await self.request( 58 | 'chats/recent/', neo=True, 59 | token=token 60 | ) 61 | 62 | return validate( 63 | recent.Chat, data['chats'] 64 | ) -------------------------------------------------------------------------------- /characterai/pycai/methods/chats.py: -------------------------------------------------------------------------------- 1 | from .utils import caimethod, validate 2 | from ...types import other 3 | 4 | class Chats: 5 | @caimethod 6 | def search( 7 | self, query: str, *, token: str = None 8 | ): 9 | """Search for characters by query 10 | 11 | EXAMPLE:: 12 | 13 | await client.search('QUERY') 14 | 15 | Args: 16 | query (``str``): 17 | Query text 18 | 19 | Returns: 20 | List of :obj:`~characterai.types.other.QueryChar` 21 | """ 22 | data = self.request( 23 | f'chat/characters/search/?query={query}' 24 | ) 25 | 26 | return validate( 27 | other.QueryChar, data['characters'] 28 | ) 29 | 30 | @caimethod 31 | def create_room( 32 | self, name: str, chars: list, 33 | topic: str = '', token: str = None 34 | ) -> str: 35 | """Creating a room with a characters 36 | 37 | EXAMPLE:: 38 | 39 | chars = [ 40 | { 41 | 'value': 'CHAR_ID' 42 | 'label': 'CHAR_NAME', 43 | } 44 | ] 45 | await client.create_room('NAME', chars) 46 | 47 | Args: 48 | name (``str``): 49 | Room name 50 | 51 | chars (``list``): 52 | The characters in the room 53 | 54 | topic (``str``): 55 | Room theme description 56 | 57 | Returns: 58 | Room ID (``str``) 59 | """ 60 | data = self.request( 61 | 'chat/room/create/', 62 | token=token, data={ 63 | 'characters': chars, 64 | 'name': name, 65 | 'topic': topic, 66 | 'visibility': 'PRIVATE' 67 | } 68 | ) 69 | 70 | return data['room']['external_id'] -------------------------------------------------------------------------------- /characterai/aiocai/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | from .methods import Methods 5 | from .methods.chat1 import ChatV1 6 | from .methods.chat2 import WSConnect 7 | from .methods.utils import Request 8 | 9 | from curl_cffi.requests import AsyncSession 10 | 11 | if sys.platform == 'win32': 12 | asyncio.set_event_loop_policy( 13 | asyncio.WindowsSelectorEventLoopPolicy() 14 | ) 15 | 16 | class aiocai(Methods, Request): 17 | chat1 = ChatV1() 18 | connect = WSConnect(start=False) 19 | 20 | class Client(Methods, Request): 21 | """CharacterAI client 22 | 23 | Args: 24 | token (``str``): 25 | Account auth token 26 | 27 | identifier (``str``): 28 | Which browser version to impersonate in the session 29 | 30 | **kwargs (``Any``): 31 | Supports all arguments from curl_cffi `Session `_ 32 | 33 | """ 34 | def __init__( 35 | self, token: str = None, 36 | identifier: str ='chrome120', 37 | **kwargs 38 | ): 39 | self.token = token 40 | self.session = AsyncSession( 41 | impersonate=identifier, 42 | headers={ 43 | 'Authorization': f'Token {token}' 44 | }, 45 | **kwargs 46 | ) 47 | 48 | self.chat1 = ChatV1(self.session, token) 49 | self.connect = WSConnect(token, start=False) 50 | 51 | async def __aenter__(self): 52 | return self 53 | 54 | async def __aexit__(self, *args): 55 | await self.session.close() 56 | 57 | async def close(self): 58 | """If you won't be using the client in the future, please close it""" 59 | await self.session.close() -------------------------------------------------------------------------------- /characterai/aiocai/methods/chats.py: -------------------------------------------------------------------------------- 1 | from .utils import caimethod, validate 2 | from ...types import other 3 | 4 | class Chats: 5 | @caimethod 6 | async def search( 7 | self, query: str, *, token: str = None 8 | ): 9 | """Search for characters by query 10 | 11 | EXAMPLE:: 12 | 13 | await client.search('QUERY') 14 | 15 | Args: 16 | query (``str``): 17 | Query text 18 | 19 | Returns: 20 | List of :obj:`~characterai.types.other.QueryChar` 21 | """ 22 | data = await self.request( 23 | f'chat/characters/search/?query={query}' 24 | ) 25 | 26 | return validate( 27 | other.QueryChar, data['characters'] 28 | ) 29 | 30 | @caimethod 31 | async def create_room( 32 | self, name: str, chars: list, 33 | topic: str = '', token: str = None 34 | ) -> str: 35 | """Creating a room with a characters 36 | 37 | EXAMPLE:: 38 | 39 | chars = [ 40 | { 41 | 'value': 'CHAR_ID' 42 | 'label': 'CHAR_NAME', 43 | } 44 | ] 45 | await client.create_room('NAME', chars) 46 | 47 | Args: 48 | name (``str``): 49 | Room name 50 | 51 | chars (``list``): 52 | The characters in the room 53 | 54 | topic (``str``): 55 | Room theme description 56 | 57 | Returns: 58 | Room ID (``str``) 59 | """ 60 | data = await self.request( 61 | 'chat/room/create/', 62 | token=token, data={ 63 | 'characters': chars, 64 | 'name': name, 65 | 'topic': topic, 66 | 'visibility': 'PRIVATE' 67 | } 68 | ) 69 | 70 | return data['room']['external_id'] -------------------------------------------------------------------------------- /docs/types/index.rst: -------------------------------------------------------------------------------- 1 | Types 2 | ===== 3 | 4 | All available server response types. Created with Pydantic, so they should be used as classes 5 | 6 | .. code-block:: 7 | 8 | from characterai import aiocai 9 | 10 | user = await aiocai.get_me('TOKEN') 11 | 12 | print(user.username) 13 | 14 | .. warning: 15 | You may have errors due to incorrect typing. Please report it on Github or Discord 16 | 17 | Account 18 | ------- 19 | 20 | .. currentmodule:: characterai.types.account 21 | 22 | .. autosummary:: 23 | :toctree: view 24 | :nosignatures: 25 | 26 | Profile 27 | Persona 28 | PersonaShort 29 | 30 | Character 31 | --------- 32 | 33 | .. currentmodule:: characterai.types.character 34 | 35 | .. autosummary:: 36 | :toctree: view 37 | :nosignatures: 38 | 39 | Character 40 | CharShort 41 | Categories 42 | 43 | Chat V1 44 | ------- 45 | 46 | .. currentmodule:: characterai.types.chat1 47 | 48 | .. autosummary:: 49 | :toctree: view 50 | :nosignatures: 51 | 52 | Message 53 | UserAccount 54 | User 55 | Participants 56 | Messages 57 | NewChat 58 | ChatHistory 59 | HisMessage 60 | HisMessages 61 | History 62 | Migrate 63 | 64 | Chat V2 65 | ------- 66 | 67 | .. currentmodule:: characterai.types.chat2 68 | 69 | .. autosummary:: 70 | :toctree: view 71 | :nosignatures: 72 | 73 | Candidate 74 | BotAnswer 75 | TurnData 76 | ChatData 77 | History 78 | 79 | 80 | Recent 81 | ------ 82 | 83 | .. currentmodule:: characterai.types.recent 84 | 85 | .. autosummary:: 86 | :toctree: view 87 | :nosignatures: 88 | 89 | Room 90 | Chat 91 | 92 | User 93 | ---- 94 | 95 | .. currentmodule:: characterai.types.user 96 | 97 | .. autosummary:: 98 | :toctree: view 99 | :nosignatures: 100 | 101 | User 102 | 103 | 104 | Other 105 | ----- 106 | 107 | .. currentmodule:: characterai.types.other 108 | 109 | .. autosummary:: 110 | :toctree: view 111 | :nosignatures: 112 | 113 | QueryChar 114 | Image 115 | Avatar 116 | Voice -------------------------------------------------------------------------------- /docs/auth.rst: -------------------------------------------------------------------------------- 1 | ############# 2 | Authorization 3 | ############# 4 | 5 | To work with the library, you need a token. You can get it through authorization by email and as a guest 6 | 7 | .. warning:: 8 | 9 | Please do not show your token to anyone. It can only be regenerated if you delete your account and create a new one 10 | 11 | Log In Via Email 12 | ================ 13 | 14 | This code asks the user for their email address and sends them a confirmation link. The user then enters the link they received in their email and is given a token for their account 15 | 16 | .. code-block:: 17 | 18 | from characterai import aiocai, sendCode, authUser 19 | import asyncio 20 | 21 | async def main(): 22 | email = input('YOUR EMAIL: ') 23 | 24 | code = sendCode(email) 25 | 26 | link = input('CODE IN MAIL: ') 27 | 28 | token = authUser(link, email) 29 | 30 | print(f'YOUR TOKEN: {token}') 31 | 32 | asyncio.run(main()) 33 | 34 | Log In As a Guest 35 | ================= 36 | 37 | Guests can't chat 38 | 39 | .. code-block:: 40 | 41 | from characterai import aiocai, authGuest 42 | import asyncio 43 | 44 | async def main(): 45 | client = aiocai.Client(authGuest()) 46 | 47 | info = await client.get_me(token=token) 48 | 49 | print(info) 50 | 51 | asyncio.run(main()) 52 | 53 | After logging in, you can find account information by using the :obj:`~characterai.aiogram.methods.account.Account.get_me` method. If you are a guest, no information will be displayed 54 | 55 | Alternative Auth Methods 56 | ======================== 57 | 58 | In addition to creating the ``Client`` class, you have the flexibility to work with both the class and individual functions. 59 | 60 | Context Manager 61 | --------------- 62 | 63 | When you create a ``Client`` class, its session will always remain active. If you don't use the library in your code later, you will need to manually close the session using the ``close()`` method 64 | 65 | And with this option, you won't need to manually close the session, as it will automatically close when the code finishes executing or an error occurs 66 | 67 | .. code-block:: 68 | 69 | async with aiocai.Client('TOKEN') as client: 70 | print(await client.get_me()) 71 | 72 | Via Function 73 | ------------ 74 | 75 | Sometimes you only need to use 1 function from the library or another token, in that case, you can use the function without creating a new client and just specify the token 76 | 77 | .. code-block:: 78 | 79 | await aiocai.get_me(token='TOKEN') 80 | -------------------------------------------------------------------------------- /docs/methods/index.rst: -------------------------------------------------------------------------------- 1 | Methods 2 | ======= 3 | 4 | All available API methods. All methods listed here are bound to the ``Client`` instance, except chat1, it is called through the class 5 | 6 | .. code-block:: 7 | 8 | from characterai import pycai 9 | 10 | client = pycai.Client('TOKEN') 11 | 12 | client.get_me() 13 | 14 | client.chat1.get_history('CHAR_ID') 15 | 16 | 17 | Character 18 | --------- 19 | 20 | .. currentmodule:: characterai.aiocai.methods.characters.Characters 21 | 22 | .. autosummary:: 23 | :toctree: view 24 | :nosignatures: 25 | 26 | get_char 27 | upvoted 28 | get_category 29 | get_recommended 30 | get_trending 31 | create_char 32 | update_char 33 | 34 | 35 | Account 36 | ------- 37 | 38 | .. currentmodule:: characterai.aiocai.methods.account.Account 39 | 40 | .. autosummary:: 41 | :toctree: view 42 | :nosignatures: 43 | 44 | get_me 45 | edit_account 46 | get_personas 47 | create_persona 48 | get_persona 49 | delete_persona 50 | followers 51 | following 52 | characters 53 | 54 | 55 | Chat V1 56 | ------- 57 | 58 | .. currentmodule:: characterai.aiocai.methods.chat1.ChatV1 59 | 60 | .. autosummary:: 61 | :toctree: view 62 | :nosignatures: 63 | 64 | get_histories 65 | get_history 66 | get_chat 67 | new_chat 68 | next_message 69 | delete_message 70 | send_message 71 | migrate 72 | 73 | 74 | Chat V2 75 | ------- 76 | 77 | .. currentmodule:: characterai.aiocai.methods.chat2.ChatV2 78 | 79 | .. autosummary:: 80 | :toctree: view 81 | :nosignatures: 82 | 83 | get_histories 84 | get_history 85 | get_chat 86 | new_chat 87 | next_message 88 | delete_message 89 | send_message 90 | edit_message 91 | pin 92 | 93 | 94 | Chats 95 | ----- 96 | 97 | .. currentmodule:: characterai.aiocai.methods.chats.Chats 98 | 99 | .. autosummary:: 100 | :toctree: view 101 | :nosignatures: 102 | 103 | search 104 | create_room 105 | 106 | Recent 107 | ------ 108 | 109 | .. currentmodule:: characterai.aiocai.methods.recent.Recent 110 | 111 | .. autosummary:: 112 | :toctree: view 113 | :nosignatures: 114 | 115 | get_recent_chats 116 | get_recent_rooms 117 | get_recent 118 | 119 | Users 120 | ----- 121 | 122 | .. currentmodule:: characterai.aiocai.methods.users.Users 123 | 124 | .. autosummary:: 125 | :toctree: view 126 | :nosignatures: 127 | 128 | get_user 129 | 130 | Other 131 | ----- 132 | 133 | .. currentmodule:: characterai.aiocai.methods.other.Other 134 | 135 | .. autosummary:: 136 | :toctree: view 137 | :nosignatures: 138 | 139 | get_voices 140 | create_image 141 | upload_image 142 | ping -------------------------------------------------------------------------------- /characterai/pycai/methods/other.py: -------------------------------------------------------------------------------- 1 | from .utils import caimethod, validate 2 | from ...types import other 3 | 4 | from curl_cffi import CurlMime 5 | 6 | class Other: 7 | @caimethod 8 | def create_image( 9 | self, promt: str, *, token: str = None 10 | ): 11 | """Image generation by description 12 | 13 | EXAMPLE:: 14 | 15 | await client.create_image('PROMT') 16 | 17 | Args: 18 | promt (``str``): 19 | Image description 20 | 21 | Returns: 22 | :obj:`~characterai.types.other.Image` 23 | """ 24 | data = self.request( 25 | 'chat/generate-image/', 26 | token=token, data={ 27 | 'image_description': promt 28 | } 29 | ) 30 | 31 | return other.Image( 32 | url=data['image_rel_path'], 33 | type='CREATED' 34 | ) 35 | 36 | @caimethod 37 | def upload_image( 38 | self, file: str, *, token: str = None 39 | ): 40 | """Uploading an image to the server 41 | 42 | EXAMPLE:: 43 | 44 | await client.upload_image('FILENAME.PNG') 45 | 46 | Args: 47 | file (``str``): 48 | File path 49 | 50 | Returns: 51 | :obj:`~characterai.types.other.Image` 52 | """ 53 | mp = CurlMime() 54 | mp.addpart( 55 | name='image', 56 | content_type='image/png', 57 | filename=file, 58 | local_path=file, 59 | ) 60 | 61 | data = self.request( 62 | 'chat/upload-image/', 63 | data={}, multipart=mp, 64 | token=token 65 | ) 66 | 67 | return other.Image( 68 | url=data['value'] 69 | ) 70 | 71 | @caimethod 72 | def ping( 73 | self, *, token: str = None 74 | ) -> bool: 75 | """Performance check 76 | 77 | EXAMPLE:: 78 | 79 | await client.ping() 80 | 81 | Returns: 82 | ``bool`` 83 | """ 84 | data = self.request( 85 | 'ping/', neo=True, 86 | token=token 87 | ) 88 | 89 | if data['status'] == 'pong': 90 | return True 91 | 92 | return False 93 | 94 | @caimethod 95 | def get_voices( 96 | self, *, token: str = None 97 | ) -> list: 98 | """List of available ready-made voices 99 | 100 | EXAMPLE:: 101 | 102 | await client.get_voices() 103 | 104 | Returns: 105 | List of :obj:`~characterai.types.other.Voice` 106 | """ 107 | data = self.request( 108 | 'chat/character/voices/', 109 | token=token 110 | ) 111 | 112 | return validate( 113 | other.Voice, data['voices'] 114 | ) -------------------------------------------------------------------------------- /docs/_static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | --color-sidebar-item-background--hover: var(--color-background-hover); 3 | } 4 | 5 | .sidebar-drawer { 6 | background: #131316; 7 | } 8 | 9 | .main { 10 | background: #18181B; 11 | } 12 | 13 | .content-icon-container { 14 | display: none; 15 | } 16 | 17 | .toc-drawer { 18 | background: #18181B; 19 | } 20 | 21 | .sidebar-search-container { 22 | background: #131316; 23 | } 24 | 25 | h2 { 26 | font-size: 1.5em; 27 | } 28 | 29 | .sidebar-tree .caption, .sidebar-tree :not(.caption)>.caption-text { 30 | font-size: 0.9em; 31 | font-weight: 800; 32 | } 33 | 34 | blockquote { 35 | display: none; 36 | } 37 | 38 | table.docutils { 39 | border-radius: 0.5em; 40 | border: 1px solid var(--color-table-border); 41 | width: 100%; 42 | } 43 | 44 | article table.align-default { 45 | overflow: hidden; 46 | } 47 | 48 | a { 49 | text-decoration: none; 50 | text-decoration-color: transparent; 51 | } 52 | 53 | .social-icon { 54 | padding-left: 8px; 55 | } 56 | 57 | .sidebar-brand-text { 58 | display: none; 59 | } 60 | 61 | .sidebar-search { 62 | border: none; 63 | border-radius: 10px; 64 | border: none; 65 | background: #202024; 66 | margin: 0px 8px; 67 | } 68 | 69 | .sidebar-search-container::before { 70 | z-index: 11; 71 | mask-image: url('search.svg'); 72 | } 73 | 74 | .sidebar-scroll, .toc-scroll, article[role="main"] * { 75 | scrollbar-width: none; 76 | } 77 | 78 | .sidebar-brand { 79 | margin-right: 0px 8px; 80 | } 81 | 82 | .sidebar-tree .current > .reference { 83 | border-radius: 20px 0px 0px 20px; 84 | } 85 | 86 | .sidebar-tree .reference:hover { 87 | color: var(--color-sidebar-link-text--top-level); 88 | border-radius: 20px 0px 0px 20px; 89 | } 90 | 91 | .tab-content { 92 | box-shadow: none; 93 | } 94 | 95 | .tab-set > label { 96 | padding: 1em 1em 0.5em; 97 | } 98 | 99 | .tab-label { 100 | background: #202020; 101 | border-radius: 10px 10px 0px 0px; 102 | } 103 | 104 | .theme-toggle { 105 | display: none; 106 | } 107 | 108 | .tab-set > input:checked + label { 109 | -webkit-tap-highlight-color: transparent; 110 | color: var(--color-content-foreground); 111 | background: #202020; 112 | } 113 | 114 | .tab-set > input:checked + label:hover { 115 | color: var(--color-content-foreground); 116 | background: #202020; 117 | } 118 | 119 | .tab-set > label { 120 | padding: 0.5em 1em; 121 | margin-left: 0px; 122 | border-bottom: none; 123 | padding: none; 124 | margin-left: none; 125 | background: none; 126 | } 127 | 128 | .tab-content > [class^="highlight-"]:first-child .highlight { 129 | border-top-right-radius: 10px; 130 | } 131 | 132 | .highlight { 133 | border-radius: 10px; 134 | } 135 | 136 | .mobile-header.scrolled { 137 | border-bottom: 1; 138 | box-shadow: none; 139 | } -------------------------------------------------------------------------------- /characterai/aiocai/methods/other.py: -------------------------------------------------------------------------------- 1 | from .utils import caimethod, validate 2 | from ...types import other 3 | 4 | from curl_cffi import CurlMime 5 | 6 | class Other: 7 | @caimethod 8 | async def create_image( 9 | self, promt: str, *, token: str = None 10 | ): 11 | """Image generation by description 12 | 13 | EXAMPLE:: 14 | 15 | await client.create_image('PROMT') 16 | 17 | Args: 18 | promt (``str``): 19 | Image description 20 | 21 | Returns: 22 | :obj:`~characterai.types.other.Image` 23 | """ 24 | data = await self.request( 25 | 'chat/generate-image/', 26 | token=token, data={ 27 | 'image_description': promt 28 | } 29 | ) 30 | 31 | return other.Image( 32 | url=data['image_rel_path'], 33 | type='CREATED' 34 | ) 35 | 36 | @caimethod 37 | async def upload_image( 38 | self, file: str, *, token: str = None 39 | ): 40 | """Uploading an image to the server 41 | 42 | EXAMPLE:: 43 | 44 | await client.upload_image('FILENAME.PNG') 45 | 46 | Args: 47 | file (``str``): 48 | File path 49 | 50 | Returns: 51 | :obj:`~characterai.types.other.Image` 52 | """ 53 | mp = CurlMime() 54 | mp.addpart( 55 | name='image', 56 | content_type='image/png', 57 | filename=file, 58 | local_path=file, 59 | ) 60 | 61 | data = await self.request( 62 | 'chat/upload-image/', 63 | data={}, multipart=mp, 64 | token=token 65 | ) 66 | 67 | return other.Image( 68 | url=data['value'] 69 | ) 70 | 71 | @caimethod 72 | async def ping( 73 | self, *, token: str = None 74 | ) -> bool: 75 | """Performance check 76 | 77 | EXAMPLE:: 78 | 79 | await client.ping() 80 | 81 | Returns: 82 | ``bool`` 83 | """ 84 | data = await self.request( 85 | 'ping/', neo=True, 86 | token=token 87 | ) 88 | 89 | if data['status'] == 'pong': 90 | return True 91 | 92 | return False 93 | 94 | @caimethod 95 | async def get_voices( 96 | self, *, token: str = None 97 | ) -> list: 98 | """List of available ready-made voices 99 | 100 | EXAMPLE:: 101 | 102 | await client.get_voices() 103 | 104 | Returns: 105 | List of :obj:`~characterai.types.other.Voice` 106 | """ 107 | data = await self.request( 108 | 'chat/character/voices/', 109 | token=token 110 | ) 111 | 112 | return validate( 113 | other.Voice, data['voices'] 114 | ) -------------------------------------------------------------------------------- /characterai/types/recent.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Any 2 | from pydantic import BaseModel, Field 3 | from datetime import datetime 4 | 5 | from .other import Avatar 6 | 7 | class Participant(BaseModel, Avatar): 8 | name: str 9 | avatar_file_name: str 10 | 11 | class Room(BaseModel): 12 | """Информация о комнатах с персонажами 13 | 14 | Parameters: 15 | external_id (``str``): 16 | Chat ID 17 | 18 | title (``str``): 19 | Room name 20 | 21 | description (``str``): 22 | Room description 23 | 24 | participants (List of ``Participant``): 25 | Characters in the room [name, avatar_file_name] 26 | 27 | img_gen_enabled (``bool``): 28 | Will it be possible to generate pictures 29 | """ 30 | external_id: str 31 | title: str 32 | description: str 33 | participants: List[Participant] 34 | img_gen_enabled: bool 35 | 36 | class Name(BaseModel): 37 | ko: Optional[str] = None 38 | ru: Optional[str] = None 39 | ja_JP: Optional[str] = None 40 | zh_CN: Optional[str] = None 41 | 42 | class CharacterTranslations(BaseModel): 43 | name: Name 44 | 45 | class Chat(BaseModel, Avatar): 46 | """Информация о чате 47 | 48 | Parameters: 49 | chat_id (``str``): 50 | Chat ID 51 | 52 | create_time (:py:obj:`~datetime`): 53 | Chat creation time 54 | 55 | creator_id (``str``): 56 | ID of the user who created the chat 57 | 58 | character_id (``str``): 59 | Character ID 60 | 61 | state (``str``): 62 | Chat state 63 | 64 | type (``str``): 65 | Chat type 66 | 67 | visibility (``str``): 68 | Chat visibility (everyone, you or from link) 69 | 70 | character_name (``str``): 71 | Character name 72 | 73 | character_visibility (``str``): 74 | Character visibility (everyone, you or from link) 75 | 76 | character_translations (``CharacterTranslations``): 77 | Translations of character information 78 | [ko, ru, ja_JP, zh_CN] 79 | 80 | default_voice_id (``str``, *optional*): 81 | Default voice ID 82 | 83 | avatar_file_name (``str``): 84 | Path to the avatar on the server 85 | 86 | avatar (:obj:`~characterai.types.other.Avatar`): 87 | Avatar info 88 | """ 89 | chat_id: str 90 | create_time: datetime 91 | creator_id: str 92 | character_id: str 93 | state: str 94 | type: str 95 | visibility: str 96 | character_name: str 97 | character_visibility: str 98 | character_translations: CharacterTranslations 99 | default_voice_id: Optional[str] = None 100 | avatar_file_name: str = Field( 101 | validation_alias='character_avatar_uri' 102 | ) -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | ###### 3 | AioCAI 4 | ###### 5 | 6 | 7 | .. image:: https://img.shields.io/pepy/dt/characterai?style=flat-square 8 | :target: https://pypi.org/project/characterai 9 | :alt: Total Downloads 10 | 11 | .. image:: https://img.shields.io/pypi/l/characterai?style=flat-square 12 | :target: https://opensource.org/licenses/MIT 13 | :alt: MIT License 14 | 15 | .. image:: https://img.shields.io/github/stars/kramcat/characterai?style=flat-square 16 | :target: https://github.com/kramcat/characterai 17 | :alt: Discord 18 | 19 | .. image:: https://img.shields.io/discord/1120066151515422772?style=flat-square 20 | :target: https://discord.com/invite/ZHJe3tXQkf 21 | :alt: Stars 22 | 23 | 24 | Welcome to the documentation for a synchronous/asynchronous unofficial library for CharacterAI using `curl_cffi `_ 25 | 26 | 27 | 💻 Installation 28 | --------------- 29 | 30 | .. code-block:: bash 31 | 32 | pip install -U characterai 33 | 34 | .. warning:: 35 | 36 | This version of the library is in alpha version, there may be bugs and errors. The library was developed without the participation of Character AI developers or their knowledge. To work with the library you need to know how to work with `asyncio `_ 37 | 38 | 39 | 🔥 Features 40 | =========== 41 | 42 | - Supports logging in via email or as a guest 43 | - Does not use web browsers like: Pypeeter, Playwright, etc. 44 | - Supports uploading/downloading pictures 45 | - Has detailed documentation 46 | - Uses Pydantic 47 | - Asynchronous 48 | 49 | 50 | 📙 Simple Example 51 | ----------------- 52 | .. literalinclude:: 53 | ../examples/async/chat2.py 54 | 55 | 56 | 👥 Community 57 | -------------- 58 | If you have any questions about our library or would like to discuss CharacterAI, LLM, or Neural Networks topics, please visit our Discord channel 59 | 60 | `discord.gg/ZHJe3tXQkf `_ 61 | 62 | 63 | 📝 TODO List 64 | ------------ 65 | 66 | - Character voice work 67 | - Community tab support 68 | - Add logging 69 | - Group chat support 70 | - Improved work with uploading pictures 71 | 72 | 73 | 💵 Support 74 | ---------- 75 | | TON - ``EQCSMftGsV4iU2b9H7tuEURIwpcWpF_maw4yknMkVxDPKs6v`` 76 | | BTC - ``bc1qghtyl43jd6xr66wwtrxkpe04sglqlwgcp04yl9`` 77 | | ETH - ``0x1489B0DDCE07C029040331e4c66F5aA94D7B4d4e`` 78 | | USDT (TRC20) - ``TJpvALv9YiL2khFBb7xfWrUDpvL5nYFs8u`` 79 | 80 | You can contact me via `Telegram `_ or `Discord `_ if you need help with parsing services or want to write a library. I can also create bots and userbots for Telegram 81 | 82 | 83 | .. toctree:: 84 | :hidden: 85 | :maxdepth: 3 86 | :caption: Getting Started 87 | 88 | starting 89 | auth 90 | 91 | .. toctree:: 92 | :hidden: 93 | :maxdepth: 3 94 | :caption: Working with API 95 | 96 | client 97 | methods/index 98 | types/index 99 | errors 100 | 101 | .. toctree:: 102 | :hidden: 103 | :maxdepth: 3 104 | :caption: More Info 105 | 106 | support 107 | qna 108 | changelog 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logo 2 | 3 | [![Downloads](https://img.shields.io/pepy/dt/characterai?style=flat-square)](https://pepy.tech/project/characterai) 4 | [![License](https://img.shields.io/pypi/l/characterai?style=flat-square)](https://opensource.org/licenses/MIT) 5 | [![Stars](https://img.shields.io/github/stars/kramcat/characterai?style=flat-square)](https://github.com/kramcat/characterai) 6 | [![Discord](https://img.shields.io/discord/1120066151515422772?style=flat-square)](https://discord.com/invite/ZHJe3tXQkf) 7 | 8 | Welcome to the documentation for a synchronous/asynchronous unofficial library for CharacterAI using [curl_cff](https://github.com/yifeikong/curl_cffi) 9 | 10 | ### 💻 Installation 11 | ```bash 12 | pip install git+https://github.com/kramcat/CharacterAI.git 13 | ``` 14 | 15 | ### ⚠️ Warning 16 | This version of the library is in alpha version, there may be bugs and errors. The library was developed without the participation of Character AI developers or their knowledge. To work with the library you need to know how to work with [asyncio](https://docs.python.org/3/library/asyncio.html) 17 | 18 | ### 🔥 Features 19 | - Supports logging in via email or as a guest 20 | - Does not use web browsers like: Pypeeter, Playwright, etc. 21 | - Supports uploading/downloading pictures 22 | - Has detailed documentation 23 | - Uses Pydantic 24 | - Asynchronous 25 | 26 | ### 📙 Simple Example 27 | You need an account to use the library. To find out your token, you must [log in through the library](https://docs.kram.cat/auth.html) 28 | ```python 29 | from characterai import aiocai 30 | import asyncio 31 | 32 | async def main(): 33 | char = input('CHAR ID: ') 34 | 35 | client = aiocai.Client('TOKEN') 36 | 37 | me = await client.get_me() 38 | 39 | async with await client.connect() as chat: 40 | new, answer = await chat.new_chat( 41 | char, me.id 42 | ) 43 | 44 | print(f'{answer.name}: {answer.text}') 45 | 46 | while True: 47 | text = input('YOU: ') 48 | 49 | message = await chat.send_message( 50 | char, new.chat_id, text 51 | ) 52 | 53 | print(f'{message.name}: {message.text}') 54 | 55 | asyncio.run(main()) 56 | ``` 57 | 58 | ### 📚 Documentation 59 | The documentation contains all the detailed information about functions and types. If you have any questions, first of all read whether there is an answer in the documentation 60 | 61 | **[docs.kram.cat](https://docs.kram.cat)** 62 | 63 | ### 👥 Community 64 | If you have any questions about our library or would like to discuss CharacterAI, LLM, or Neural Networks topics, please visit our Discord channel 65 | 66 | **[discord.gg/ZHJe3tXQkf](https://discord.com/invite/ZHJe3tXQkf)** 67 | 68 | ### 📝 TODO List 69 | - [ ] Character voice work 70 | - [ ] Community tab support 71 | - [ ] Add logging 72 | - [ ] Group chat support 73 | - [ ] Improved work with uploading pictures 74 | 75 | ### 💵 Support 76 | TON - `EQCSMftGsV4iU2b9H7tuEURIwpcWpF_maw4yknMkVxDPKs6v` 77 |
BTC - `bc1qghtyl43jd6xr66wwtrxkpe04sglqlwgcp04yl9` 78 |
ETH - `0x1489B0DDCE07C029040331e4c66F5aA94D7B4d4e` 79 |
USDT (TRC20) - `TJpvALv9YiL2khFBb7xfWrUDpvL5nYFs8u` 80 | 81 | You can contact me via [Telegram](https://t.me/kramcat) or [Discord](https://discordapp.com/users/480976972277874690) if you need help with parsing services or want to write a library. I can also create bots and userbots for Telegram 82 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | ######### 2 | Changelog 3 | ######### 4 | 5 | 1.0.0a1 6 | ======= 7 | 8 | - Adding authorization via mail and as a guest 9 | - Fix old bugs related to authorization by token and chat2 10 | - Added ability to download and upload pictures 11 | - Improved performance by switching to `curl_cffi` 12 | - Adding typing via Pydantic 13 | - Added new features: modify and pin character messages 14 | - Moved documentation to Sphinx Furo 15 | - PyAsyncCAI name changed to AioCAI 16 | 17 | 0.8.0 - Much faster, chat2, Fixes 18 | ================================= 19 | 20 | - There is no Playwright anymore, another library is used that does not load the system (!!!) 21 | - Added chat2 support (new chats with a different design) 22 | - Deleted ``client.upload_image()``, ``client.start()`` 23 | - chat2 is only available in the asynchronous version 24 | - Fixes and improvements 25 | 26 | 27 | 0.7.0 - Managing Posts and Characters, Upload Images, Create Rooms 28 | ================================================================== 29 | 30 | - Small fixes by KubaPro010 31 | - Private variables 32 | - ``start()`` for PyCAI 33 | - Added plus for ``start()`` 34 | - Added timeout for ``start()`` 35 | - Added ``ping()``, ``upload_image('PATH')``, ``user.update('USERNAME')``, ``character.create(...)``, ``character.update(...)``, ``character.voices()``, ``character.create_room(...)`` 36 | - Added the post class 37 | - ``post.get_post('POST_ID')`` 38 | - ``post.my_posts()`` 39 | - ``post.get_posts('USERNAME')`` 40 | - ``post.upvote('POST_ID')`` 41 | - ``post.undo_upvote('POST_ID')`` 42 | - ``post.send_comment('POST_ID', 'TEXT')`` 43 | - ``post.delete_comment('MESSAGE_ID', 'POST_ID')`` 44 | - ``post.create('HISTORY_ID', 'TITLE')`` 45 | - ``post.delete('POST_ID')`` 46 | - Some fixes for parameters, check documentation 47 | - Some optimization 48 | - Added kwargs for functions 49 | 50 | 51 | 0.6.0 - Documentation, New Functions, Fixes 52 | =========================================== 53 | 54 | - Name of pyCAI changed to PyCAI 55 | - Name of pyAsyncCAI changed to PyAsyncCAI 56 | - Written documentation 57 | - Fix error 500 58 | - Added more errors 59 | - Added ``chat.rate('CHAR', RATE)`` 60 | - Added ``chat.next_message('CHAR')`` 61 | - Added ``chat.get_chat('CHAR')`` 62 | - Added ``chat.delete_message('HISTORY_ID', 'UUIDS_TO_DELETE')`` 63 | - Fixed showing filtered messages 64 | - Code changed to PEP8 65 | 66 | 67 | 0.5.0 - POST methods, New chat.get_histories() 68 | =============================================== 69 | 70 | - Now some functions work via POST requests, it's faster and more reliable 71 | - Fixed ``chat.new_chat()`` 72 | - ``chat.send_message()`` was rewritten 73 | - Instead of an error, it returns an error page 74 | - This function can show a message even if it is filtered 75 | - Fixed bugs with JSON 76 | - New ``chat.get_histories('CHAR')`` 77 | - This function returns all the stories with the character 78 | - Find users by nickname with ``user.info('NICKNAME')`` 79 | - This function returns information about the user 80 | 81 | 82 | 0.4.0 - New chat.send_message(), Async, Fixes 83 | ============================================= 84 | 85 | - Add user.get_history in async 86 | - Fix ``chat.new_chat()`` 87 | - New ``chat.send_message()`` 88 | - New chat.get_history() 89 | - ``character.get_info()`` rename to ``character.info()`` and rewritten 90 | - Add character.search() 91 | - Add ``user.recent()`` 92 | - Fix small bugs 93 | 94 | 95 | 0.3.0 - Fixed Functions, New Parameters 96 | ======================================= 97 | 98 | - Fix broken functions 99 | - Remove ``user.get_history`` in async (I'll add in next version) 100 | - Add token parameter on all functions for custom token 101 | - Add wait parameter on all functions for waiting a responce when site is overloaded 102 | - Add headless parameter on pyCAI for browser (I don't know why, but suddenly you need) 103 | - Other changes (I don't remember which ones) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | sys.path.insert(0, os.path.abspath('../')) 3 | 4 | import characterai 5 | 6 | project = 'AioCAI' 7 | title = '1.0.0a1' 8 | copyright = ' 2023 – 2024, kramcat' 9 | author = 'kramcat' 10 | release = '1.0.0a1' 11 | language = 'en' 12 | 13 | extensions = [ 14 | 'sphinx.ext.autodoc', 15 | 'sphinx.ext.autosummary', 16 | 'sphinx.ext.napoleon', 17 | 'sphinx_copybutton', 18 | 'sphinx_inline_tabs' 19 | ] 20 | 21 | autosummary_generate = True 22 | 23 | html_logo = 'images/title.png' 24 | html_title = 'AioCAI 1.0.0a1' 25 | highligh_language = 'python3' 26 | html_theme = 'furo' 27 | html_show_sphinx = False 28 | 29 | templates_path = ['_templates'] 30 | html_static_path = ['_static'] 31 | html_css_files = ['style.css'] 32 | html_js_files = ['theme.js'] 33 | 34 | html_theme_options = { 35 | 'footer_icons': [ 36 | { 37 | 'name': 'GitHub', 38 | 'class': 'social-icon', 39 | 'url': 'https://github.com/kramcat/characterai', 40 | 'html': ''' 41 | 42 | 43 | 44 | ''', 45 | }, 46 | { 47 | 'name': 'Telegram', 48 | 'class': 'social-icon', 49 | 'url': 'https://t.me/kramdev', 50 | 'html': ''' 51 | 52 | 53 | 54 | ''', 55 | }, 56 | { 57 | 'name': 'Discord', 58 | 'class': 'social-icon', 59 | 'url': 'https://discord.gg/ZHJe3tXQkf', 60 | 'html': ''' 61 | 62 | 63 | 64 | ''', 65 | } 66 | ], 67 | } 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /characterai/auth.py: -------------------------------------------------------------------------------- 1 | from .errors import AuthError, ServerError 2 | 3 | from curl_cffi.requests import Session 4 | import uuid 5 | 6 | URL = 'https://beta.character.ai' 7 | 8 | def sendCode(email: str) -> bool: 9 | with Session(impersonate='chrome120') as s: 10 | r = s.post( 11 | 'https://identitytoolkit.googleapis.com' 12 | '/v1/accounts:sendOobCode?key=' 13 | 'AIzaSyAbLy_s6hJqVNr2ZN0UHHiCbJX1X8smTws', 14 | json={ 15 | 'requestType': 'EMAIL_SIGNIN', 16 | 'email': email, 17 | 'clientType': 'CLIENT_TYPE_WEB', 18 | 'continueUrl': 'https://beta.character.ai', 19 | 'canHandleCodeInApp': True 20 | } 21 | ) 22 | 23 | data = r.json() 24 | 25 | try: 26 | if data['email'] == email: 27 | return True 28 | except KeyError: 29 | raise ServerError(data['error']['message']) 30 | 31 | def authUser(link: str, email: str) -> str: 32 | with Session(impersonate='chrome120') as s: 33 | r = s.get(link, allow_redirects=True) 34 | 35 | oobCode = r.url.split('oobCode=')[1].split('&')[0] 36 | 37 | r = s.post( 38 | 'https://identitytoolkit.googleapis.com' 39 | '/v1/accounts:signInWithEmailLink?key=' 40 | 'AIzaSyAbLy_s6hJqVNr2ZN0UHHiCbJX1X8smTws', 41 | headers={ 42 | # Firebase key for GoogleAuth API 43 | 'X-Firebase-AppCheck': 'eyJraWQiOiJYcEhKU0EiLCJ' 44 | '0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIx' 45 | 'OjQ1ODc5NzcyMDY3NDp3ZWI6YjMzNGNhNDM2MWU5MzRkYWV' 46 | 'iOWQzYiIsImF1ZCI6WyJwcm9qZWN0c1wvNDU4Nzk3NzIwNjc' 47 | '0IiwicHJvamVjdHNcL2NoYXJhY3Rlci1haSJdLCJwcm92aWR' 48 | 'lciI6InJlY2FwdGNoYV9lbnRlcnByaXNlIiwiaXNzIjoiaHR0' 49 | 'cHM6XC9cL2ZpcmViYXNlYXBwY2hlY2suZ29vZ2xlYXBpcy5jb' 50 | '21cLzQ1ODc5NzcyMDY3NCIsImV4cCI6MTcxMTAxNzE2MiwiaWF' 51 | '0IjoxNzEwNDEyMzYyLCJqdGkiOiJkSXlkWVFPZEhnaTRmc2ZGU' 52 | 'DMtWHNZVU0zZG01eFY4R05ncDItOWxCQ2xVIn0.o2g6-5Pl7rj' 53 | 'iKdQ4X9bdOe6tDSVmdODFZUljHDnF5cNCik6masItwpeL3Yh6h' 54 | '78sQKNwuKzCUBFjsvDsEIdu71gW4lAuDxhKxljffX9nRuh8d0j-' 55 | 'ofmwq_4abpA3LdY12gIibvMigf3ncBQiJzu4SVQUKEdO810oUG8' 56 | 'G4RWlQfBIo-PpCO8jhyGZ0sjcklibEObq_4-ynMZnhTuIN_J183' 57 | '-RibxiKMjMTVaCcb1XfPxXi-zFr2NFVhSM1oTWSYmhseQ219ppH' 58 | 'A_-cQQIH6MwC0haHDsAAntjQkjbnG2HhPQrigdbeiXfpMGHAxLR' 59 | 'XXsgaPuEkjYFUPoIfIITgvkj5iJ-33vji2NgmDCpCmpxpx5wTHOC' 60 | '8OEZqSoCyi3mOkJNXTxOHmxvS-5glMrcgoipVJ3Clr-pes3-aI5Y' 61 | 'w7n3kmd4YfsKTadYuE8vyosq_MplEQKolRKj67CSNTsdt2fOsLCW' 62 | 'Nohduup6qJrUroUpN35R9JuUWgSy7Y4MI6NM-bKJ' 63 | }, 64 | json={ 65 | 'email': email, 66 | 'oobCode': oobCode 67 | } 68 | ) 69 | 70 | data = r.json() 71 | 72 | try: 73 | idToken = data['idToken'] 74 | except KeyError: 75 | raise AuthError(data['error']['message']) 76 | 77 | 78 | with Session(impersonate='chrome120') as s: 79 | r = s.post( 80 | f'{URL}/dj-rest-auth/google_idp/', 81 | json={ 82 | 'id_token': idToken 83 | } 84 | ) 85 | 86 | data = r.json() 87 | 88 | try: 89 | return data['key'] 90 | except KeyError: 91 | raise AuthError(data['error']) 92 | 93 | def authGuest() -> str: 94 | with Session(impersonate='chrome120') as s: 95 | r = s.post( 96 | f'{URL}/chat/auth/lazy/', 97 | json={ 98 | 'lazy_uuid': str(uuid.uuid1()) 99 | } 100 | ) 101 | 102 | data = r.json() 103 | 104 | try: 105 | return data['token'] 106 | except KeyError: 107 | raise AuthError(data['error']) -------------------------------------------------------------------------------- /docs/starting.rst: -------------------------------------------------------------------------------- 1 | ########### 2 | Quick Start 3 | ########### 4 | 5 | To better understand how the library works, let's take a simple example of a conversation between a user and a character 6 | 7 | I recommend using AioCAI instead of PyCAI when working with asynchronous libraries 8 | 9 | First, you need to create a skeleton for your project. In PyCAI, this involves simply importing the library 10 | 11 | .. tab:: AioCAI 12 | 13 | .. code-block:: 14 | 15 | from characterai import aiocai 16 | import asyncio 17 | 18 | async def main(): 19 | # YOUR CODE 20 | 21 | asyncio.run(main()) 22 | 23 | .. tab:: PyCAI 24 | 25 | .. code-block:: 26 | 27 | from characterai import pycai 28 | 29 | # YOUR CODE 30 | 31 | At the beginning of the code, we will ask the user for the character ID 32 | 33 | .. code-block:: 34 | 35 | char = input('CHAR: ') 36 | 37 | And now you need to create a class :obj:`~characterai.aiocai.client.aiocai.Client`, which will be the main class you will be working with all the time 38 | 39 | The required argument is `token`, you can find out about the rest on the class page itself 40 | 41 | .. code-block:: 42 | 43 | client = aiocai.Client('TOKEN') 44 | 45 | 46 | After that, we collect information about our account through :obj:`~characterai.aiocai.methods.account.Account.get_me`, It's necessary to create a new chat that requires an author ID 47 | 48 | .. tab:: AioCAI 49 | 50 | .. code-block:: 51 | 52 | me = await client.get_me() 53 | 54 | .. tab:: PyCAI 55 | 56 | .. code-block:: 57 | 58 | me = client.get_me() 59 | 60 | And now, let's move on to the chat. Once upon a time, c.ai had only an old version of the chat that worked via HTTPS. The new version (chat2) works via WebSockets, which means you need to maintain a connection with the server. In Python, this is done using context managers. For more information, please see the following object: `~characterai.aiocai.methods.utils.WSConnect` 61 | 62 | .. tab:: AioCAI 63 | 64 | .. code-block:: 65 | 66 | async with await client.connect() as chat: 67 | # YOUR CODE 68 | 69 | .. tab:: PyCAI 70 | 71 | .. code-block:: 72 | 73 | with client.connect() as chat: 74 | # YOUR CODE 75 | 76 | Now, we will create a new chat and communicate directly within it. The function will always return 2 variables: information about the new chat and a welcoming message 77 | 78 | After that, we will immediately display the greeting message for the character. If you do not wish to have this message displayed in new chats, you can set ``greeting=False`` in the function, and it will only return ``new`` 79 | 80 | .. tab:: AioCAI 81 | 82 | .. code-block:: 83 | 84 | new, answer = await chat.new_chat( 85 | char, me.id 86 | ) 87 | 88 | .. tab:: PyCAI 89 | 90 | .. code-block:: 91 | 92 | new, answer = chat.new_chat( 93 | char, me.id 94 | ) 95 | 96 | And in the chat itself, which will run continuously, immediately show the input from a user's message 97 | 98 | .. code-block:: 99 | 100 | while True: 101 | text = input('YOU: ') 102 | 103 | # YOUR CODE 104 | 105 | After that, we receive a message and see the character's answer 106 | 107 | .. tab:: AioCAI 108 | 109 | .. code-block:: 110 | 111 | message = await chat.send_message( 112 | char, new.chat_id, text 113 | ) 114 | 115 | print(f'{message.name}: {message.text}') 116 | 117 | .. tab:: PyCAI 118 | 119 | .. code-block:: 120 | 121 | message = chat.send_message( 122 | char, new.chat_id, text 123 | ) 124 | 125 | print(f'{message.name}: {message.text}') 126 | 127 | This information is enough to give you a basic understanding of the library. You can also find examples in the `examples `_ folder on GitHub 128 | 129 | For more information, please refer to the documentation. Before using a function or asking a question, make sure to read the documentation and use the search -------------------------------------------------------------------------------- /characterai/pycai/methods/utils.py: -------------------------------------------------------------------------------- 1 | from characterai.pycai import client, methods 2 | from ...errors import ( 3 | ServerError, AuthError, 4 | JSONError 5 | ) 6 | 7 | from curl_cffi import CurlMime 8 | 9 | from functools import wraps 10 | import json 11 | 12 | class Request: 13 | def request( 14 | self, url: str, *, token: str = None, 15 | method: str = 'GET', data: dict = {}, 16 | split: bool = False, neo: bool = False, 17 | multipart: CurlMime = None 18 | ): 19 | key = self.token or token 20 | 21 | if key == None: 22 | raise AuthError('No token') 23 | 24 | headers = { 25 | "Authorization": f"Token {key}" 26 | } 27 | 28 | link = f'https://plus.character.ai/{url}' 29 | 30 | if neo: 31 | link = link.replace('plus', 'neo') 32 | 33 | if multipart != None: 34 | r = self.session.post( 35 | link, headers=headers, data=data, 36 | multipart=multipart 37 | ) 38 | elif data != {} or data: 39 | r = self.session.post( 40 | link, headers=headers, json=data 41 | ) 42 | elif method == 'GET': 43 | r = self.session.get( 44 | link, headers=headers 45 | ) 46 | elif method == 'PUT': 47 | r = self.session.put( 48 | link, headers=headers, json=data 49 | ) 50 | 51 | if neo and not r.ok: 52 | try: 53 | raise ServerError(r.json()['comment']) 54 | except KeyError: 55 | raise ServerError(r.text) 56 | 57 | if r == 404: 58 | raise ServerError('Not Found') 59 | elif not r.ok: 60 | raise ServerError(r.status_code) 61 | 62 | text = r.text 63 | 64 | if '}\n{' in text: 65 | text = '{' + text.split('}\n{')[-1] 66 | 67 | try: 68 | res = json.loads( 69 | text.encode('utf-8') 70 | ) 71 | except json.decoder.JSONDecodeError: 72 | raise JSONError( 73 | 'Unable to decode JSON.' 74 | f'Server response: {r.text}' 75 | ) 76 | 77 | try: 78 | if res['force_login']: 79 | raise AuthError('Need Auth') 80 | elif res['status'] != 'OK' or res['abort']: 81 | raise ServerError(res['error']) 82 | elif res['error'] != None: 83 | raise ServerError(res['error']) 84 | except KeyError: 85 | return res 86 | 87 | def checkSession(args) -> bool: 88 | return any( 89 | isinstance( 90 | a, client.pycai.Client 91 | ) for a in args 92 | ) 93 | 94 | def delClass(args) -> tuple: 95 | new = () 96 | 97 | for a in args: 98 | if not isinstance(a, ( 99 | methods.chat1.ChatV1 100 | )): 101 | new = (a, *new) 102 | 103 | return new[::-1] 104 | 105 | def caimethod(func): 106 | @wraps(func) 107 | def wrapper(*args, **kwargs): 108 | try: 109 | try: 110 | token = kwargs['token'] 111 | except (AttributeError, KeyError): 112 | token = args[0].token 113 | except AttributeError: 114 | token = None 115 | 116 | temp = False 117 | 118 | if not checkSession(args): 119 | temp = True 120 | 121 | args = (client.pycai.Client( 122 | token=token 123 | ), *args) 124 | 125 | args = delClass(args) 126 | 127 | result = func(*args, **kwargs) 128 | 129 | # Closing the session if the function 130 | # was used through a library class 131 | if temp: 132 | args[0].close() 133 | 134 | return result 135 | 136 | return wrapper 137 | 138 | def flatten(d, parent_key='', sep=''): 139 | items = [] 140 | for k, v in d.items(): 141 | if v == '': 142 | v = None 143 | 144 | if isinstance(v, dict): 145 | items.extend(flatten(v, k, sep=sep).items()) 146 | else: 147 | items.append((k, v)) 148 | return dict(items) 149 | 150 | def validate(_class, data): 151 | return [ 152 | _class(**a) for a in data 153 | ] 154 | -------------------------------------------------------------------------------- /characterai/aiocai/methods/utils.py: -------------------------------------------------------------------------------- 1 | from characterai.aiocai import client, methods 2 | from ...errors import ( 3 | ServerError, AuthError, 4 | JSONError 5 | ) 6 | 7 | from curl_cffi import CurlMime 8 | 9 | from functools import wraps 10 | import json 11 | 12 | class Request: 13 | async def request( 14 | self, url: str, *, token: str = None, 15 | method: str = 'GET', data: dict = {}, 16 | split: bool = False, neo: bool = False, 17 | multipart: CurlMime = None 18 | ): 19 | key = self.token or token 20 | 21 | if key == None: 22 | raise AuthError('No token') 23 | 24 | headers = { 25 | "Authorization": f"Token {key}" 26 | } 27 | 28 | link = f'https://plus.character.ai/{url}' 29 | 30 | if neo: 31 | link = link.replace('plus', 'neo') 32 | 33 | if multipart != None: 34 | r = await self.session.post( 35 | link, headers=headers, data=data, 36 | multipart=multipart 37 | ) 38 | elif data != {} or data: 39 | r = await self.session.post( 40 | link, headers=headers, json=data 41 | ) 42 | elif method == 'GET': 43 | r = await self.session.get( 44 | link, headers=headers 45 | ) 46 | elif method == 'PUT': 47 | r = await self.session.put( 48 | link, headers=headers, json=data 49 | ) 50 | 51 | if neo and not r.ok: 52 | try: 53 | raise ServerError(r.json()['comment']) 54 | except KeyError: 55 | raise ServerError(r.text) 56 | 57 | if r == 404: 58 | raise ServerError('Not Found') 59 | elif not r.ok: 60 | raise ServerError(r.status_code) 61 | 62 | text = r.text 63 | 64 | if '}\n{' in text: 65 | text = '{' + text.split('}\n{')[-1] 66 | 67 | try: 68 | res = json.loads( 69 | text.encode('utf-8') 70 | ) 71 | except json.decoder.JSONDecodeError: 72 | raise JSONError( 73 | 'Unable to decode JSON.' 74 | f'Server response: {r.text}' 75 | ) 76 | 77 | try: 78 | if res['force_login']: 79 | raise AuthError('Need Auth') 80 | elif res['status'] != 'OK' or res['abort']: 81 | raise ServerError(res['error']) 82 | elif res['error'] != None: 83 | raise ServerError(res['error']) 84 | except KeyError: 85 | return res 86 | 87 | async def close(self): 88 | return await self.session.close() 89 | 90 | def checkSession(args) -> bool: 91 | return any( 92 | isinstance( 93 | a, client.aiocai.Client 94 | ) for a in args 95 | ) 96 | 97 | def delClass(args) -> tuple: 98 | new = () 99 | 100 | for a in args: 101 | if not isinstance(a, ( 102 | methods.chat1.ChatV1 103 | )): 104 | new = (a, *new) 105 | 106 | return new[::-1] 107 | 108 | def caimethod(func): 109 | @wraps(func) 110 | async def wrapper(*args, **kwargs): 111 | try: 112 | try: 113 | token = kwargs['token'] 114 | except (AttributeError, KeyError): 115 | token = args[0].token 116 | except AttributeError: 117 | token = None 118 | 119 | temp = False 120 | 121 | if not checkSession(args): 122 | temp = True 123 | 124 | args = (client.aiocai.Client( 125 | token=token 126 | ), *args) 127 | 128 | args = delClass(args) 129 | 130 | result = await func(*args, **kwargs) 131 | 132 | # Closing the session if the function 133 | # was used through a library class 134 | if temp: 135 | await args[0].close() 136 | 137 | return result 138 | 139 | return wrapper 140 | 141 | def flatten(d, parent_key='', sep=''): 142 | items = [] 143 | for k, v in d.items(): 144 | if v == '': 145 | v = None 146 | 147 | if isinstance(v, dict): 148 | items.extend(flatten(v, k, sep=sep).items()) 149 | else: 150 | items.append((k, v)) 151 | return dict(items) 152 | 153 | def validate(_class, data): 154 | return [ 155 | _class(**a) for a in data 156 | ] 157 | -------------------------------------------------------------------------------- /characterai/types/other.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Optional 3 | 4 | from curl_cffi.requests import AsyncSession 5 | from pydantic import BaseModel 6 | 7 | class Avatar: 8 | """A class for working with avatars. 9 | 10 | It exists in all classes with ``avatar_file_name`` parameter. 11 | 12 | .. code-block:: python 13 | 14 | await data.avatar.download('FILE.PNG') 15 | 16 | print(data.avatar.url) 17 | 18 | Parameters: 19 | avatar (:obj:`~characterai.types.other.Image`, *property*): 20 | Avatar object 21 | 22 | url (``str``): 23 | Incomplete URL 24 | 25 | type (``str``): 26 | Avatar type 27 | 28 | .. autofunction:: characterai.types.other.Image.download 29 | """ 30 | @property 31 | def avatar(self): 32 | return Image( 33 | url=self.avatar_file_name, 34 | icon='avatars' 35 | ) 36 | 37 | class QueryChar(BaseModel): 38 | """Character in search 39 | 40 | Parameters: 41 | document_id (``str``): 42 | ID in search (?) 43 | 44 | external_id (``str``): 45 | Char ID 46 | 47 | title (``str``): 48 | Short character description 49 | 50 | greeting (``str``): 51 | Character greeting (first message) 52 | 53 | avatar_file_name (``str``): 54 | Path to the avatar on the server 55 | 56 | avatar (:obj:`~characterai.types.other.Avatar`): 57 | Avatar info 58 | 59 | visibility (``str``): 60 | Who can see the character (everyone, you or from link) 61 | 62 | participant__name (``str``): 63 | Character name 64 | 65 | participant__num_interactions (``float``): 66 | Number of interactions (chats) with the character 67 | 68 | user__username (``str``): 69 | Author name 70 | 71 | priority (``float``): 72 | Priority in the search 73 | 74 | upvotes (``int``, *optional*): 75 | Number of character likes 76 | """ 77 | document_id: str 78 | external_id: str 79 | title: str 80 | greeting: str 81 | avatar_file_name: str 82 | visibility: str 83 | participant__name: str 84 | participant__num_interactions: float 85 | user__username: str 86 | priority: float 87 | search_score: float 88 | upvotes: Optional[int] = None 89 | 90 | class Image(BaseModel): 91 | """Image from server 92 | 93 | This class has a ``download()`` function that can be used 94 | to download a picture. If you are using the async version 95 | of the library, this function will be async 96 | 97 | Parameters: 98 | url (``str``): 99 | Incomplete URL 100 | 101 | type (``str``): 102 | Image type 103 | 104 | .. autofunction:: characterai.types.other.Image.download 105 | """ 106 | url: str 107 | type: str = 'UPLOADED' 108 | icon: str = 'user' 109 | 110 | async def download( 111 | self, path: str = None, 112 | width: int = 400, type: str = 'user' 113 | ): 114 | """Download any picture 115 | 116 | EXAMPLE:: 117 | 118 | await data.avatar.download('FILE.PNG') 119 | 120 | Parameters: 121 | path (``str``, *optional*): 122 | The path to the file or the file name. 123 | If no path is specified, a file will be 124 | created with a name from the server 125 | 126 | width (``int``, *optional*): 127 | File resolution. Default is 400 128 | 129 | Returns: 130 | Downloaded file 131 | """ 132 | if type == 'CREATED': 133 | await asyncio.sleep(3) 134 | else: 135 | self.url = f'https://characterai.io/i/{width}/static/{self.icon}/{self.url}' 136 | 137 | async with AsyncSession() as s: 138 | data = await s.get( 139 | self.url 140 | ) 141 | 142 | with open( 143 | path or self.url.split('/')[-1], 'wb' 144 | ) as f: 145 | f.write(data.content) 146 | 147 | class Voice(BaseModel): 148 | """Voice for character messages 149 | 150 | Parameters: 151 | id (``int``): 152 | Vote number 153 | 154 | name (``str``): 155 | Voice name 156 | 157 | voice_id (``str``): 158 | Voice ID 159 | 160 | country_code (``str``): 161 | Language that supports voice. Other languages 162 | are not voiced/skipped in the text 163 | 164 | lang_code (``str``): 165 | Same as ``country_code``, but there may be difference 166 | """ 167 | id: int 168 | name: str 169 | voice_id: str 170 | country_code: str 171 | lang_code: str -------------------------------------------------------------------------------- /characterai/types/chat2.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from datetime import datetime 3 | from typing import List, Optional 4 | 5 | class Author(BaseModel): 6 | """Message author 7 | 8 | Parameters: 9 | author_id (``str``): 10 | Account ID 11 | 12 | name (``str``): 13 | User name 14 | 15 | is_human (``bool``, *option*): 16 | Is the author an account 17 | """ 18 | author_id: str 19 | name: str 20 | is_human: Optional[bool] = None 21 | 22 | class Editor(BaseModel): 23 | """Message editor 24 | 25 | Parameters: 26 | author_id (``str``): 27 | Account ID 28 | 29 | name (``str``, *optional*): 30 | User name 31 | """ 32 | author_id: str 33 | name: Optional[str] = None 34 | 35 | class TurnKey(BaseModel): 36 | chat_id: str 37 | turn_id: str 38 | 39 | class Candidate(BaseModel): 40 | """Контент сообщения 41 | 42 | Parameters: 43 | candidate_id (``str``): 44 | Message ID 45 | 46 | create_time (:py:obj:`~datetime.datetime`): 47 | Date of message creation 48 | 49 | raw_content (``str``): 50 | Message text 51 | 52 | editor (:obj:`~characterai.types.chat2.Editor`): 53 | Information about who modified the message 54 | 55 | is_final (``bool``): 56 | Is this the last chunk of the message 57 | 58 | base_candidate_id (``str``): 59 | Date of message update 60 | """ 61 | candidate_id: str 62 | create_time: datetime 63 | raw_content: str 64 | editor: Optional[Editor] = None 65 | is_final: bool 66 | base_candidate_id: Optional[str] = None 67 | 68 | class BotAnswer(BaseModel): 69 | """Message object 70 | 71 | Parameters: 72 | text (``str``): 73 | Message text 74 | 75 | id (``str``): 76 | Message ID 77 | 78 | name (``str``): 79 | Sender's name 80 | 81 | turn_key (``TurnKey``): 82 | Message and chat ID [chat_id, turn_id] 83 | 84 | create_time (:py:obj:`~datetime.datetime`): 85 | Date of message creation 86 | 87 | last_update_time (:py:obj:`~datetime.datetime`): 88 | Date of message update 89 | 90 | state (``str``): 91 | Message state 92 | 93 | author (:obj:`~characterai.types.chat2.Author`): 94 | Message author 95 | 96 | candidates (List of :obj:`~characterai.types.chat2.Candidate`): 97 | Message content 98 | 99 | primary_candidate_id (``str``): 100 | Message ID (?) 101 | """ 102 | @property 103 | def text(self): 104 | return self.candidates[0].raw_content 105 | 106 | @property 107 | def id(self): 108 | return self.candidates[0].candidate_id 109 | 110 | @property 111 | def name(self): 112 | return self.author.name 113 | 114 | turn_key: TurnKey 115 | create_time: datetime 116 | last_update_time: datetime 117 | state: str 118 | author: Author 119 | candidates: List[Candidate] 120 | primary_candidate_id: str 121 | 122 | class TurnData(BaseModel): 123 | """Preview message 124 | 125 | Parameters: 126 | turn_key (``str``): 127 | Message ID 128 | 129 | create_time (:py:obj:`~datetime.datetime`): 130 | Date of message creation 131 | 132 | last_update_time (:py:obj:`~datetime.datetime`): 133 | Date of message update 134 | 135 | state (``str``): 136 | Message state 137 | 138 | author (:obj:`~characterai.types.chat2.Author`): 139 | Message author 140 | 141 | candidates (List of :obj:`~characterai.types.chat2.Candidate`): 142 | Message content 143 | 144 | primary_candidate_id (``str``): 145 | Message ID (?) 146 | """ 147 | turn_key: TurnKey 148 | create_time: datetime 149 | last_update_time: datetime 150 | state: str 151 | author: Author 152 | candidates: List[Candidate] 153 | primary_candidate_id: str 154 | 155 | class ChatData(BaseModel): 156 | """Chat info 157 | 158 | Parameters: 159 | chat_id (``str``): 160 | Chat ID 161 | 162 | create_time (:py:obj:`~datetime.datetime`): 163 | Date of message creation 164 | 165 | creator_id (``str``): 166 | Author ID 167 | 168 | character_id (``str``): 169 | Character ID 170 | 171 | state (``str``): 172 | Chat state 173 | 174 | type (``str``): 175 | Chat type 176 | 177 | visibility (``str``): 178 | Who can see the chat room 179 | 180 | preview_turns (List of :obj:`~characterai.types.chat2.TurnData`, *optional*): 181 | Message's preview 182 | """ 183 | chat_id: str 184 | create_time: datetime 185 | creator_id: str 186 | character_id: str 187 | state: str 188 | type: str 189 | visibility: str 190 | preview_turns: Optional[List[TurnData]] = None 191 | 192 | class Meta(BaseModel): 193 | next_token: str 194 | 195 | class History(BaseModel): 196 | """Chat history 197 | 198 | turns (List of :obj:`~characterai.types.chat2.TurnData`): 199 | Message information 200 | 201 | meta (``Meta``): 202 | I don't know what it is, maybe someone could use it 203 | """ 204 | turns: List[TurnData] 205 | meta: Meta -------------------------------------------------------------------------------- /characterai/pycai/methods/chat1.py: -------------------------------------------------------------------------------- 1 | from .utils import Request, caimethod, validate 2 | from ...types import chat1 3 | 4 | class ChatV1(Request): 5 | def __init__(self, session = None, token = None): 6 | self.session = session 7 | self.token = token 8 | 9 | @caimethod 10 | def send_message( 11 | self, chat_id: str, tgt: str, text: str, 12 | token: str = None, **kwargs 13 | ): 14 | """Sending a message to chat 15 | 16 | EXAMPLE:: 17 | 18 | await client.chat1.send_message('CHAT_ID', 'TGT', 'TEXT') 19 | 20 | Args: 21 | chat_id (``str``): 22 | Chat or room ID 23 | 24 | tgt (``str``): 25 | Old character ID type 26 | 27 | text (``str``): 28 | Message text 29 | 30 | primary_msg_uuid (``str``, *optional*): 31 | Reply to the next generated message from 32 | :obj:`~characterai.aiocai.methods.chat1.ChatV1.next_message` 33 | 34 | Returns: 35 | :obj:`~characterai.types.chat1.Message` 36 | """ 37 | data = self.request( 38 | 'chat/streaming/', token=token, 39 | data={ 40 | 'history_external_id': chat_id, 41 | 'text': text, 42 | 'tgt': tgt, 43 | **kwargs 44 | } 45 | ) 46 | 47 | return chat1.Message.model_validate( 48 | data 49 | ) 50 | 51 | @caimethod 52 | def get_chat( 53 | self, char_id: str, chat_id: str, 54 | token: str = None 55 | ): 56 | """Get information about the chat 57 | 58 | EXAMPLE:: 59 | 60 | await client.chat1.get_chat('CHAR', 'CHAT_ID') 61 | 62 | Args: 63 | char_id (``str``): 64 | Character ID 65 | 66 | chat_id (``str``): 67 | Room or chat ID 68 | 69 | Returns: 70 | :obj:`~characterai.types.chat1.ChatHistory` 71 | """ 72 | data = self.request( 73 | 'chat/history/continue/', 74 | token=token, data={ 75 | 'character_external_id': char_id, 76 | 'history_external_id': chat_id 77 | } 78 | ) 79 | 80 | return chat1.ChatHistory.model_validate( 81 | data 82 | ) 83 | 84 | @caimethod 85 | def new_chat( 86 | self, char_id: str, *, token: str = None 87 | ): 88 | """Create a new chat 89 | 90 | EXAMPLE:: 91 | 92 | await client.chat1.new_chat('CHAR_ID') 93 | 94 | Args: 95 | char_id (``str``): 96 | Character ID 97 | 98 | Returns: 99 | :obj:`~characterai.types.chat1.NewChat` 100 | """ 101 | data = self.request( 102 | 'chat/history/create/', 103 | token=token, data={ 104 | 'character_external_id': char_id 105 | } 106 | ) 107 | 108 | return chat1.NewChat.model_validate( 109 | data 110 | ) 111 | 112 | @caimethod 113 | def next_message( 114 | self, chat_id: str, tgt: str, 115 | parent_msg_uuid: str, *, 116 | token: str = None, **kwargs 117 | ): 118 | """Generate an alternative answer 119 | 120 | EXAMPLE:: 121 | 122 | msg = await client.chat1.send_message( 123 | 'CHAT_ID', 'TGT', 'TEXT' 124 | ) 125 | 126 | await client.chat1.next_message( 127 | 'CHAT_ID', 'TGT', msg.last_user_msg_uuid 128 | ) 129 | 130 | Args: 131 | chat_id (``str``): 132 | Chat ID 133 | 134 | tgt (``str``): 135 | Old character ID type 136 | 137 | parent_msg_uuid (``str``): 138 | ID of the message from which you 139 | want to get an alternative reply 140 | 141 | Returns: 142 | :obj:`~characterai.types.chat1.Message` 143 | """ 144 | return chat1.Message.model_validate( 145 | self.request( 146 | 'chat/streaming/', 147 | token=token, data={ 148 | 'history_external_id': chat_id, 149 | 'parent_msg_uuid': parent_msg_uuid, 150 | 'tgt': tgt, 151 | **kwargs 152 | } 153 | ) 154 | ) 155 | 156 | @caimethod 157 | def get_histories( 158 | self, char: str, *, num: int = 999, 159 | token: str = None 160 | ): 161 | """Get a list of your character's chat histories 162 | 163 | EXAMPLE:: 164 | 165 | await client.chat1.get_histories('CHAR_ID') 166 | 167 | Args: 168 | char (``str``): 169 | Character ID 170 | 171 | num (``str``): 172 | Maximum number of chats 173 | 174 | Returns: 175 | :obj:`~characterai.types.chat1.History` 176 | """ 177 | data = self.request( 178 | 'chat/character/histories_v2/', 179 | token=token, data={ 180 | 'external_id': char, 181 | 'number': num 182 | } 183 | ) 184 | 185 | return validate( 186 | chat1.History, 187 | data['histories'] 188 | ) 189 | 190 | @caimethod 191 | def get_history( 192 | self, chat_id: str, 193 | *, token: str = None 194 | ): 195 | """Get chat history 196 | 197 | EXAMPLE:: 198 | 199 | await client.chat1.get_history('CHAT_ID') 200 | 201 | Args: 202 | chat_id (``str``): 203 | Chat ID 204 | 205 | Returns: 206 | :obj:`~characterai.types.chat1.History` 207 | """ 208 | data = self.request( 209 | 'chat/history/msgs/user/?' 210 | 'history_external_id=' 211 | f'{chat_id}', token=token 212 | ) 213 | 214 | return chat1.HisMessages.model_validate( 215 | data 216 | ) 217 | 218 | @caimethod 219 | def delete_message( 220 | self, chat_id: str, uuids: list, 221 | *, token: str = None 222 | ) -> bool: 223 | """Deleting messages. Returns ``True`` on success. 224 | 225 | EXAMPLE:: 226 | 227 | await client.chat1.delete_message('CHAT_ID', ['UUID']) 228 | 229 | Args: 230 | chat_id (``str``): 231 | Chat ID 232 | 233 | uuids (List of ``str``): 234 | List of message IDs to be deleted 235 | 236 | Returns: 237 | ``bool`` 238 | """ 239 | self.request( 240 | 'chat/history/msgs/delete/', 241 | token=token, data={ 242 | 'history_id': chat_id, 243 | 'uuids_to_delete': uuids 244 | } 245 | ) 246 | 247 | return True 248 | 249 | @caimethod 250 | def migrate( 251 | self, chat_id: str, *, 252 | token: str = None 253 | ): 254 | """Migrate chat1 to chat2 255 | 256 | EXAMPLE:: 257 | 258 | await client.chat1.migrate('CHAT_ID') 259 | 260 | Args: 261 | chat_id (``str``): 262 | Chat1 ID 263 | 264 | Returns: 265 | :obj:`~characterai.types.chat1.Migrate` 266 | """ 267 | self.request( 268 | f'migration/{chat_id}', 269 | token=token, data=True, neo=True 270 | ) 271 | 272 | data = self.request( 273 | f'migration/{chat_id}', 274 | token=token, neo=True 275 | ) 276 | 277 | return chat1.Migrate.model_validate( 278 | data['migration'] 279 | ) -------------------------------------------------------------------------------- /characterai/pycai/methods/account.py: -------------------------------------------------------------------------------- 1 | from ...types import account, character 2 | from .utils import flatten, caimethod, validate 3 | 4 | import uuid 5 | 6 | class Account: 7 | @caimethod 8 | def get_me(self, *, token: str = None): 9 | """Information about your account 10 | 11 | EXAMPLE:: 12 | 13 | await client.get_me() 14 | 15 | Returns: 16 | :obj:`~characterai.types.account.Profile` 17 | """ 18 | data = self.request( 19 | 'chat/user/', token=token 20 | ) 21 | 22 | name = data['user']['user']['username'] 23 | 24 | if name == 'ANONYMOUS': 25 | return account.Anonymous() 26 | elif name.startswith('Guest'): 27 | return account.Guest.model_validate( 28 | flatten(data) 29 | ) 30 | 31 | return account.Profile.model_validate( 32 | flatten(data) 33 | ) 34 | 35 | @caimethod 36 | def edit_account( 37 | self, *, token: str = None, **kwargs 38 | ) -> bool: 39 | """Change your account information. Returns ``True`` on success. 40 | 41 | EXAMPLE:: 42 | 43 | await client.edit_account() 44 | 45 | Args: 46 | username (``str``, *optional*): 47 | New nickname 48 | 49 | name (``str``, *optional*): 50 | New name 51 | 52 | avatar_type (``str``, *optional*): 53 | The type of the new avatar 54 | 55 | avatar_rel_path (``str``, *optional*): 56 | A new avatar. You need to provide a link to it. Files are uploaded 57 | via :obj:`~characterai.aiocai.methods.other.Other.upload_image` 58 | 59 | bio (``str``, *optional*): 60 | New description 61 | 62 | Returns: 63 | ``bool`` 64 | """ 65 | user = self.request( 66 | 'chat/user/', token=token 67 | ) 68 | 69 | info = user['user'] 70 | avatar = info['user']['account'] 71 | 72 | settings = { 73 | 'username': info['user']['username'], 74 | 'name': info['name'], 75 | 'avatar_type': avatar['avatar_type'], 76 | 'avatar_rel_path': avatar['avatar_file_name'], 77 | 'bio': info['bio'] 78 | } 79 | 80 | for k in kwargs: 81 | settings[k] = kwargs[k] 82 | 83 | self.request( 84 | 'chat/user/update/', 85 | token=token, data=settings 86 | ) 87 | 88 | return True 89 | 90 | @caimethod 91 | def get_personas( 92 | self, *, token: str = None 93 | ) -> list: 94 | """Get all the personas in your account 95 | 96 | EXAMPLE:: 97 | 98 | await client.get_personas() 99 | 100 | Returns: 101 | List of :obj:`~characterai.types.account.PersonaShort` 102 | """ 103 | data = self.request( 104 | 'chat/personas/?force_refresh=1', 105 | token=token 106 | ) 107 | 108 | return validate( 109 | account.PersonaShort, 110 | data['personas'] 111 | ) 112 | 113 | @caimethod 114 | def create_persona( 115 | self, title: str, *, 116 | token: str = None, 117 | definition: str = '', 118 | custom_id: str = None 119 | ): 120 | """Create persona 121 | 122 | EXAMPLE:: 123 | 124 | await client.create_persona('TITLE') 125 | 126 | Args: 127 | title (``str``): 128 | Persona's name 129 | 130 | definition (``str``, *optional*): 131 | Persona's definition 132 | 133 | custom_id (``str``, *optional*): 134 | Your UUID for a persona. If you don't provide it, 135 | it will be generated automatically 136 | 137 | Returns: 138 | :obj:`~characterai.types.account.Persona` 139 | """ 140 | identifier = custom_id or f'id:{uuid.uuid1()}' 141 | 142 | data = self.request( 143 | 'chat/persona/create/', 144 | token=token, data={ 145 | 'title': title, 146 | 'name': title, 147 | 'identifier': identifier, 148 | 'categories': [], 149 | 'visibility': 'PUBLIC', 150 | 'copyable': False, 151 | 'description': 'This is my persona.', 152 | 'greeting': 'Hello! This is my persona', 153 | 'definition': definition, 154 | 'avatar_rel_path': '', 155 | 'img_gen_enabled': False, 156 | 'strip_img_prompt_from_msg': False, 157 | } 158 | ) 159 | 160 | return account.Persona.model_validate( 161 | data['persona'] 162 | ) 163 | 164 | @caimethod 165 | def get_persona( 166 | self, persona_id: str, *, token: str = None 167 | ): 168 | """Get information about a persona 169 | 170 | EXAMPLE:: 171 | 172 | await client.get_persona('ID') 173 | 174 | Args: 175 | persona_id (``str``): 176 | Persona's UUID 177 | 178 | Returns: 179 | :obj:`~characterai.types.account.Persona` 180 | """ 181 | data = self.request( 182 | f'chat/persona/?id={persona_id}', 183 | token=token 184 | ) 185 | 186 | return account.Persona.model_validate( 187 | data['persona'] 188 | ) 189 | 190 | @caimethod 191 | def delete_persona( 192 | self, persona_id: str, *, token: str = None 193 | ): 194 | """Delete persona 195 | 196 | EXAMPLE:: 197 | 198 | await client.delete_persona('ID') 199 | 200 | Args: 201 | persona_id (``str``): 202 | Persona's UUID 203 | 204 | Returns: 205 | :obj:`~characterai.types.account.Persona` 206 | """ 207 | persona = self.request( 208 | f'chat/persona/?id={persona_id}', 209 | token=token 210 | ) 211 | 212 | data = self.request( 213 | 'chat/persona/update/', 214 | token=token, data={ 215 | 'archived': True, 216 | **persona['persona'] 217 | } 218 | ) 219 | 220 | return account.Persona.model_validate( 221 | data['persona'] 222 | ) 223 | 224 | 225 | @caimethod 226 | def followers( 227 | self, *, token: str = None 228 | ) -> list: 229 | """Get your subscribers 230 | 231 | EXAMPLE:: 232 | 233 | await client.followers() 234 | 235 | Returns: 236 | List of ``str`` 237 | """ 238 | return (self.request( 239 | 'chat/user/followers/', 240 | token=token 241 | ))['followers'] 242 | 243 | @caimethod 244 | def following( 245 | self, *, token: str = None 246 | ) -> list: 247 | """Get those you subscribe to 248 | 249 | EXAMPLE:: 250 | 251 | await client.following() 252 | 253 | Returns: 254 | List of ``str`` 255 | """ 256 | return (self.request( 257 | 'chat/user/following/', 258 | token=token 259 | ))['following'] 260 | 261 | @caimethod 262 | def characters( 263 | self, *, token: str = None 264 | ): 265 | """Get your public characters 266 | 267 | EXAMPLE:: 268 | 269 | await client.characters() 270 | 271 | Returns: 272 | List of :obj:`~characterai.types.character.CharShort` 273 | """ 274 | data = self.request( 275 | 'chat/characters/?scope=user', 276 | token=token 277 | ) 278 | 279 | return validate( 280 | character.CharShort, 281 | data['characters'] 282 | ) 283 | -------------------------------------------------------------------------------- /characterai/aiocai/methods/chat1.py: -------------------------------------------------------------------------------- 1 | from .utils import Request, caimethod, validate 2 | from ...types import chat1 3 | 4 | class ChatV1(Request): 5 | def __init__(self, session = None, token = None): 6 | self.session = session 7 | self.token = token 8 | 9 | @caimethod 10 | async def send_message( 11 | self, chat_id: str, tgt: str, text: str, 12 | token: str = None, **kwargs 13 | ): 14 | """Sending a message to chat 15 | 16 | EXAMPLE:: 17 | 18 | await client.chat1.send_message('CHAT_ID', 'TGT', 'TEXT') 19 | 20 | Args: 21 | chat_id (``str``): 22 | Chat or room ID 23 | 24 | tgt (``str``): 25 | Old character ID type 26 | 27 | text (``str``): 28 | Message text 29 | 30 | primary_msg_uuid (``str``, *optional*): 31 | Reply to the next generated message from 32 | :obj:`~characterai.aiocai.methods.chat1.ChatV1.next_message` 33 | 34 | Returns: 35 | :obj:`~characterai.types.chat1.Message` 36 | """ 37 | data = await self.request( 38 | 'chat/streaming/', 39 | token=token, data={ 40 | 'history_external_id': chat_id, 41 | 'text': text, 42 | 'tgt': tgt, 43 | **kwargs 44 | } 45 | ) 46 | 47 | return chat1.Message.model_validate( 48 | data 49 | ) 50 | 51 | @caimethod 52 | async def get_chat( 53 | self, char_id: str, chat_id: str, 54 | token: str = None 55 | ): 56 | """Get information about the chat 57 | 58 | EXAMPLE:: 59 | 60 | await client.chat1.get_chat('CHAR', 'CHAT_ID') 61 | 62 | Args: 63 | char_id (``str``): 64 | Character ID 65 | 66 | chat_id (``str``): 67 | Room or chat ID 68 | 69 | Returns: 70 | :obj:`~characterai.types.chat1.ChatHistory` 71 | """ 72 | data = await self.request( 73 | 'chat/history/continue/', 74 | token=token, data={ 75 | 'character_external_id': char_id, 76 | 'history_external_id': chat_id 77 | } 78 | ) 79 | 80 | return chat1.ChatHistory.model_validate( 81 | data 82 | ) 83 | 84 | @caimethod 85 | async def new_chat( 86 | self, char_id: str, *, token: str = None 87 | ): 88 | """Create a new chat 89 | 90 | EXAMPLE:: 91 | 92 | await client.chat1.new_chat('CHAR_ID') 93 | 94 | Args: 95 | char_id (``str``): 96 | Character ID 97 | 98 | Returns: 99 | :obj:`~characterai.types.chat1.NewChat` 100 | """ 101 | data = await self.request( 102 | 'chat/history/create/', 103 | token=token, data={ 104 | 'character_external_id': char_id 105 | } 106 | ) 107 | 108 | return chat1.NewChat.model_validate( 109 | data 110 | ) 111 | 112 | @caimethod 113 | async def next_message( 114 | self, chat_id: str, tgt: str, 115 | parent_msg_uuid: str, *, 116 | token: str = None, **kwargs 117 | ): 118 | """Generate an alternative answer 119 | 120 | EXAMPLE:: 121 | 122 | msg = await client.chat1.send_message( 123 | 'CHAT_ID', 'TGT', 'TEXT' 124 | ) 125 | 126 | await client.chat1.next_message( 127 | 'CHAT_ID', 'TGT', msg.last_user_msg_uuid 128 | ) 129 | 130 | Args: 131 | chat_id (``str``): 132 | Chat ID 133 | 134 | tgt (``str``): 135 | Old character ID type 136 | 137 | parent_msg_uuid (``str``): 138 | ID of the message from which you 139 | want to get an alternative reply 140 | 141 | Returns: 142 | :obj:`~characterai.types.chat1.Message` 143 | """ 144 | return chat1.Message.model_validate( 145 | await self.request( 146 | 'chat/streaming/', 147 | token=token, data={ 148 | 'history_external_id': chat_id, 149 | 'parent_msg_uuid': parent_msg_uuid, 150 | 'tgt': tgt, 151 | **kwargs 152 | } 153 | ) 154 | ) 155 | 156 | @caimethod 157 | async def get_histories( 158 | self, char: str, *, num: int = 999, 159 | token: str = None 160 | ): 161 | """Get a list of your character's chat histories 162 | 163 | EXAMPLE:: 164 | 165 | await client.chat1.get_histories('CHAR_ID') 166 | 167 | Args: 168 | char (``str``): 169 | Character ID 170 | 171 | num (``str``): 172 | Maximum number of chats 173 | 174 | Returns: 175 | :obj:`~characterai.types.chat1.History` 176 | """ 177 | data = await self.request( 178 | 'chat/character/histories_v2/', 179 | token=token, data={ 180 | 'external_id': char, 181 | 'number': num 182 | } 183 | ) 184 | 185 | return validate( 186 | chat1.History, 187 | data['histories'] 188 | ) 189 | 190 | @caimethod 191 | async def get_history( 192 | self, chat_id: str, 193 | *, token: str = None 194 | ): 195 | """Get chat history 196 | 197 | EXAMPLE:: 198 | 199 | await client.chat1.get_history('CHAT_ID') 200 | 201 | Args: 202 | chat_id (``str``): 203 | Chat ID 204 | 205 | Returns: 206 | :obj:`~characterai.types.chat1.History` 207 | """ 208 | data = await self.request( 209 | 'chat/history/msgs/user/?' 210 | 'history_external_id=' 211 | f'{chat_id}', token=token 212 | ) 213 | 214 | return chat1.HisMessages.model_validate( 215 | data 216 | ) 217 | 218 | @caimethod 219 | async def delete_message( 220 | self, chat_id: str, uuids: list, 221 | *, token: str = None 222 | ) -> bool: 223 | """Deleting messages. Returns ``True`` on success. 224 | 225 | EXAMPLE:: 226 | 227 | await client.chat1.delete_message('CHAT_ID', ['UUID']) 228 | 229 | Args: 230 | chat_id (``str``): 231 | Chat ID 232 | 233 | uuids (List of ``str``): 234 | List of message IDs to be deleted 235 | 236 | Returns: 237 | ``bool`` 238 | """ 239 | await self.request( 240 | 'chat/history/msgs/delete/', 241 | token=token, data={ 242 | 'history_id': chat_id, 243 | 'uuids_to_delete': uuids 244 | } 245 | ) 246 | 247 | return True 248 | 249 | @caimethod 250 | async def migrate( 251 | self, chat_id: str, *, 252 | token: str = None 253 | ): 254 | """Migrate chat1 to chat2 255 | 256 | EXAMPLE:: 257 | 258 | await client.chat1.migrate('CHAT_ID') 259 | 260 | Args: 261 | chat_id (``str``): 262 | Chat1 ID 263 | 264 | Returns: 265 | :obj:`~characterai.types.chat1.Migrate` 266 | """ 267 | await self.request( 268 | f'migration/{chat_id}', 269 | token=token, data=True, neo=True 270 | ) 271 | 272 | data = await self.request( 273 | f'migration/{chat_id}', 274 | token=token, neo=True 275 | ) 276 | 277 | return chat1.Migrate.model_validate( 278 | data['migration'] 279 | ) -------------------------------------------------------------------------------- /characterai/types/account.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pydantic import BaseModel 3 | from typing import Any, List, Optional 4 | 5 | from .other import Avatar 6 | 7 | class Anonymous(BaseModel): 8 | username: str = 'ANONYMOUS' 9 | 10 | class Guest(BaseModel): 11 | username: str 12 | id: int 13 | account: Optional[Any] = None 14 | is_staff: bool = False 15 | subscription: Optional[int] = None 16 | is_human: bool = True 17 | name: str 18 | email: Optional[str] = None 19 | hidden_characters: list 20 | blocked_users: list 21 | 22 | class Profile(BaseModel, Avatar): 23 | """Your account info 24 | 25 | Parameters: 26 | name (``str``): 27 | Your name 28 | 29 | avatar_type (``str``): 30 | Avatar status, uploaded or not 31 | 32 | avatar_file_name (``str``, *optional*): 33 | Path to the avatar on the server 34 | 35 | avatar (:obj:`~characterai.types.other.Avatar`): 36 | Avatar info 37 | 38 | onboarding_complete (``bool``): 39 | For pop-up banners (?) 40 | 41 | mobile_onboarding_complete (``int``, *optional*): 42 | For mobile pop-up banners (?) 43 | 44 | bio (``str``, *optional*): 45 | Account description 46 | 47 | username (``str``): 48 | Public user nickname 49 | 50 | id (``str``): 51 | ID аккаунта, также ``author_id`` 52 | 53 | first_name (``str``, *optional*): 54 | Account email 55 | 56 | is_staff (``bool``): 57 | Is the account an employee of the service 58 | 59 | subscription (``int``, *optional*): 60 | Subscription type 61 | 62 | is_human (``bool``): 63 | Is the account a human 64 | 65 | email (``str``): 66 | Your email 67 | 68 | needs_to_acknowledge_policy (``str``): 69 | Should the user agreement be accepted 70 | 71 | suspended_until (:py:obj:`~datetime.datetime`, *optional*): 72 | Account lockout end date 73 | 74 | hidden_characters (List of ``str``): 75 | Hidden characters 76 | 77 | blocked_users (List of ``str``): 78 | Blocked users 79 | """ 80 | name: Optional[str] = None 81 | avatar_type: Optional[str] = None 82 | onboarding_complete: bool 83 | avatar_file_name: str | None 84 | mobile_onboarding_complete: int | None 85 | bio: str | None 86 | username: str 87 | id: int 88 | first_name: str | None 89 | is_staff: bool 90 | subscription: Optional[dict] = None 91 | is_human: bool 92 | email: str 93 | needs_to_acknowledge_policy: bool 94 | suspended_until: datetime | None 95 | hidden_characters: List[str] 96 | blocked_users: List[str] 97 | 98 | class Persona(BaseModel, Avatar): 99 | """Информация о вашей персоне 100 | 101 | Parameters: 102 | external_id (``str``): 103 | Persona ID 104 | 105 | title (``str``): 106 | Persona title 107 | 108 | name (``str``): 109 | Persona name 110 | 111 | visibiility (``str``): 112 | Persona visibility 113 | 114 | copyable (``bool``): 115 | Can other users copy a persona 116 | 117 | greeting (``str``): 118 | Persona greeting (?) 119 | 120 | description (``str``): 121 | Persona description 122 | 123 | identifier (``str``): 124 | Persona ID 125 | 126 | avatar_file_name (``str``): 127 | Path to the avatar on the server 128 | 129 | avatar (:obj:`~characterai.types.other.Avatar`): 130 | Avatar info 131 | 132 | songs (``list``): 133 | Songs list (?) 134 | 135 | img_gen_enabled (``bool``): 136 | Can a persona generate pictures (?) 137 | 138 | base_img_prompt (``str``): 139 | Basic prompt for generating pictures (?) 140 | 141 | img_prompt_regex (``str``): 142 | Regex for a picture prompt (?) 143 | 144 | strip_img_prompt_from_msg (``str``): 145 | Remove the prompt from the message (?) 146 | 147 | definition (``str``): 148 | Persona definition 149 | 150 | default_voice_id (``str``): 151 | Default voice ID (?) 152 | 153 | starter_prompts (``Any``): 154 | Prompts for a start (?) 155 | 156 | comments_enabled (``bool``): 157 | Can comment on the persona (?) 158 | 159 | categories (``list``): 160 | Persona categories (?) 161 | 162 | user__username (``str``): 163 | User name 164 | 165 | participant__name (``str``): 166 | Person's name, same as ``name`` 167 | 168 | participant__user__username (``str``): 169 | Persona ID 170 | 171 | num_interactions (``str``): 172 | Number of interactions with the person (?) 173 | 174 | voice_id (``str``): 175 | Voice ID (?) 176 | """ 177 | external_id: str 178 | title: str 179 | name: str 180 | visibility: str 181 | copyable: bool 182 | greeting: str 183 | description: str 184 | identifier: str 185 | avatar_file_name: str 186 | songs: list 187 | img_gen_enabled: bool 188 | base_img_prompt: str 189 | img_prompt_regex: str 190 | strip_img_prompt_from_msg: bool 191 | definition: str 192 | default_voice_id: str 193 | starter_prompts: Any 194 | comments_enabled: bool 195 | categories: list 196 | user__username: str 197 | participant__name: str 198 | participant__user__username: str 199 | num_interactions: int 200 | voice_id: str 201 | 202 | class PersonaShort(BaseModel, Avatar): 203 | """Короткая информация о вашей персоне 204 | 205 | Parameters: 206 | external_id (``str``): 207 | Persona ID 208 | 209 | title (``str``, *optional*): 210 | Persona title 211 | 212 | greeting (``str``): 213 | Persona greeting (?) 214 | 215 | description (``str``, *optional*): 216 | Persona description 217 | 218 | definition (``str``): 219 | Persona definition 220 | 221 | avatar_file_name (``str``): 222 | Path to the avatar on the server 223 | 224 | avatar (:obj:`~characterai.types.other.Avatar`): 225 | Avatar info 226 | 227 | visibiility (``str``): 228 | Persona visibility 229 | 230 | copyable (``bool``): 231 | Can other users copy a persona 232 | 233 | participant__name (``str``): 234 | Person's name, same as ``name`` 235 | 236 | participant__num_interactions (``str``): 237 | Number of interactions with the person (?) 238 | 239 | user__id (``int``): 240 | User ID, and also ``author_id`` 241 | 242 | user__username (``str``): 243 | User name 244 | 245 | img_gen_enabled (``bool``): 246 | Can a persona generate pictures (?) 247 | 248 | default_voice_id (``str``, *optional*): 249 | Default voice ID (?) 250 | 251 | is_persona (``bool``): 252 | Is the object a person 253 | """ 254 | external_id: str 255 | title: str | None 256 | greeting: str 257 | description: str | None 258 | definition: str 259 | avatar_file_name: str | None 260 | visibility: str 261 | copyable: bool 262 | participant__name: str 263 | participant__num_interactions: int 264 | user__id: int 265 | user__username: str 266 | img_gen_enabled: bool 267 | default_voice_id: str | None 268 | is_persona: bool 269 | -------------------------------------------------------------------------------- /characterai/aiocai/methods/account.py: -------------------------------------------------------------------------------- 1 | from ...types import account, character 2 | from .utils import flatten, caimethod, validate 3 | 4 | import uuid 5 | 6 | class Account: 7 | @caimethod 8 | async def get_me(self, *, token: str = None): 9 | """Information about your account 10 | 11 | EXAMPLE:: 12 | 13 | await client.get_me() 14 | 15 | Returns: 16 | :obj:`~characterai.types.account.Profile` 17 | """ 18 | data = await self.request( 19 | 'chat/user/', token=token 20 | ) 21 | 22 | name = data['user']['user']['username'] 23 | 24 | if name == 'ANONYMOUS': 25 | return account.Anonymous() 26 | elif name.startswith('Guest'): 27 | return account.Guest.model_validate( 28 | flatten(data) 29 | ) 30 | 31 | return account.Profile.model_validate( 32 | flatten(data) 33 | ) 34 | 35 | @caimethod 36 | async def edit_account( 37 | self, *, token: str = None, **kwargs 38 | ) -> bool: 39 | """Change your account information. Returns ``True`` on success. 40 | 41 | EXAMPLE:: 42 | 43 | await client.edit_account() 44 | 45 | Args: 46 | username (``str``, *optional*): 47 | New nickname 48 | 49 | name (``str``, *optional*): 50 | New name 51 | 52 | avatar_type (``str``, *optional*): 53 | The type of the new avatar 54 | 55 | avatar_rel_path (``str``, *optional*): 56 | A new avatar. You need to provide a link to it. Files are uploaded 57 | via :obj:`~characterai.aiocai.methods.other.Other.upload_image` 58 | 59 | bio (``str``, *optional*): 60 | New description 61 | 62 | Returns: 63 | ``bool`` 64 | """ 65 | user = await self.request( 66 | 'chat/user/', token=token 67 | ) 68 | 69 | info = user['user'] 70 | avatar = info['user']['account'] 71 | 72 | settings = { 73 | 'username': info['user']['username'], 74 | 'name': info['name'], 75 | 'avatar_type': avatar['avatar_type'], 76 | 'avatar_rel_path': avatar['avatar_file_name'], 77 | 'bio': info['bio'] 78 | } 79 | 80 | for k in kwargs: 81 | settings[k] = kwargs[k] 82 | 83 | await self.request( 84 | 'chat/user/update/', 85 | token=token, data=settings 86 | ) 87 | 88 | return True 89 | 90 | @caimethod 91 | async def get_personas( 92 | self, *, token: str = None 93 | ) -> list: 94 | """Get all the personas in your account 95 | 96 | EXAMPLE:: 97 | 98 | await client.get_personas() 99 | 100 | Returns: 101 | List of :obj:`~characterai.types.account.PersonaShort` 102 | """ 103 | data = await self.request( 104 | 'chat/personas/?force_refresh=1', 105 | token=token 106 | ) 107 | 108 | return validate( 109 | account.PersonaShort, 110 | data['personas'] 111 | ) 112 | 113 | @caimethod 114 | async def create_persona( 115 | self, title: str, *, 116 | token: str = None, 117 | definition: str = '', 118 | custom_id: str = None 119 | ): 120 | """Create persona 121 | 122 | EXAMPLE:: 123 | 124 | await client.create_persona('TITLE') 125 | 126 | Args: 127 | title (``str``): 128 | Persona's name 129 | 130 | definition (``str``, *optional*): 131 | Persona's definition 132 | 133 | custom_id (``str``, *optional*): 134 | Your UUID for a persona. If you don't provide it, 135 | it will be generated automatically 136 | 137 | Returns: 138 | :obj:`~characterai.types.account.Persona` 139 | """ 140 | identifier = custom_id or f'id:{uuid.uuid1()}' 141 | 142 | data = await self.request( 143 | 'chat/persona/create/', 144 | token=token, data={ 145 | 'title': title, 146 | 'name': title, 147 | 'identifier': identifier, 148 | 'categories': [], 149 | 'visibility': 'PUBLIC', 150 | 'copyable': False, 151 | 'description': 'This is my persona.', 152 | 'greeting': 'Hello! This is my persona', 153 | 'definition': definition, 154 | 'avatar_rel_path': '', 155 | 'img_gen_enabled': False, 156 | 'strip_img_prompt_from_msg': False, 157 | } 158 | ) 159 | 160 | return account.Persona.model_validate( 161 | data['persona'] 162 | ) 163 | 164 | @caimethod 165 | async def get_persona( 166 | self, persona_id: str, *, token: str = None 167 | ): 168 | """Get information about a persona 169 | 170 | EXAMPLE:: 171 | 172 | await client.get_persona('ID') 173 | 174 | Args: 175 | persona_id (``str``): 176 | Persona's UUID 177 | 178 | Returns: 179 | :obj:`~characterai.types.account.Persona` 180 | """ 181 | data = await self.request( 182 | f'chat/persona/?id={persona_id}', 183 | token=token 184 | ) 185 | 186 | return account.Persona.model_validate( 187 | data['persona'] 188 | ) 189 | 190 | @caimethod 191 | async def delete_persona( 192 | self, persona_id: str, *, token: str = None 193 | ): 194 | """Delete persona 195 | 196 | EXAMPLE:: 197 | 198 | await client.delete_persona('ID') 199 | 200 | Args: 201 | persona_id (``str``): 202 | Persona's UUID 203 | 204 | Returns: 205 | :obj:`~characterai.types.account.Persona` 206 | """ 207 | persona = await self.request( 208 | f'chat/persona/?id={persona_id}', 209 | token=token 210 | ) 211 | 212 | data = await self.request( 213 | 'chat/persona/update/', 214 | token=token, data={ 215 | 'archived': True, 216 | **persona['persona'] 217 | } 218 | ) 219 | 220 | return account.Persona.model_validate( 221 | data['persona'] 222 | ) 223 | 224 | 225 | @caimethod 226 | async def followers( 227 | self, *, token: str = None 228 | ) -> list: 229 | """Get your subscribers 230 | 231 | EXAMPLE:: 232 | 233 | await client.followers() 234 | 235 | Returns: 236 | List of ``str`` 237 | """ 238 | return (await self.request( 239 | 'chat/user/followers/', 240 | token=token 241 | ))['followers'] 242 | 243 | @caimethod 244 | async def following( 245 | self, *, token: str = None 246 | ) -> list: 247 | """Get those you subscribe to 248 | 249 | EXAMPLE:: 250 | 251 | await client.following() 252 | 253 | Returns: 254 | List of ``str`` 255 | """ 256 | return (await self.request( 257 | 'chat/user/following/', 258 | token=token 259 | ))['following'] 260 | 261 | @caimethod 262 | async def characters( 263 | self, *, token: str = None 264 | ): 265 | """Get your public characters 266 | 267 | EXAMPLE:: 268 | 269 | await client.characters() 270 | 271 | Returns: 272 | List of :obj:`~characterai.types.character.CharShort` 273 | """ 274 | data = await self.request( 275 | 'chat/characters/?scope=user', 276 | token=token 277 | ) 278 | 279 | return validate( 280 | character.CharShort, 281 | data['characters'] 282 | ) 283 | -------------------------------------------------------------------------------- /characterai/types/character.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import Optional, List 3 | from datetime import datetime 4 | 5 | from .other import Avatar 6 | 7 | class Promts(BaseModel): 8 | phrases: list 9 | 10 | class Character(BaseModel, Avatar): 11 | """Character info 12 | 13 | Parameters: 14 | external_id (``str``): 15 | Character ID 16 | 17 | title (``str``, *optional*): 18 | Short character description 19 | 20 | name (``str``): 21 | Character name 22 | 23 | visibility (``str``): 24 | Character visibility (everyone, you or from link) 25 | 26 | copyable (``bool``): 27 | Can other users copy a character 28 | 29 | greeting (``str``): 30 | Character greeting (first message) 31 | 32 | description (``str``, *optional*): 33 | Character description 34 | 35 | identifier (``str``): 36 | Character ID 37 | 38 | avatar_file_name (``str``): 39 | Path to the avatar on the server 40 | 41 | avatar (:obj:`~characterai.types.other.Avatar`): 42 | Avatar info 43 | 44 | songs (``list``): 45 | List of songs (?) 46 | 47 | img_gen_enabled (``bool``): 48 | Can the character generate pictures 49 | 50 | base_img_prompt (``str``, *optional*): 51 | Basic prompt for generating pictures 52 | 53 | img_prompt_regex (``str``, *optional*): 54 | Regex for a picture prompt 55 | 56 | strip_img_prompt_from_msg (``str``): 57 | Remove the prompt from the message 58 | 59 | definition (``str``): 60 | Character definition 61 | 62 | default_voice_id (``str``): 63 | Default voice ID 64 | 65 | starter_prompts (``Promts``, *optional*): 66 | Prompts for start (?) 67 | 68 | comments_enabled (``bool``): 69 | Can comment on the character (?) 70 | 71 | user__username (``str``): 72 | Author nickname 73 | 74 | participant__name (``str``): 75 | Character name, same as ``name`` 76 | 77 | participant__user__username (``str``): 78 | Character ID 79 | 80 | participant__num_interactions (``int``, *optional*): 81 | Number of interactions (chats) with the character 82 | 83 | voice_id (``str``, *optional*): 84 | Voice ID 85 | 86 | usage (``str``, *optional*): 87 | How well the character is used (?) 88 | 89 | upvotes (``int``, *optional*): 90 | Number of likes 91 | """ 92 | external_id: str 93 | title: str | None 94 | name: str 95 | visibility: str 96 | copyable: bool 97 | greeting: str 98 | description: str | None 99 | identifier: str 100 | avatar_file_name: str | None 101 | songs: list 102 | img_gen_enabled: bool 103 | base_img_prompt: str | None 104 | img_prompt_regex: str | None 105 | strip_img_prompt_from_msg: bool 106 | default_voice_id: str | None 107 | starter_prompts: Optional[Promts] = None 108 | comments_enabled: bool 109 | user__username: str 110 | participant__name: str 111 | participant__num_interactions: Optional[int] = None 112 | participant__user__username: str 113 | voice_id: str | None 114 | usage: Optional[str] = None 115 | upvotes: Optional[int] = None 116 | 117 | class CharShort(BaseModel, Avatar): 118 | """Краткая информация о персонаже 119 | 120 | Parameters: 121 | external_id (``str``): 122 | Character ID 123 | 124 | title (``str``, *optional*): 125 | Short character description 126 | 127 | name (``str``, *optional*): 128 | Character name 129 | 130 | greeting (``str``): 131 | Character greeting (first message) 132 | 133 | description (``str``, *optional*): 134 | Character description 135 | 136 | avatar_file_name (``str``): 137 | Path to the avatar on the server 138 | 139 | avatar (:obj:`~characterai.types.other.Avatar`): 140 | Avatar info 141 | 142 | visibility (``str``, *optional*): 143 | Character visibility (everyone, you or from link) 144 | 145 | copyable (``bool``, *optional*): 146 | Can other users copy a character 147 | 148 | participant__name (``str``): 149 | Character name, same as ``name`` 150 | 151 | user__id (``int``, *optional*): 152 | Author ID (?) 153 | 154 | user__username (``str``): 155 | Author nickname 156 | 157 | img_gen_enabled (``bool``): 158 | Can the character generate pictures 159 | 160 | participant__num_interactions (``int``, *optional*): 161 | Number of interactions (chats) with the character 162 | 163 | default_voice_id (``str``, *optional*): 164 | Default voice ID 165 | 166 | upvotes (``int``, *optional*): 167 | Number of likes 168 | 169 | max_last_interaction (:py:obj:`~datetime.datetime`, *optional*): 170 | Maximum time of the last interaction with the character (?) 171 | """ 172 | external_id: str 173 | title: str | None 174 | description: Optional[str | None] = None 175 | greeting: str 176 | avatar_file_name: str | None 177 | visibility: Optional[str] = None 178 | copyable: Optional[str | bool] = None 179 | participant__name: str 180 | user__id: Optional[int] = None 181 | user__username: str 182 | img_gen_enabled: bool 183 | participant__num_interactions: Optional[int] = None 184 | default_voice_id: Optional[str] = None 185 | upvotes: Optional[int] = None 186 | max_last_interaction: Optional[datetime] = None 187 | 188 | class Categories(BaseModel): 189 | """List of categories 190 | 191 | Parameters: 192 | animals (List of :obj:`~characterai.types.character.CharShort`): 193 | Animals bots 194 | 195 | anime (List of :obj:`~characterai.types.character.CharShort`): 196 | Anime bots 197 | 198 | anime_game (List of :obj:`~characterai.types.character.CharShort`): 199 | Anime game bots 200 | 201 | chinese (List of :obj:`~characterai.types.character.CharShort`): 202 | Chinese bots 203 | 204 | comedy (List of :obj:`~characterai.types.character.CharShort`): 205 | Comedy bots 206 | 207 | discussion (List of :obj:`~characterai.types.character.CharShort`): 208 | Discussion bots 209 | 210 | famous (List of :obj:`~characterai.types.character.CharShort`): 211 | Famous bots 212 | 213 | game (List of :obj:`~characterai.types.character.CharShort`): 214 | Game bots 215 | 216 | games (List of :obj:`~characterai.types.character.CharShort`): 217 | Games bots 218 | 219 | helpers (List of :obj:`~characterai.types.character.CharShort`): 220 | Helpers bots 221 | 222 | image (List of :obj:`~characterai.types.character.CharShort`): 223 | Image bots 224 | 225 | movies (List of :obj:`~characterai.types.character.CharShort`): 226 | Movies bots 227 | 228 | philosophy (List of :obj:`~characterai.types.character.CharShort`): 229 | Philosophy bots 230 | 231 | politics (List of :obj:`~characterai.types.character.CharShort`): 232 | Politics bots 233 | 234 | religion (List of :obj:`~characterai.types.character.CharShort`): 235 | Religion bots 236 | 237 | vtuber (List of :obj:`~characterai.types.character.CharShort`): 238 | VTuber bots 239 | """ 240 | animals: List[CharShort] = Field(validation_alias='Animals') 241 | anime: List[CharShort] = Field(validation_alias='Anime') 242 | anime_game: List[CharShort] = Field(validation_alias='Anime Game Characters') 243 | books: List[CharShort] = Field(validation_alias='Books') 244 | chinese: List[CharShort] = Field(validation_alias='Chinese') 245 | comedy: List[CharShort] = Field(validation_alias='Comedy') 246 | discussion: List[CharShort] = Field(validation_alias='Discussion') 247 | famous: List[CharShort] = Field(validation_alias='Famous People') 248 | game: List[CharShort] = Field(validation_alias='Game Characters') 249 | games: List[CharShort] = Field(validation_alias='Games') 250 | helpers: List[CharShort] = Field(validation_alias='Helpers') 251 | image: List[CharShort] = Field(validation_alias='Image Generating') 252 | movies: List[CharShort] = Field(validation_alias='Movies & TV') 253 | philosophy: List[CharShort] = Field(validation_alias='Philosophy') 254 | politics: List[CharShort] = Field(validation_alias='Politics') 255 | religion: List[CharShort] = Field(validation_alias='Religion') 256 | vtuber: List[CharShort] = Field(validation_alias='VTuber') -------------------------------------------------------------------------------- /characterai/pycai/methods/characters.py: -------------------------------------------------------------------------------- 1 | from .utils import caimethod, validate 2 | from ...types import character 3 | 4 | import uuid 5 | 6 | class Characters: 7 | @caimethod 8 | def get_char( 9 | self, external_id: str, 10 | *, token: str = None 11 | ): 12 | """Get information about the character 13 | 14 | EXAMPLE:: 15 | 16 | await client.get_char('ID') 17 | 18 | Args: 19 | external_id (``str``): Character ID 20 | 21 | Returns: 22 | :obj:`~characterai.types.character.Character` 23 | """ 24 | data = self.request( 25 | 'chat/character/info/', token=token, 26 | data={'external_id': external_id} 27 | ) 28 | 29 | return character.Character.model_validate( 30 | data['character'] 31 | ) 32 | 33 | @caimethod 34 | def upvoted(self, *, token: str = None): 35 | """The list of characters you have given a voice to 36 | 37 | EXAMPLE:: 38 | 39 | await client.upvoted() 40 | 41 | Returns: 42 | List of :obj:`~characterai.types.character.CharShort` 43 | """ 44 | data = self.request( 45 | 'chat/user/characters/upvoted/', 46 | token=token 47 | ) 48 | 49 | return validate( 50 | character.CharShort, 51 | data['characters'] 52 | ) 53 | 54 | @caimethod 55 | def get_category( 56 | self, name: str = 'All', 57 | *, token: str = None 58 | ): 59 | """Get characters from categories 60 | 61 | EXAMPLE:: 62 | 63 | await client.get_category() 64 | 65 | Args: 66 | name (``str``): Category name 67 | 68 | Returns: 69 | :obj:`~characterai.types.character.Categories` | List of :obj:`~characterai.types.character.CharShort` 70 | """ 71 | data = self.request( 72 | 'chat/curated_categories' 73 | '/characters/', 74 | ) 75 | 76 | categories = data['characters_by_curated_category'] 77 | 78 | if name != 'All': 79 | return character.CharShort.model_validate( 80 | categories[name] 81 | ) 82 | 83 | return character.Categories.model_validate( 84 | categories 85 | ) 86 | 87 | @caimethod 88 | def get_recommended( 89 | self, *, token: str = None 90 | ): 91 | """Get a list of recommended characters 92 | 93 | EXAMPLE:: 94 | 95 | await client.get_recommended() 96 | 97 | Returns: 98 | List of :obj:`~characterai.types.character.CharShort` 99 | """ 100 | data = self.request( 101 | 'chat/characters/trending/' 102 | ) 103 | 104 | return validate( 105 | character.CharShort, 106 | data['trending_characters'] 107 | ) 108 | 109 | @caimethod 110 | def get_trending( 111 | self, *, token: str = None 112 | ): 113 | """Get a list of trending characters 114 | 115 | EXAMPLE:: 116 | 117 | await client.get_trending() 118 | 119 | Returns: 120 | List of :obj:`~characterai.types.character.CharShort` 121 | """ 122 | data = self.request( 123 | 'recommendation/v1/user', 124 | token=token, neo=True 125 | ) 126 | 127 | return validate( 128 | character.CharShort, 129 | data['characters'] 130 | ) 131 | 132 | @caimethod 133 | def create_char( 134 | self, 135 | name: str, 136 | greeting: str, 137 | *, 138 | tgt: str = None, 139 | title: str = '', 140 | visinility: str = 'PRIVATE', 141 | copyable: bool = True, 142 | description: str = '', 143 | definition: str = '', 144 | avatar_path: str = '', 145 | token: str = None, 146 | **kwargs 147 | ): 148 | """Create a character 149 | 150 | EXAMPLE:: 151 | 152 | await client.create_char('NAME', 'GREETING') 153 | 154 | Args: 155 | name (``str``): 156 | Character name 157 | 158 | greeting (``str``): 159 | Character greeting 160 | 161 | tgt (``str``, *optional*): 162 | Old type Character ID 163 | 164 | title (``str``, *optional*): 165 | Short description of the character 166 | 167 | visibility (``str``, *optional*): 168 | Character visibility 169 | 170 | copyable (``bool``, *optional*): 171 | Ability to copy a character 172 | 173 | description (``str``, *optional*): 174 | Character description 175 | 176 | definition (``str``, *optional*: 177 | Character definition (memory, chat examples) 178 | 179 | avatar_path (``str``, *optional*): 180 | Path to the character's avatar on the c.ai server. 181 | Example: ``uploaded/2022/12/26/some_id.webp`` 182 | 183 | Returns: 184 | :obj:`~characterai.types.character.Character` 185 | """ 186 | tgt = f'id:{uuid.uuid4()}' or tgt 187 | 188 | data = self.request( 189 | 'chat/character/create/', 190 | token=token, data={ 191 | 'title': title, 192 | 'name': name, 193 | 'identifier': tgt, 194 | 'visibility': visinility, 195 | 'copyable': copyable, 196 | 'description': description, 197 | 'greeting': greeting, 198 | 'definition': definition, 199 | 'avatar_rel_path': avatar_path, 200 | **kwargs 201 | } 202 | ) 203 | 204 | return character.Character.model_validate( 205 | data['character'] 206 | ) 207 | 208 | @caimethod 209 | def update_char( 210 | self, 211 | char: str, 212 | greeting: str, 213 | name: str, 214 | *, 215 | title: str = '', 216 | visinility: str = 'PRIVATE', 217 | copyable: bool = True, 218 | description: str = '', 219 | definition: str = '', 220 | token: str = None, 221 | **kwargs 222 | ): 223 | """Editing a character 224 | 225 | EXAMPLE:: 226 | 227 | await client.create_char('CHAR_ID', 'NAME', 'GREETING') 228 | 229 | Args: 230 | char (``str``): 231 | Character ID 232 | 233 | name (``str``, *optional*): 234 | Character name 235 | 236 | greeting (``str``, *optional*): 237 | Character greeting 238 | 239 | title (``str``, *optional*): 240 | Short description of the character 241 | 242 | visibility (``str``, *optional*): 243 | Character visibility 244 | 245 | copyable (``bool``, *optional*): 246 | Ability to copy a character 247 | 248 | description (``str``, *optional*): 249 | Character description 250 | 251 | definition (``str``, *optional*): 252 | Character definition (memory, chat examples) 253 | 254 | default_voice_id (``str``, *optional*): 255 | Voice ID for TTS 256 | 257 | voice_id (``str``, *optional*): 258 | Voice ID for TTS 259 | 260 | strip_img_prompt_from_msg (``bool``, *optional*): 261 | Remove the picture hint from the message. 262 | I guess that means the characters 263 | won't be able to see the pictures (?) 264 | 265 | base_img_prompt (``str``, *optional*): 266 | Default promt for pictures (?) 267 | 268 | img_gen_enabled (``bool``, *optional*): 269 | Can pictures be generated 270 | 271 | avatar_rel_path (``str``, *optional*): 272 | Path to the character's avatar on the c.ai server. 273 | Example: ``uploaded/2022/12/26/some_id.webp`` 274 | 275 | categories (``list``, *optional*): 276 | Character сategories 277 | 278 | archived (``bool``, *optional*): 279 | Is the character archived 280 | 281 | Returns: 282 | :obj:`~characterai.types.character.Character` 283 | """ 284 | info = self.request( 285 | 'chat/character/info/', token=token, 286 | data={'external_id': char} 287 | )['character'] 288 | 289 | data = self.request( 290 | 'chat/character/update/', 291 | token=token, data={ 292 | 'external_id': char or info['external_id']['external_id'], 293 | 'name': name or info['name'], 294 | 'greeting': greeting or info['greeting'], 295 | 'title': title or info['title'], 296 | 'visibility': visibility or info['visibility'], 297 | 'copyable': copyable or info['copyable'], 298 | 'description': description or info['description'], 299 | 'definition': definition or info['definition'], 300 | 'default_voice_id': default_voice_id or info['default_voice_id'], 301 | 'voice_id': voice_id or info['voice_id'], 302 | 'strip_img_prompt_from_msg': strip_img_prompt_from_msg \ 303 | or info['strip_img_prompt_from_msg'], 304 | 'base_img_prompt': base_img_prompt or info['base_img_prompt'], 305 | 'img_gen_enabled': img_gen_enabled or info['img_gen_enabled'], 306 | 'avatar_rel_path': avatar_rel_path or info['avatar_file_name'], 307 | 'categories': categories or [], 308 | 'archived': archived or None 309 | } 310 | ) 311 | 312 | return character.Character.model_validate( 313 | data['character'] 314 | ) -------------------------------------------------------------------------------- /characterai/aiocai/methods/characters.py: -------------------------------------------------------------------------------- 1 | from .utils import caimethod, validate 2 | from ...types import character 3 | 4 | import uuid 5 | 6 | class Characters: 7 | @caimethod 8 | async def get_char( 9 | self, external_id: str, 10 | *, token: str = None 11 | ): 12 | """Get information about the character 13 | 14 | EXAMPLE:: 15 | 16 | await client.get_char('ID') 17 | 18 | Args: 19 | external_id (``str``): Character ID 20 | 21 | Returns: 22 | :obj:`~characterai.types.character.Character` 23 | """ 24 | data = await self.request( 25 | 'chat/character/info/', token=token, 26 | data={'external_id': external_id} 27 | ) 28 | 29 | return character.Character.model_validate( 30 | data['character'] 31 | ) 32 | 33 | @caimethod 34 | async def upvoted(self, *, token: str = None): 35 | """The list of characters you have given a voice to 36 | 37 | EXAMPLE:: 38 | 39 | await client.upvoted() 40 | 41 | Returns: 42 | List of :obj:`~characterai.types.character.CharShort` 43 | """ 44 | data = await self.request( 45 | 'chat/user/characters/upvoted/', 46 | token=token 47 | ) 48 | 49 | return validate( 50 | character.CharShort, 51 | data['characters'] 52 | ) 53 | 54 | @caimethod 55 | async def get_category( 56 | self, name: str = 'All', 57 | *, token: str = None 58 | ): 59 | """Get characters from categories 60 | 61 | EXAMPLE:: 62 | 63 | await client.get_category() 64 | 65 | Args: 66 | name (``str``): Category name 67 | 68 | Returns: 69 | :obj:`~characterai.types.character.Categories` | List of :obj:`~characterai.types.character.CharShort` 70 | """ 71 | data = await self.request( 72 | 'chat/curated_categories' 73 | '/characters/', 74 | ) 75 | 76 | categories = data['characters_by_curated_category'] 77 | 78 | if name != 'All': 79 | return character.CharShort.model_validate( 80 | categories[name] 81 | ) 82 | 83 | return character.Categories.model_validate( 84 | categories 85 | ) 86 | 87 | @caimethod 88 | async def get_recommended( 89 | self, *, token: str = None 90 | ): 91 | """Get a list of recommended characters 92 | 93 | EXAMPLE:: 94 | 95 | await client.get_recommended() 96 | 97 | Returns: 98 | List of :obj:`~characterai.types.character.CharShort` 99 | """ 100 | data = await self.request( 101 | 'chat/characters/trending/' 102 | ) 103 | 104 | return validate( 105 | character.CharShort, 106 | data['trending_characters'] 107 | ) 108 | 109 | @caimethod 110 | async def get_trending( 111 | self, *, token: str = None 112 | ): 113 | """Get a list of trending characters 114 | 115 | EXAMPLE:: 116 | 117 | await client.get_trending() 118 | 119 | Returns: 120 | List of :obj:`~characterai.types.character.CharShort` 121 | """ 122 | data = await self.request( 123 | 'recommendation/v1/user', 124 | token=token, neo=True 125 | ) 126 | 127 | return validate( 128 | character.CharShort, 129 | data['characters'] 130 | ) 131 | 132 | @caimethod 133 | async def create_char( 134 | self, 135 | name: str, 136 | greeting: str, 137 | *, 138 | tgt: str = None, 139 | title: str = '', 140 | visibility: str = 'PRIVATE', 141 | copyable: bool = True, 142 | description: str = '', 143 | definition: str = '', 144 | avatar_path: str = '', 145 | token: str = None, 146 | **kwargs 147 | ): 148 | """Create a character 149 | 150 | EXAMPLE:: 151 | 152 | await client.create_char('NAME', 'GREETING') 153 | 154 | Args: 155 | name (``str``): 156 | Character name 157 | 158 | greeting (``str``): 159 | Character greeting 160 | 161 | tgt (``str``, *optional*): 162 | Old type Character ID 163 | 164 | title (``str``, *optional*): 165 | Short description of the character 166 | 167 | visibility (``str``, *optional*): 168 | Character visibility 169 | 170 | copyable (``bool``, *optional*): 171 | Ability to copy a character 172 | 173 | description (``str``, *optional*): 174 | Character description 175 | 176 | definition (``str``, *optional*: 177 | Character definition (memory, chat examples) 178 | 179 | avatar_path (``str``, *optional*): 180 | Path to the character's avatar on the c.ai server. 181 | Example: ``uploaded/2022/12/26/some_id.webp`` 182 | 183 | Returns: 184 | :obj:`~characterai.types.character.Character` 185 | """ 186 | tgt = f'id:{uuid.uuid4()}' or tgt 187 | 188 | data = await self.request( 189 | 'chat/character/create/', 190 | token=token, data={ 191 | 'title': title, 192 | 'name': name, 193 | 'identifier': tgt, 194 | 'visibility': visibility, 195 | 'copyable': copyable, 196 | 'description': description, 197 | 'greeting': greeting, 198 | 'definition': definition, 199 | 'avatar_rel_path': avatar_path, 200 | **kwargs 201 | } 202 | ) 203 | 204 | return character.Character.model_validate( 205 | data['character'] 206 | ) 207 | 208 | @caimethod 209 | async def update_char( 210 | self, 211 | char: str, 212 | *, 213 | greeting: str = '', 214 | name: str = '', 215 | title: str = '', 216 | visibility: str = 'PRIVATE', 217 | copyable: bool = True, 218 | description: str = '', 219 | definition: str = '', 220 | archived: bool = False, 221 | default_voice_id: str = '', 222 | voice_id: str = '', 223 | strip_img_prompt_from_msg: bool = False, 224 | base_img_prompt: str = '', 225 | img_gen_enabled: bool = False, 226 | avatar_rel_path: str = '', 227 | categories: list = [], 228 | token: str = None 229 | ): 230 | """Editing a character 231 | 232 | EXAMPLE:: 233 | 234 | await client.create_char('CHAR_ID', 'NAME', 'GREETING') 235 | 236 | Args: 237 | char (``str``): 238 | Character ID 239 | 240 | name (``str``, *optional*): 241 | Character name 242 | 243 | greeting (``str``, *optional*): 244 | Character greeting 245 | 246 | title (``str``, *optional*): 247 | Short description of the character 248 | 249 | visibility (``str``, *optional*): 250 | Character visibility 251 | 252 | copyable (``bool``, *optional*): 253 | Ability to copy a character 254 | 255 | description (``str``, *optional*): 256 | Character description 257 | 258 | definition (``str``, *optional*): 259 | Character definition (memory, chat examples) 260 | 261 | default_voice_id (``str``, *optional*): 262 | Voice ID for TTS 263 | 264 | voice_id (``str``, *optional*): 265 | Voice ID for TTS 266 | 267 | strip_img_prompt_from_msg (``bool``, *optional*): 268 | Remove the picture hint from the message. 269 | I guess that means the characters 270 | won't be able to see the pictures (?) 271 | 272 | base_img_prompt (``str``, *optional*): 273 | Default promt for pictures (?) 274 | 275 | img_gen_enabled (``bool``, *optional*): 276 | Can pictures be generated 277 | 278 | avatar_rel_path (``str``, *optional*): 279 | Path to the character's avatar on the c.ai server. 280 | Example: ``uploaded/2022/12/26/some_id.webp`` 281 | 282 | categories (``list``, *optional*): 283 | Character сategories 284 | 285 | archived (``bool``, *optional*): 286 | Is the character archived 287 | 288 | Returns: 289 | :obj:`~characterai.types.character.Character` 290 | """ 291 | charInfo = await self.request( 292 | 'chat/character/info/', token=token, 293 | data={'external_id': char} 294 | ) 295 | 296 | info = charInfo['character'] 297 | 298 | data = await self.request( 299 | 'chat/character/update/', 300 | token=token, data={ 301 | 'external_id': char or info['external_id']['external_id'], 302 | 'name': name or info['name'], 303 | 'greeting': greeting or info['greeting'], 304 | 'title': title or info['title'], 305 | 'visibility': visibility or info['visibility'], 306 | 'copyable': copyable or info['copyable'], 307 | 'description': description or info['description'], 308 | 'definition': definition or info['definition'], 309 | 'default_voice_id': default_voice_id or info['default_voice_id'], 310 | 'voice_id': voice_id or info['voice_id'], 311 | 'strip_img_prompt_from_msg': strip_img_prompt_from_msg \ 312 | or info['strip_img_prompt_from_msg'], 313 | 'base_img_prompt': base_img_prompt or info['base_img_prompt'], 314 | 'img_gen_enabled': img_gen_enabled or info['img_gen_enabled'], 315 | 'avatar_rel_path': avatar_rel_path or info['avatar_file_name'], 316 | 'categories': categories or [], 317 | 'archived': archived or None 318 | } 319 | ) 320 | 321 | return character.Character.model_validate( 322 | data['character'] 323 | ) -------------------------------------------------------------------------------- /characterai/types/chat1.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import List, Optional 4 | 5 | from .other import Avatar 6 | 7 | class Replies(BaseModel): 8 | """Message info 9 | 10 | Parameters: 11 | text (``str``): 12 | Message text 13 | 14 | uuid (``str``): 15 | Message UUID 16 | 17 | id (``int``): 18 | Message ID 19 | """ 20 | text: str 21 | uuid: str 22 | id: int 23 | 24 | class Participant(BaseModel): 25 | name: str 26 | 27 | class SrcChar(BaseModel, Avatar): 28 | """Character info in the message 29 | 30 | Parameters: 31 | name (``str``, *property*): 32 | Character name 33 | 34 | avatar_file_name (``str``): 35 | Path to the avatar on the server 36 | 37 | avatar (:obj:`~characterai.types.other.Avatar`): 38 | Avatar info 39 | """ 40 | participant: Participant 41 | avatar_file_name: Optional[str] = None 42 | 43 | @property 44 | def name(self): 45 | return self.participant.name 46 | 47 | class Message(BaseModel): 48 | """Информация о сообщении типа chat1 49 | 50 | Parameters: 51 | replies (List of :obj:`~characterai.types.chat1.Replies`): 52 | Message object 53 | 54 | src_char (:obj:`~characterai.types.SrcChar`): 55 | Character info 56 | 57 | is_final_chunk (``bool``): 58 | Whether the message (chunk) is the last in generation 59 | 60 | last_user_msg_id (``int``): 61 | ID последнего сообщения пользователя 62 | 63 | last_user_msg_uuid (``str``): 64 | UUID of the last user message 65 | 66 | id (``int``, *property*): 67 | Message ID 68 | 69 | text (``str``, *property*): 70 | Message text 71 | 72 | uuid (``str``, *property*): 73 | Message UUID 74 | 75 | author (``str``, *property*): 76 | Author name 77 | """ 78 | replies: List[Replies] 79 | src_char: SrcChar 80 | is_final_chunk: bool 81 | last_user_msg_id: int 82 | last_user_msg_uuid: str 83 | 84 | @property 85 | def id(self): 86 | return self.replies[0].id 87 | 88 | @property 89 | def text(self): 90 | return self.replies[0].text 91 | 92 | @property 93 | def uuid(self): 94 | return self.replies[0].uuid 95 | 96 | @property 97 | def author(self): 98 | return self.src_char.participant.name 99 | 100 | class UserAccount(BaseModel, Avatar): 101 | """Your account 102 | 103 | Parameters: 104 | name (``str``): 105 | Your name 106 | 107 | avatar_type (``str``): 108 | Avatar status (uploaded or not) 109 | 110 | onboarding_complete (``bool``): 111 | For pop-up banners (?) 112 | 113 | mobile_onboarding_complete (``int``, *optional*): 114 | For mobile pop-up banners (?) 115 | 116 | avatar_file_name (``str``): 117 | Path to the avatar on the server 118 | 119 | avatar (:obj:`~characterai.types.other.Avatar`): 120 | Avatar info 121 | """ 122 | name: str 123 | avatar_type: str 124 | onboarding_complete: bool 125 | avatar_file_name: str 126 | mobile_onboarding_complete: int 127 | 128 | class User(BaseModel): 129 | """Object in chat 130 | 131 | Parameters: 132 | username (``str``): 133 | Your nickname or character ID 134 | 135 | id (``bool``): 136 | Object ID 137 | 138 | first_name (``str``): 139 | Your email or character name 140 | 141 | account (:obj:`~characterai.types.chat1.UserAccount`, *optional*): 142 | Your account information 143 | 144 | is_staff (``bool``): 145 | Is the object an employee of the service 146 | """ 147 | username: str 148 | id: int 149 | first_name: str 150 | account: Optional[UserAccount] = None 151 | is_staff: bool 152 | 153 | class Participants(BaseModel): 154 | """Objects in chat 155 | 156 | Parameters: 157 | user (:obj:`~characterai.types.chat1.User`): 158 | Object information 159 | 160 | is_human (``bool``): 161 | Is a human 162 | 163 | name (``str``): 164 | Object name 165 | 166 | num_interactions (``int``): 167 | Total number of chats 168 | """ 169 | user: User 170 | is_human: bool 171 | name: str 172 | num_interactions: int 173 | 174 | class Messages(BaseModel): 175 | """Сообщения в чате 176 | 177 | Parameters: 178 | deleted (``bool``): 179 | Has the message been deleted 180 | 181 | id (``int``): 182 | Message ID 183 | 184 | text (``str``): 185 | Message text 186 | 187 | image_prompt_text (``str``): 188 | Picture generation promt 189 | 190 | image_rel_path (``str``): 191 | URL path to the picture 192 | 193 | is_alternative (``str``): 194 | Is the message alternatively generated 195 | 196 | responsible_user__username (``str``): 197 | Nickname of the character's author 198 | 199 | src__is_human (``bool``): 200 | Is the message from the object a person 201 | 202 | src__name (``str``): 203 | Character name 204 | 205 | src__user__username (``str``): 206 | Nickname of the character's author 207 | 208 | src_char (:obj:`~characterai.types.SrcChar`): 209 | Character object 210 | """ 211 | deleted: bool 212 | id: int = Field(validation_alias='id ') 213 | image_prompt_text: str 214 | image_rel_path: str 215 | is_alternative: bool 216 | responsible_user__username: str 217 | src__character__avatar_file_name: str 218 | src__is_human: bool 219 | src__name: str 220 | src__user__username: str 221 | src_char: SrcChar 222 | text: str 223 | 224 | class NewChat(BaseModel): 225 | """New chat info 226 | 227 | Parameters: 228 | title (``str``): 229 | Chat title (?) 230 | 231 | participants (List of :obj:`~characterai.types.chat1.Participants`): 232 | Objects in chat 233 | 234 | external_id (``str``): 235 | Chat ID 236 | 237 | last_interaction (:py:obj:`~datetime.datetime`): 238 | Date of last message 239 | 240 | created (:py:obj:`~datetime.datetime`): 241 | Chat creation date 242 | 243 | type (``str``): 244 | Chat type (chat or room) 245 | 246 | description (``str``): 247 | Chat description (?) 248 | 249 | speech (``str``): 250 | WebRTC voice 251 | 252 | status (``str``): 253 | Status of function execution 254 | 255 | has_more (``bool``): 256 | Are there any more chat messages 257 | 258 | messages (List of :obj:`~characterai.types.chat1.Messages`): 259 | Messages list 260 | 261 | id (``str``): 262 | Chat ID 263 | 264 | tgt (``str``, *property*): 265 | Old character ID type 266 | """ 267 | title: str 268 | participants: List[Participants] 269 | external_id: str 270 | created: datetime 271 | last_interaction: datetime 272 | type: str 273 | description: str 274 | speech: str 275 | status: str 276 | has_more: bool 277 | messages: List[Messages] 278 | 279 | id: str = Field( 280 | validation_alias='external_id' 281 | ) 282 | 283 | @property 284 | def tgt(self): 285 | return self.participants[1].user.username 286 | 287 | class Avatars(BaseModel, Avatar): 288 | name: str 289 | 290 | avatar_file_name: Optional[str] = \ 291 | Field(validation_alias='user__account__avatar_file_name') or \ 292 | Field(validation_alias='character__avatar_file_name') 293 | 294 | class ChatHistory(BaseModel): 295 | """Chat history 296 | 297 | Parameters: 298 | title (``str``): 299 | Chat name (for rooms) 300 | 301 | participants (List of :obj:`~characterai.types.chat1.Participants`): 302 | Objects in chat 303 | 304 | external_id (``str``): 305 | Chat ID 306 | 307 | last_interaction (:py:obj:`~datetime.datetime`): 308 | Date of last message 309 | 310 | created (:py:obj:`~datetime.datetime`): 311 | Chat creation date 312 | 313 | type (``str``): 314 | Chat type (chat or room) 315 | 316 | avatars (List of :obj:`~characterai.types.chat1.Avatars`): 317 | Avatars of objects in chat 318 | 319 | room_img_gen_enabled (``bool``): 320 | Can the pictures be generated 321 | """ 322 | title: str 323 | participants: List[Participants] 324 | external_id: str 325 | created: datetime 326 | last_interaction: datetime 327 | type: str 328 | description: str 329 | avatars: List[Avatars] 330 | room_img_gen_enabled: bool 331 | 332 | class HisMessage(BaseModel): 333 | """Message in the chat history list 334 | 335 | Parameters: 336 | id (``str``): 337 | Message ID 338 | 339 | uuid (``str``): 340 | Message UUID 341 | 342 | text (``str``): 343 | Message text 344 | 345 | src (``str``): 346 | User message ID 347 | 348 | tgt (``str``): 349 | Old character ID type 350 | 351 | is_alternative (``bool``, *optional*): 352 | Is the message alternatively generated 353 | 354 | image_rel_path (``str``): 355 | Path to the picture, if available 356 | 357 | image_prompt_text (``str``): 358 | Promt for the generated image 359 | 360 | deleted (``bool``, *optional*): 361 | Has the message been deleted 362 | 363 | src__name (``str``): 364 | User name 365 | 366 | src__user__username (``str``): 367 | User nickname 368 | 369 | src__is_human (``bool``, *optional*): 370 | Is the user a human/account 371 | 372 | src__character__avatar_file_name (``str``, *optional*): 373 | URL link to avatar 374 | 375 | src_char (:obj:`~characterai.types.SrcChar`): 376 | Character info 377 | 378 | responsible_user__username (``str``, *optional*): 379 | I don't know what it is 380 | """ 381 | id: int 382 | uuid: str 383 | text: str 384 | src: str 385 | tgt: str 386 | is_alternative: Optional[bool] = None 387 | image_rel_path: str 388 | image_prompt_text: str 389 | deleted: Optional[bool] = None 390 | src__name: str 391 | src__user__username: str 392 | src__is_human: Optional[bool] = None 393 | src__character__avatar_file_name: Optional[str] = None 394 | src_char: SrcChar 395 | responsible_user__username: Optional[str] = None 396 | 397 | class History(BaseModel): 398 | """Chat history info 399 | 400 | Parameters: 401 | external_id (``str``): 402 | Chat ID 403 | 404 | last_interaction (:py:obj:`~datetime.datetime`): 405 | Date of last message 406 | 407 | created (:py:obj:`~datetime.datetime`): 408 | Chat creation date 409 | 410 | msgs (List of :obj:`~characterai.types.chat1.HisMessage`): 411 | Messages list 412 | """ 413 | external_id: str 414 | last_interaction: datetime 415 | created: datetime 416 | msgs: List[HisMessage] 417 | 418 | class HisMessages(BaseModel): 419 | """Chat message list 420 | 421 | Parameters: 422 | messages (List of :obj:`~characterai.types.chat1.HisMessage`): 423 | Messages 424 | 425 | next_page (``int``): 426 | Next page number 427 | 428 | has_more (``bool``): 429 | Is there any more messages 430 | """ 431 | messages: List[HisMessage] 432 | next_page: int 433 | has_more: bool 434 | 435 | class Migrate(BaseModel): 436 | """Info about migrating from chat1 to chat2 437 | 438 | Parameters: 439 | id (``str``): 440 | New chat ID 441 | 442 | create_time (:py:obj:`~datetime.datetime`): 443 | Date of migration creation 444 | 445 | last_update (:py:obj:`~datetime.datetime`): 446 | Date of data update 447 | 448 | status (``str``): 449 | Migration status 450 | 451 | properties (``str``): 452 | Additional migration options (?) 453 | """ 454 | id: str = Field(validation_alias='migrationId') 455 | create_time: datetime = Field(validation_alias='createTime') 456 | last_update: datetime = Field(validation_alias='lastUpdateTime') 457 | status: str 458 | properties: str -------------------------------------------------------------------------------- /characterai/pycai/methods/chat2.py: -------------------------------------------------------------------------------- 1 | import json 2 | from websockets import exceptions 3 | from websockets.sync import client as websockets 4 | import uuid 5 | 6 | from .utils import Request, caimethod, validate 7 | from ...errors import ServerError 8 | from ...types import chat2 9 | 10 | class ChatV2(Request): 11 | def __init__( 12 | self, session = None, 13 | token: str = None 14 | ): 15 | self.session = session 16 | self.token = token 17 | 18 | @caimethod 19 | def get_histories( 20 | self, char: str, *, 21 | preview: int = 2, token: str = None 22 | ): 23 | """Get a list of your character's chat histories 24 | 25 | EXAMPLE:: 26 | 27 | await client.get_chat('CHAR') 28 | 29 | Args: 30 | char (``str``): 31 | Character ID 32 | 33 | preview (``int``, *optional*): 34 | The number of recent messages 35 | that will be shown 36 | 37 | Returns: 38 | List of :obj:`~characterai.types.chat2.ChatData` 39 | """ 40 | data = self.request( 41 | f'chats/?character_ids={char}' 42 | f'&num_preview_turns={preview}', 43 | token=token, neo=True 44 | ) 45 | 46 | return validate( 47 | chat2.ChatData, 48 | data['chats'] 49 | ) 50 | 51 | @caimethod 52 | def get_history( 53 | self, chat_id: str, *, 54 | token: str = None 55 | ): 56 | """Get chat history 57 | 58 | EXAMPLE:: 59 | 60 | await client.get_history('CHAT_ID') 61 | 62 | Args: 63 | chat_id (``str``): 64 | Chat ID 65 | 66 | Returns: 67 | :obj:`~characterai.types.chat2.History` 68 | """ 69 | return chat2.History.model_validate( 70 | self.request( 71 | f'turns/{chat_id}/', 72 | token=token, neo=True 73 | ) 74 | ) 75 | 76 | @caimethod 77 | def get_chat( 78 | self, char: str, *, 79 | token: str = None 80 | ): 81 | """Get information about the last chat 82 | 83 | EXAMPLE:: 84 | 85 | await client.get_chat('CHAR') 86 | 87 | Args: 88 | char (``str``): 89 | Character ID 90 | 91 | Returns: 92 | :obj:`~characterai.types.chat2.ChatData` 93 | """ 94 | return chat2.ChatData.model_validate( 95 | (self.request( 96 | f'chats/recent/{char}', 97 | token=token, neo=True 98 | ))['chats'][0] 99 | ) 100 | 101 | @caimethod 102 | def pin( 103 | self, pinned: bool, chat_id: str, 104 | turn_id: str, *, token: str = None 105 | ): 106 | """Pin chat messages 107 | 108 | This is to make sure characters 109 | don't forget certain things 110 | and they will always remember 111 | what's in the pinned posts 112 | 113 | EXAMPLE:: 114 | 115 | await client.pin(PINNED, 'CHAT_ID', 'TURN_ID') 116 | 117 | Args: 118 | pinned (``bool``): 119 | ``True`` pin, ``False`` unpin 120 | 121 | chat_id (``str``): 122 | Chat ID 123 | 124 | turn_id (``str``): 125 | Message ID 126 | 127 | Returns: 128 | :obj:`~characterai.types.chat2.BotAnswer` 129 | """ 130 | return chat2.BotAnswer.model_validate( 131 | (self.request( 132 | 'turn/pin', neo=True, 133 | token=token, data={ 134 | 'is_pinned': pinned, 135 | 'turn_key': { 136 | 'chat_id': chat_id, 137 | 'turn_id': turn_id 138 | } 139 | } 140 | ))['turn'] 141 | ) 142 | 143 | def delete_message( 144 | self, chat_id: str, ids: list 145 | ) -> bool: 146 | """Deleting messages. Returns ``True`` on success 147 | 148 | EXAMPLE:: 149 | 150 | await client.delete_message('CHAT_ID', ['UUID']) 151 | 152 | Args: 153 | chat_id (``str``): 154 | Chat ID 155 | 156 | ids (List of ``str``): 157 | List of message IDs to be deleted 158 | 159 | Returns: 160 | ``bool`` 161 | """ 162 | self.ws.send(json.dumps({ 163 | 'command':'remove_turns', 164 | 'payload': { 165 | 'chat_id': chat_id, 166 | 'turn_ids': ids 167 | } 168 | })) 169 | 170 | response = json.loads(self.ws.recv()) 171 | 172 | if response['command'] == 'neo_error': 173 | raise ServerError(response['comment']) 174 | 175 | return True 176 | 177 | def next_message( 178 | self, char: str, chat_id: str, turn_id: str, 179 | *, tts: bool = False, lang: str = 'English' 180 | ): 181 | """Generate an alternative answer 182 | 183 | EXAMPLE:: 184 | 185 | await client.next_message( 186 | 'CHAR', 'CHAT_ID', 'MSG_ID' 187 | ) 188 | 189 | Args: 190 | char (``str``): 191 | Character ID 192 | 193 | chat_id (``str``): 194 | Chat ID 195 | 196 | turn_id (``str``): 197 | Message ID 198 | 199 | tts (``bool``, *optional*): 200 | Generate audio for the message 201 | 202 | lang (``str``, *optional*): 203 | The language of your message. 204 | That's the language you're most 205 | likely to respond in. 206 | 207 | Returns: 208 | :obj:`~characterai.types.chat2.BotAnswer` 209 | """ 210 | self.ws.send(json.dumps({ 211 | 'command':'generate_turn_candidate', 212 | 'payload': { 213 | 'tts_enabled': tts, 214 | 'selected_language': lang, 215 | 'character_id': char, 216 | 'turn_key': { 217 | 'turn_id': turn_id, 218 | 'chat_id': chat_id 219 | } 220 | } 221 | })) 222 | 223 | while True: 224 | response = json.loads(self.ws.recv()) 225 | 226 | try: 227 | turn = response['turn'] 228 | except: 229 | raise ServerError(response['comment']) 230 | 231 | if not turn['author']['author_id'].isdigit(): 232 | try: 233 | turn['candidates'][0]['is_final'] 234 | except: ... 235 | else: 236 | return chat2.BotAnswer.model_validate( 237 | turn 238 | ) 239 | 240 | def new_chat( 241 | self, char: str, creator_id: str, 242 | *, greeting: bool = True, chat_id: str = None 243 | ): 244 | """Editing the message text 245 | 246 | EXAMPLE:: 247 | 248 | await client.new_chat('CHAR', 'CREATOR_ID') 249 | 250 | Args: 251 | char (``str``): 252 | Character ID 253 | 254 | creator_id (``str``): 255 | Your account ID. Can be found at 256 | :obj:`~characterai.aiocai.methods.chat2.ChatV2.get_me` 257 | 258 | greeting (``bool``, *optional*): 259 | If ``False``, the new chat will be 260 | without the character's first message 261 | 262 | chat_id (``str``, *optional*): 263 | You can specify your chat ID, 264 | it can be any ``str`` 265 | 266 | Returns: 267 | :obj:`~characterai.types.chat2.BotAnswer` 268 | """ 269 | chat_id = str(uuid.uuid4()) or chat_id 270 | 271 | if isinstance(creator_id, int): 272 | creator_id = str(creator_id) 273 | 274 | self.ws.send(json.dumps({ 275 | 'command': 'create_chat', 276 | 'payload': { 277 | 'chat': { 278 | 'chat_id': chat_id, 279 | 'creator_id': creator_id, 280 | 'visibility': 'VISIBILITY_PRIVATE', 281 | 'character_id': char, 282 | 'type': 'TYPE_ONE_ON_ONE' 283 | }, 284 | 'with_greeting': greeting 285 | } 286 | })) 287 | 288 | response = json.loads(self.ws.recv()) 289 | try: response['chat'] 290 | except KeyError: 291 | raise ServerError(response['comment']) 292 | else: 293 | answer = chat2.BotAnswer.model_validate( 294 | json.loads( 295 | self.ws.recv() 296 | )['turn'] 297 | ) 298 | 299 | response = chat2.ChatData.model_validate( 300 | response['chat'] 301 | ) 302 | 303 | return response, answer 304 | 305 | def send_message( 306 | self, char: str, chat_id: str, text: str, 307 | author: dict = {}, *, image: str = None, 308 | custom_id: str = None 309 | ): 310 | """Sending a message to chat 311 | 312 | EXAMPLE:: 313 | 314 | await client.send_message('CHAR', 'CHAT_ID', 'TEXT') 315 | 316 | Args: 317 | char (``str``): 318 | Character ID 319 | 320 | chat_id (``str``): 321 | Chat ID 322 | 323 | text (``str``): 324 | Message text 325 | 326 | custom_id (``str``, *optional*): 327 | Its ID for the message, can be any ``str`` 328 | 329 | image (``str``, *optional*): 330 | Attach image to message. This should 331 | be the URL path on the server 332 | 333 | Returns: 334 | :obj:`~characterai.types.chat2.BotAnswer` 335 | """ 336 | turn_key = { 337 | 'chat_id': chat_id 338 | } 339 | 340 | if custom_id != None: 341 | turn_key['turn_id'] = custom_id 342 | 343 | message = { 344 | 'command': 'create_and_generate_turn', 345 | 'payload': { 346 | 'character_id': char, 347 | 'turn': { 348 | 'turn_key': turn_key, 349 | 'author': author, 350 | 'candidates': [ 351 | { 352 | 'raw_content': text, 353 | 'tti_image_rel_path': image 354 | } 355 | ] 356 | } 357 | } 358 | } 359 | 360 | self.ws.send(json.dumps(message)) 361 | 362 | while True: 363 | response = json.loads(self.ws.recv()) 364 | 365 | try: 366 | turn = response['turn'] 367 | except: 368 | raise ServerError(response['comment']) 369 | 370 | if not turn['author']['author_id'].isdigit(): 371 | try: 372 | turn['candidates'][0]['is_final'] 373 | except: ... 374 | else: 375 | return chat2.BotAnswer.model_validate( 376 | turn 377 | ) 378 | 379 | def edit_message( 380 | self, chat_id: str, message_id: str, 381 | text: str, *, token: str = None 382 | ): 383 | """Edit the message text 384 | 385 | EXAMPLE:: 386 | 387 | await client.edit_message('CHAT_ID', 'MSG_ID', 'TEXT') 388 | 389 | Args: 390 | chat_id (``str``): 391 | Chat ID 392 | 393 | message_id (``str``): 394 | Message ID 395 | 396 | text (``str``): 397 | New message text 398 | 399 | Returns: 400 | :obj:`~characterai.types.chat2.BotAnswer` 401 | """ 402 | self.ws.send(json.dumps({ 403 | 'command':'edit_turn_candidate', 404 | 'payload': { 405 | 'turn_key': { 406 | 'chat_id': chat_id, 407 | 'turn_id': message_id 408 | }, 409 | 'new_candidate_raw_content': text 410 | } 411 | })) 412 | 413 | response = json.loads(self.ws.recv()) 414 | 415 | try: response['turn'] 416 | except KeyError: 417 | raise ServerError(response['comment']) 418 | else: 419 | return chat2.BotAnswer.model_validate( 420 | response['turn'] 421 | ) 422 | 423 | class WSConnect(ChatV2): 424 | def __init__( 425 | self, token: str = None, 426 | *, start: bool = True 427 | ): 428 | if not start: 429 | self.token = token 430 | 431 | def __enter__(self): 432 | return self 433 | 434 | def __exit__(self, *args): 435 | self.close() 436 | 437 | def _connect(self): 438 | cookie = f'HTTP_AUTHORIZATION="Token {self.token}"' 439 | 440 | try: 441 | self.ws = websockets.connect( 442 | 'wss://neo.character.ai/ws/', 443 | additional_headers={ 444 | 'Cookie': cookie 445 | } 446 | ) 447 | except exceptions.InvalidStatusCode as e: 448 | if e.status_code == 403: 449 | raise ServerError('Wrong token') 450 | 451 | return self 452 | 453 | def __call__( 454 | self, token: str = None, 455 | *, start: bool = True 456 | ): 457 | self.token = token or self.token 458 | 459 | return self._connect() 460 | 461 | def close(self): 462 | return self.ws.close() 463 | -------------------------------------------------------------------------------- /characterai/aiocai/methods/chat2.py: -------------------------------------------------------------------------------- 1 | import json 2 | import websockets 3 | from websockets import exceptions 4 | import uuid 5 | 6 | from .utils import Request, caimethod, validate 7 | from ...errors import ServerError 8 | from ...types import chat2 9 | 10 | class ChatV2(Request): 11 | def __init__( 12 | self, session = None, 13 | token: str = None 14 | ): 15 | self.session = session 16 | self.token = token 17 | 18 | @caimethod 19 | async def get_histories( 20 | self, char: str, *, 21 | preview: int = 2, token: str = None 22 | ): 23 | """Get a list of your character's chat histories 24 | 25 | EXAMPLE:: 26 | 27 | await client.get_chat('CHAR') 28 | 29 | Args: 30 | char (``str``): 31 | Character ID 32 | 33 | preview (``int``, *optional*): 34 | The number of recent messages 35 | that will be shown 36 | 37 | Returns: 38 | List of :obj:`~characterai.types.chat2.ChatData` 39 | """ 40 | data = await self.request( 41 | f'chats/?character_ids={char}' 42 | f'&num_preview_turns={preview}', 43 | token=token, neo=True 44 | ) 45 | 46 | return validate( 47 | chat2.ChatData, 48 | data['chats'] 49 | ) 50 | 51 | @caimethod 52 | async def get_history( 53 | self, chat_id: str, *, 54 | token: str = None 55 | ): 56 | """Get chat history 57 | 58 | EXAMPLE:: 59 | 60 | await client.get_history('CHAT_ID') 61 | 62 | Args: 63 | chat_id (``str``): 64 | Chat ID 65 | 66 | Returns: 67 | :obj:`~characterai.types.chat2.History` 68 | """ 69 | return chat2.History.model_validate( 70 | await self.request( 71 | f'turns/{chat_id}/', 72 | token=token, neo=True 73 | ) 74 | ) 75 | 76 | @caimethod 77 | async def get_chat( 78 | self, char: str, *, 79 | token: str = None 80 | ): 81 | """Get information about the last chat 82 | 83 | EXAMPLE:: 84 | 85 | await client.get_chat('CHAR') 86 | 87 | Args: 88 | char (``str``): 89 | Character ID 90 | 91 | Returns: 92 | :obj:`~characterai.types.chat2.ChatData` 93 | """ 94 | return chat2.ChatData.model_validate( 95 | (await self.request( 96 | f'chats/recent/{char}', 97 | token=token, neo=True 98 | ))['chats'][0] 99 | ) 100 | 101 | @caimethod 102 | async def pin( 103 | self, pinned: bool, chat_id: str, 104 | turn_id: str, *, token: str = None 105 | ): 106 | """Pin chat messages 107 | 108 | This is to make sure characters 109 | don't forget certain things 110 | and they will always remember 111 | what's in the pinned posts 112 | 113 | EXAMPLE:: 114 | 115 | await client.pin(PINNED, 'CHAT_ID', 'TURN_ID') 116 | 117 | Args: 118 | pinned (``bool``): 119 | ``True`` pin, ``False`` unpin 120 | 121 | chat_id (``str``): 122 | Chat ID 123 | 124 | turn_id (``str``): 125 | Message ID 126 | 127 | Returns: 128 | :obj:`~characterai.types.chat2.BotAnswer` 129 | """ 130 | return chat2.BotAnswer.model_validate( 131 | (await self.request( 132 | 'turn/pin', neo=True, 133 | token=token, data={ 134 | 'is_pinned': pinned, 135 | 'turn_key': { 136 | 'chat_id': chat_id, 137 | 'turn_id': turn_id 138 | } 139 | } 140 | ))['turn'] 141 | ) 142 | 143 | async def delete_message( 144 | self, chat_id: str, ids: list 145 | ) -> bool: 146 | """Deleting messages. Returns ``True`` on success 147 | 148 | EXAMPLE:: 149 | 150 | await client.delete_message('CHAT_ID', ['UUID']) 151 | 152 | Args: 153 | chat_id (``str``): 154 | Chat ID 155 | 156 | ids (List of ``str``): 157 | List of message IDs to be deleted 158 | 159 | Returns: 160 | ``bool`` 161 | """ 162 | await self.ws.send(json.dumps({ 163 | 'command':'remove_turns', 164 | 'payload': { 165 | 'chat_id': chat_id, 166 | 'turn_ids': ids 167 | } 168 | })) 169 | 170 | response = json.loads(await self.ws.recv()) 171 | 172 | if response['command'] == 'neo_error': 173 | raise ServerError(response['comment']) 174 | 175 | return True 176 | 177 | async def next_message( 178 | self, char: str, chat_id: str, turn_id: str, 179 | *, tts: bool = False, lang: str = 'English' 180 | ): 181 | """Generate an alternative answer 182 | 183 | EXAMPLE:: 184 | 185 | await client.next_message( 186 | 'CHAR', 'CHAT_ID', 'MSG_ID' 187 | ) 188 | 189 | Args: 190 | char (``str``): 191 | Character ID 192 | 193 | chat_id (``str``): 194 | Chat ID 195 | 196 | turn_id (``str``): 197 | Message ID 198 | 199 | tts (``bool``, *optional*): 200 | Generate audio for the message 201 | 202 | lang (``str``, *optional*): 203 | The language of your message. 204 | That's the language you're most 205 | likely to respond in. 206 | 207 | Returns: 208 | :obj:`~characterai.types.chat2.BotAnswer` 209 | """ 210 | await self.ws.send(json.dumps({ 211 | 'command':'generate_turn_candidate', 212 | 'payload': { 213 | 'tts_enabled': tts, 214 | 'selected_language': lang, 215 | 'character_id': char, 216 | 'turn_key': { 217 | 'turn_id': turn_id, 218 | 'chat_id': chat_id 219 | } 220 | } 221 | })) 222 | 223 | while True: 224 | response = json.loads(await self.ws.recv()) 225 | 226 | try: 227 | turn = response['turn'] 228 | except: 229 | raise ServerError(response['comment']) 230 | 231 | if not turn['author']['author_id'].isdigit(): 232 | try: 233 | turn['candidates'][0]['is_final'] 234 | except: ... 235 | else: 236 | return chat2.BotAnswer.model_validate( 237 | turn 238 | ) 239 | 240 | async def new_chat( 241 | self, char: str, creator_id: str, 242 | *, greeting: bool = True, chat_id: str = None 243 | ): 244 | """Editing the message text 245 | 246 | EXAMPLE:: 247 | 248 | await client.new_chat('CHAR', 'CREATOR_ID') 249 | 250 | Args: 251 | char (``str``): 252 | Character ID 253 | 254 | creator_id (``str``): 255 | Your account ID. Can be found at 256 | :obj:`~characterai.aiocai.methods.chat2.ChatV2.get_me` 257 | 258 | greeting (``bool``, *optional*): 259 | If ``False``, the new chat will be 260 | without the character's first message 261 | 262 | chat_id (``str``, *optional*): 263 | You can specify your chat ID, 264 | it can be any ``str`` 265 | 266 | Returns: 267 | :obj:`~characterai.types.chat2.BotAnswer` 268 | """ 269 | chat_id = str(uuid.uuid4()) or chat_id 270 | 271 | if isinstance(creator_id, int): 272 | creator_id = str(creator_id) 273 | 274 | await self.ws.send(json.dumps({ 275 | 'command': 'create_chat', 276 | 'payload': { 277 | 'chat': { 278 | 'chat_id': chat_id, 279 | 'creator_id': creator_id, 280 | 'visibility': 'VISIBILITY_PRIVATE', 281 | 'character_id': char, 282 | 'type': 'TYPE_ONE_ON_ONE' 283 | }, 284 | 'with_greeting': greeting 285 | } 286 | })) 287 | 288 | response = json.loads(await self.ws.recv()) 289 | try: response['chat'] 290 | except KeyError: 291 | raise ServerError(response['comment']) 292 | else: 293 | answer = chat2.BotAnswer.model_validate( 294 | json.loads( 295 | await self.ws.recv() 296 | )['turn'] 297 | ) 298 | 299 | response = chat2.ChatData.model_validate( 300 | response['chat'] 301 | ) 302 | 303 | return response, answer 304 | 305 | async def send_message( 306 | self, char: str, chat_id: str, text: str, 307 | author: dict = {}, *, image: str = None, 308 | custom_id: str = None 309 | ): 310 | """Sending a message to chat 311 | 312 | EXAMPLE:: 313 | 314 | await client.send_message('CHAR', 'CHAT_ID', 'TEXT') 315 | 316 | Args: 317 | char (``str``): 318 | Character ID 319 | 320 | chat_id (``str``): 321 | Chat ID 322 | 323 | text (``str``): 324 | Message text 325 | 326 | custom_id (``str``, *optional*): 327 | Its ID for the message, can be any ``str`` 328 | 329 | image (``str``, *optional*): 330 | Attach image to message. This should 331 | be the URL path on the server 332 | 333 | Returns: 334 | :obj:`~characterai.types.chat2.BotAnswer` 335 | """ 336 | turn_key = { 337 | 'chat_id': chat_id 338 | } 339 | 340 | if custom_id != None: 341 | turn_key['turn_id'] = custom_id 342 | 343 | message = { 344 | 'command': 'create_and_generate_turn', 345 | 'payload': { 346 | 'character_id': char, 347 | 'turn': { 348 | 'turn_key': turn_key, 349 | 'author': author, 350 | 'candidates': [ 351 | { 352 | 'raw_content': text, 353 | 'tti_image_rel_path': image 354 | } 355 | ] 356 | } 357 | } 358 | } 359 | 360 | await self.ws.send(json.dumps(message)) 361 | 362 | while True: 363 | response = json.loads(await self.ws.recv()) 364 | 365 | try: 366 | turn = response['turn'] 367 | except: 368 | raise ServerError(response['comment']) 369 | 370 | if not turn['author']['author_id'].isdigit(): 371 | try: 372 | turn['candidates'][0]['is_final'] 373 | except: ... 374 | else: 375 | return chat2.BotAnswer.model_validate( 376 | turn 377 | ) 378 | 379 | async def edit_message( 380 | self, chat_id: str, message_id: str, 381 | text: str, *, token: str = None 382 | ): 383 | """Edit the message text 384 | 385 | EXAMPLE:: 386 | 387 | await client.edit_message('CHAT_ID', 'MSG_ID', 'TEXT') 388 | 389 | Args: 390 | chat_id (``str``): 391 | Chat ID 392 | 393 | message_id (``str``): 394 | Message ID 395 | 396 | text (``str``): 397 | New message text 398 | 399 | Returns: 400 | :obj:`~characterai.types.chat2.BotAnswer` 401 | """ 402 | await self.ws.send(json.dumps({ 403 | 'command':'edit_turn_candidate', 404 | 'payload': { 405 | 'turn_key': { 406 | 'chat_id': chat_id, 407 | 'turn_id': message_id 408 | }, 409 | 'new_candidate_raw_content': text 410 | } 411 | })) 412 | 413 | response = json.loads(await self.ws.recv()) 414 | 415 | try: response['turn'] 416 | except KeyError: 417 | raise ServerError(response['comment']) 418 | else: 419 | return chat2.BotAnswer.model_validate( 420 | response['turn'] 421 | ) 422 | 423 | class WSConnect(ChatV2): 424 | def __init__( 425 | self, token: str = None, 426 | *, start: bool = True 427 | ): 428 | if not start: 429 | self.token = token 430 | 431 | async def __call__( 432 | self, token: str = None, 433 | *, start: bool = True 434 | ): 435 | self.token = token or self.token 436 | 437 | if not start: 438 | return None 439 | 440 | return await self.__aenter__(self.token) 441 | 442 | async def __aenter__( 443 | self, token: str = None 444 | ): 445 | cookie = f'HTTP_AUTHORIZATION="Token {self.token}"' 446 | try: 447 | self.ws = await websockets.connect( 448 | 'wss://neo.character.ai/ws/', 449 | extra_headers={ 450 | 'Cookie': cookie 451 | } 452 | ) 453 | except exceptions.InvalidStatusCode as e: 454 | if e.status_code == 403: 455 | raise ServerError('Wrong token') 456 | 457 | return self 458 | 459 | async def __aexit__(self, *args): 460 | await self.close() 461 | 462 | async def close(self): 463 | return await self.ws.close() --------------------------------------------------------------------------------