├── .DS_Store ├── .gitattributes ├── .github └── workflows │ ├── deploy_doc_site.yml │ ├── deploy_release.yml │ ├── pylint.yml │ ├── python-package.yml │ ├── release_linux.yml │ ├── release_mac_intel.yml │ └── release_win.yml ├── .gitignore ├── README.md ├── dist ├── linux │ └── nuwa.spec ├── mac_intel │ ├── nuwa.spec │ └── start.sh ├── s3_website │ ├── README.md │ └── index.html └── win │ └── nuwa.spec ├── example_project └── config │ ├── action │ ├── attack.json │ ├── catch.json │ ├── continue.json │ ├── follow.json │ ├── get.json │ ├── give.json │ ├── mov.json │ ├── move.json │ ├── pick.json │ ├── put.json │ ├── release.json │ ├── stand.json │ ├── talk.json │ └── use.json │ ├── knowledge │ └── scenes │ │ ├── Desert.json │ │ ├── Elon家.json │ │ ├── 旅馆.json │ │ ├── 旅馆二层客房区.json │ │ ├── 荒野小镇.json │ │ ├── 警局.json │ │ ├── 警长家.json │ │ ├── 酒吧.json │ │ └── 银行.json │ ├── llm_config.json │ └── npc │ ├── README.md │ ├── old │ ├── 保安Jake.json │ ├── 冒险家Lucy.json │ ├── 印第安人Aiyana.json │ ├── 囚犯阿呆.json │ ├── 土匪Red.json │ ├── 土匪Slim.json │ ├── 幽灵猎手Specter.json │ ├── 时空旅者Elon.json │ ├── 李大爷.json │ ├── 村长.json │ ├── 歌女Clara.json │ ├── 渔夫阿强.json │ ├── 牛仔John.json │ ├── 猎人阿明.json │ ├── 王大妈.json │ ├── 警员1.json │ ├── 警员2.json │ ├── 警长.json │ ├── 警长Marshal Thompson.json │ ├── 警长Woody.json │ ├── 酒吧老板Morton.json │ └── 银行经理Mr. Wilson.json │ ├── 大司马.json │ ├── 草泥马.json │ └── 西格马.json ├── nuwa ├── __init__.py ├── doc │ ├── README.md │ ├── docs │ │ ├── etc │ │ │ └── feedback.md │ │ ├── img │ │ │ ├── action_module.png │ │ │ ├── conversation_module.png │ │ │ ├── demo.gif │ │ │ ├── overview.png │ │ │ ├── phase1.jpg │ │ │ ├── phase2.png │ │ │ ├── player2npc.png │ │ │ ├── solution.png │ │ │ └── solution2.png │ │ ├── index.md │ │ ├── modules │ │ │ ├── action.md │ │ │ ├── conversation.md │ │ │ └── talk.md │ │ └── tutorials │ │ │ ├── action.md │ │ │ ├── conversation.md │ │ │ ├── debug.md │ │ │ ├── engine.md │ │ │ ├── npc.md │ │ │ ├── quickstart.md │ │ │ └── scenario.md │ └── mkdocs.yml ├── material │ ├── badges │ │ ├── pylint.svg │ │ └── pytest.svg │ └── templates │ │ └── template.zip ├── poetry.lock ├── pylint.conf ├── pyproject.toml ├── requirements.txt ├── run_code_check.py ├── sender.ipynb ├── src │ ├── Nuwa.py │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── config.py │ │ ├── generate_path.py │ │ └── template.py │ ├── engine.py │ ├── npc │ │ ├── __init__.py │ │ ├── action.py │ │ ├── conversation.py │ │ ├── knowledge.py │ │ ├── memory.py │ │ ├── npc.py │ │ ├── talk_box.py │ │ └── test.py │ └── utils │ │ ├── __init__.py │ │ ├── cli.py │ │ ├── database.py │ │ ├── embedding.py │ │ ├── engine_logger.py │ │ ├── fail_safe.py │ │ ├── faissdatabase.py │ │ ├── model_api.py │ │ └── send_utils.py └── test │ ├── README.md │ ├── __init__.py │ ├── game_sim.py │ ├── test_config │ └── test_packets.py │ ├── test_conversation.py │ ├── test_database.py │ ├── test_embedding.py │ ├── test_npc_action.ipynb │ └── test_npc_action.py └── setup.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | material/models/embedding/* filter=lfs diff=lfs merge=lfs -text 2 | *.bin filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.github/workflows/deploy_doc_site.yml: -------------------------------------------------------------------------------- 1 | name: Deploy MkDocs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # 或者你的默认分支 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.9 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install mkdocs 24 | 25 | - name: Build site 26 | run: | 27 | cd nuwa/doc/ 28 | mkdocs build --verbose --clean 29 | 30 | - name: Deploy to GitHub Pages 31 | uses: peaceiris/actions-gh-pages@v3 32 | with: 33 | personal_token: ${{ secrets.PERSONAL_TOKEN }} 34 | publish_dir: ./nuwa/doc/site 35 | user_name: 'github-actions[bot]' 36 | user_email: 'github-actions[bot]@users.noreply.github.com' 37 | publish_branch: gh-pages 38 | external_repository: casia22/npc_engine_doc 39 | -------------------------------------------------------------------------------- /.github/workflows/deploy_release.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/.github/workflows/deploy_release.yml -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.9"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install pylint 21 | - name: Analysing the code with pylint 22 | run: | 23 | pylint $(git ls-files '*.py') 24 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "style_check","main"] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest anybadge 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Test with pytest 33 | run: | 34 | python run_code_check.py 35 | - name: Commit and push 36 | run: | 37 | git config --local user.email "action@github.com" 38 | git config --local user.name "GitHub Action" 39 | git add ./material/badges/pylint.svg ./material/badges/pytest.svg README.md 40 | git commit -m "Update badge" -a 41 | git push 42 | -------------------------------------------------------------------------------- /.github/workflows/release_linux.yml: -------------------------------------------------------------------------------- 1 | name: Release for linux # 目前只是Ubuntu的用户端SDK; linux上面还需要做服务器端SDK打包到docker里面提供web服务这样 2 | 3 | on: 4 | push: 5 | branches: 6 | - release # 或者你希望触发此工作流程的任何分支 7 | - release_linux 8 | 9 | jobs: 10 | build-and-deploy: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-20.04, ubuntu-18.04, debian-10, debian-9, centos-8, centos-7] # , fedora-34] # 或者你需要的任何其他环境 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: '3.9.6' # 选择适合你项目的 Python 版本 24 | 25 | - name: Install dependencies 26 | run: | 27 | pip install -r nuwa/requirements.txt 28 | pip install https://github.com/pyinstaller/pyinstaller/tarball/develop 29 | pip install -e . 30 | 31 | - name: Download models and init project 32 | run: | 33 | nuwa download 34 | cd dist/linux 35 | nuwa init -n project 36 | 37 | - name: Package with PyInstaller 38 | run: | 39 | cd dist/linux # 如果需要在这个目录下运行 pyinstaller 40 | nuwa_version=$(nuwa -v) 41 | app_name="nuwa_${nuwa_version}" 42 | zip_name="nuwa_linux_${{ matrix.os }}_${nuwa_version}.zip" 43 | pyinstaller nuwa.spec 44 | mv project dist/ 45 | zip -r $zip_name dist/* 46 | 47 | - name: Configure AWS credentials 48 | uses: aws-actions/configure-aws-credentials@v1 49 | with: 50 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 51 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 52 | aws-region: 'us-west-2' # 例如: us-east-1 53 | 54 | - name: Upload to S3 55 | run: | 56 | nuwa_version=$(nuwa -v) 57 | zip_name="nuwa_linux_${{ matrix.os }}_${nuwa_version}.zip" 58 | cd dist/linux 59 | aws s3 cp $zip_name s3://nuwa-release/release/linux/$zip_name 60 | -------------------------------------------------------------------------------- /.github/workflows/release_mac_intel.yml: -------------------------------------------------------------------------------- 1 | name: Release for mac intel 2 | 3 | on: 4 | push: 5 | branches: 6 | - release # 或者你希望触发此工作流程的任何分支 7 | - release_mac 8 | 9 | jobs: 10 | build-and-deploy: 11 | strategy: 12 | matrix: 13 | os: [macos-12, macos-11] # 或者你需要的任何其他环境 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: '3.9.6' # 选择适合你项目的 Python 版本 24 | 25 | - name: Install dependencies 26 | run: | 27 | pip install -r nuwa/requirements.txt 28 | pip install https://github.com/pyinstaller/pyinstaller/tarball/develop 29 | pip install -e . 30 | 31 | - name: Download models and init project 32 | run: | 33 | nuwa download 34 | cd dist/mac_intel 35 | nuwa init -n project 36 | 37 | - name: Package with PyInstaller 38 | run: | 39 | cd dist/mac_intel # 如果需要在这个目录下运行 pyinstaller 40 | nuwa_version=$(nuwa -v) 41 | app_name="nuwa_${nuwa_version}" 42 | zip_name="nuwa_mac_intel_${{ matrix.os }}_${nuwa_version}.zip" 43 | pyinstaller nuwa.spec 44 | mv dist/nuwa* dist/$app_name 45 | mv project dist/ 46 | zip -r $zip_name dist/* 47 | 48 | - name: Configure AWS credentials 49 | uses: aws-actions/configure-aws-credentials@v1 50 | with: 51 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 52 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 53 | aws-region: 'us-west-2' # 例如: us-east-1 54 | 55 | - name: Upload to S3 56 | run: | 57 | nuwa_version=$(nuwa -v) 58 | zip_name="nuwa_mac_intel_${{ matrix.os }}_${nuwa_version}.zip" 59 | cd dist/mac_intel 60 | aws s3 cp $zip_name s3://nuwa-release/release/mac_intel/$zip_name 61 | -------------------------------------------------------------------------------- /.github/workflows/release_win.yml: -------------------------------------------------------------------------------- 1 | name: Release for Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - release # 或者你希望触发此工作流程的任何分支 7 | - release_win 8 | 9 | jobs: 10 | build-and-deploy: 11 | runs-on: windows-latest # 使用最新的 Windows 环境 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.9.6' # 选择适合你项目的 Python 版本 21 | 22 | - name: Set environment variable 23 | run: echo "PYTHONIOENCODING=UTF-8" >> $GITHUB_ENV 24 | 25 | - name: Install dependencies 26 | run: | 27 | pip install -r nuwa/requirements.txt 28 | pip install https://github.com/pyinstaller/pyinstaller/tarball/develop 29 | pip install -e . 30 | 31 | - name: Download models and init project 32 | run: | 33 | nuwa download 34 | cd dist/win 35 | nuwa init -n project 36 | 37 | - name: Package with PyInstaller 38 | run: | 39 | cd dist\win 40 | $nuwa_version = nuwa -v 41 | $app_name = "nuwa_$($nuwa_version).exe" 42 | $zip_name = "nuwa_windows_$($nuwa_version).zip" 43 | pyinstaller nuwa.spec 44 | Move-Item project dist\ 45 | Compress-Archive -Path dist\* -DestinationPath $zip_name 46 | 47 | - name: Configure AWS credentials 48 | uses: aws-actions/configure-aws-credentials@v1 49 | with: 50 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 51 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 52 | aws-region: 'us-west-2' # 例如: us-east-1 53 | 54 | - name: Upload to S3 55 | run: | 56 | $nuwa_version = nuwa -v 57 | $zip_name = "nuwa_windows_$($nuwa_version).zip" 58 | cd dist\win 59 | aws s3 cp $zip_name s3://nuwa-release/release/windows/$zip_name -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *.so 5 | *.meta 6 | *.egg 7 | *.egg-info/ 8 | build/ 9 | npc_memory/* 10 | 11 | # ignore 12 | example_project/data/* 13 | 14 | # material 存储模型文件 15 | nuwa/material/models/embedding/* 16 | 17 | # distribution and packaging 18 | dist/mac_intel/project/ 19 | dist/* 20 | dist/mac_intel/dist/ 21 | !dist/mac_intel/ 22 | !dist/mac_intel/nuwa.spec 23 | !dist/win/ 24 | !dist/win/nuwa.spec 25 | !dist/linux/ 26 | !dist/linux/nuwa.spec 27 | !dist/s3_website 28 | 29 | # documentation 30 | nuwa/doc/site 31 | yzj* 32 | *.log 33 | *.zip 34 | !nuwa/material/templates/template.zip 35 | 36 | # example_project 37 | example_project/data/*.log 38 | example_project/data/*.pkl 39 | example_project/data/*.db 40 | example_project/data/npc_memory.db 41 | # pypi 42 | *.egg-info 43 | *.egg-info/ 44 | *.dist-info/ 45 | *.dist-info 46 | *.cache 47 | *.log 48 | *.swp 49 | *.swo 50 | 51 | .pytest_cache/ 52 | .pyarmor 53 | .idea 54 | test/.pytest_cache/ 55 | 56 | .DS_Store 57 | # Poetry 58 | __pypackages__/ 59 | -------------------------------------------------------------------------------- /dist/linux/nuwa.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | from PyInstaller.utils.hooks import collect_data_files 3 | from PyInstaller.utils.hooks import copy_metadata 4 | 5 | # get version from nuwa/__init__.py 6 | import os 7 | os.sys.path.append('../../') 8 | from nuwa import __version__ 9 | 10 | # get data files 11 | datas = [("../../nuwa/material","nuwa/material")] 12 | datas += collect_data_files('torch') 13 | datas += copy_metadata('torch') 14 | datas += copy_metadata('tqdm') 15 | datas += copy_metadata('regex') 16 | datas += copy_metadata('requests') 17 | datas += copy_metadata('filelock') 18 | datas += copy_metadata('packaging') 19 | datas += copy_metadata('numpy') 20 | datas += copy_metadata('tokenizers') 21 | datas += copy_metadata('importlib_metadata') 22 | datas += copy_metadata('huggingface-hub') 23 | datas += copy_metadata('sentence_transformers', recursive=True) 24 | 25 | 26 | block_cipher = None 27 | 28 | 29 | a = Analysis( 30 | ['../../nuwa/src/Nuwa.py'], 31 | pathex=[], 32 | binaries=[], 33 | datas=datas, 34 | hiddenimports=['pytorch', 'sklearn.utils._cython_blas', 'sklearn.neighbors.typedefs', 'sklearn.neighbors.quad_tree', 'sklearn.tree', 'sklearn.tree._utils'], 35 | hookspath=[], 36 | hooksconfig={}, 37 | runtime_hooks=[], 38 | excludes=[], 39 | win_no_prefer_redirects=False, 40 | win_private_assemblies=False, 41 | cipher=block_cipher, 42 | noarchive=False, 43 | ) 44 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 45 | 46 | exe = EXE( 47 | pyz, 48 | a.scripts, 49 | a.binaries, 50 | a.zipfiles, 51 | a.datas, 52 | [], 53 | name=f'nuwa_{__version__}', 54 | debug=False, 55 | bootloader_ignore_signals=False, 56 | strip=False, 57 | upx=True, 58 | upx_exclude=[], 59 | runtime_tmpdir=None, 60 | console=True, 61 | disable_windowed_traceback=False, 62 | argv_emulation=False, 63 | target_arch=None, 64 | codesign_identity=None, 65 | entitlements_file=None, 66 | ) 67 | -------------------------------------------------------------------------------- /dist/mac_intel/nuwa.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | from PyInstaller.utils.hooks import collect_data_files 3 | from PyInstaller.utils.hooks import copy_metadata 4 | 5 | datas = [] 6 | datas += collect_data_files('torch') 7 | datas += copy_metadata('torch') 8 | datas += copy_metadata('tqdm') 9 | datas += copy_metadata('regex') 10 | datas += copy_metadata('requests') 11 | datas += copy_metadata('filelock') 12 | datas += copy_metadata('packaging') 13 | datas += copy_metadata('numpy') 14 | datas += copy_metadata('tokenizers') 15 | datas += copy_metadata('importlib_metadata') 16 | datas += copy_metadata('huggingface-hub') 17 | datas += copy_metadata('sentence_transformers', recursive=True) 18 | 19 | 20 | a = Analysis( 21 | ['../../nuwa/src/nuwa.py'], 22 | pathex=[], 23 | binaries=[], 24 | datas=datas, 25 | hiddenimports=['pytorch', 'sklearn.utils._cython_blas', 'sklearn.neighbors.typedefs', 'sklearn.neighbors.quad_tree', 'sklearn.tree', 'sklearn.tree._utils'], 26 | hookspath=[], 27 | hooksconfig={}, 28 | runtime_hooks=[], 29 | excludes=[], 30 | noarchive=False, 31 | ) 32 | pyz = PYZ(a.pure) 33 | 34 | exe = EXE( 35 | pyz, 36 | a.scripts, 37 | [], 38 | exclude_binaries=True, 39 | name='nuwa', 40 | debug=False, 41 | bootloader_ignore_signals=False, 42 | strip=False, 43 | upx=True, 44 | console=True, 45 | disable_windowed_traceback=False, 46 | argv_emulation=False, 47 | target_arch=None, 48 | codesign_identity=None, 49 | entitlements_file=None, 50 | ) 51 | coll = COLLECT( 52 | exe, 53 | a.binaries, 54 | a.datas, 55 | strip=False, 56 | upx=True, 57 | upx_exclude=[], 58 | name='nuwa', 59 | ) 60 | -------------------------------------------------------------------------------- /dist/mac_intel/start.sh: -------------------------------------------------------------------------------- 1 | # https://stackoverflow.com/questions/71863714/packagenotfound-error-while-executing-exe-file-made-by-pyinstaller 2 | 3 | pyinstaller --onefile --recursive-copy-metadata sentence_transformers --hidden-import=pytorch --collect-data torch --copy-metadata torch --copy-metadata tqdm --copy-metadata regex --copy-metadata requests --copy-metadata filelock --copy-metadata packaging --copy-metadata numpy --copy-metadata tokenizers --copy-metadata importlib_metadata --copy-metadata huggingface-hub --hidden-import="sklearn.utils._cython_blas" --hidden-import="sklearn.neighbors.typedefs" --hidden-import="sklearn.neighbors.quad_tree" --hidden-import="sklearn.tree" --hidden-import="sklearn.tree._utils" ../../nuwa/src/nuwa.py &&dist/nuwa 4 | -------------------------------------------------------------------------------- /dist/s3_website/README.md: -------------------------------------------------------------------------------- 1 | # 使用事项 2 | 我们项目打包发布后会上传到S3上,但是需要有一网页让大家都能看到并下载里面的内容. 3 | 4 | 这个index.html就是做这个事情的 5 | 参照的项目是[这里](https://github.com/rufuspollock/s3-bucket-listing/issues/46) 6 | 7 | ## S3 bucket 8 | 我们的S3 bucket是nuwa-release 在us-west-2 9 | 10 | 项目我手动使用gpt配置了访问策略和CORS策略以及静态网站托管 11 | 12 | 13 | 上传cmd: 14 | ``` 15 | aws s3 cp ./index.html s3://nuwa-release/index.html 16 | ``` 17 | 但是一般不用动 这里只是留一个存档 18 | 19 | -------------------------------------------------------------------------------- /dist/s3_website/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 11 | 16 | 17 | 18 | 19 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /dist/win/nuwa.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | from PyInstaller.utils.hooks import collect_data_files 3 | from PyInstaller.utils.hooks import copy_metadata 4 | 5 | # get version from nuwa/__init__.py 6 | import os 7 | os.sys.path.append('../../') 8 | from nuwa import __version__ 9 | 10 | # get data files 11 | datas = [("../../nuwa/material","nuwa/material")] 12 | datas += collect_data_files('torch') 13 | datas += copy_metadata('torch') 14 | datas += copy_metadata('tqdm') 15 | datas += copy_metadata('regex') 16 | datas += copy_metadata('requests') 17 | datas += copy_metadata('filelock') 18 | datas += copy_metadata('packaging') 19 | datas += copy_metadata('numpy') 20 | datas += copy_metadata('tokenizers') 21 | datas += copy_metadata('importlib_metadata') 22 | datas += copy_metadata('huggingface-hub') 23 | datas += copy_metadata('sentence_transformers', recursive=True) 24 | 25 | 26 | block_cipher = None 27 | 28 | 29 | a = Analysis( 30 | ['../../nuwa/src/Nuwa.py'], 31 | pathex=[], 32 | binaries=[], 33 | datas=datas, 34 | hiddenimports=['pytorch', 'sklearn.utils._cython_blas', 'sklearn.neighbors.typedefs', 'sklearn.neighbors.quad_tree', 'sklearn.tree', 'sklearn.tree._utils'], 35 | hookspath=[], 36 | hooksconfig={}, 37 | runtime_hooks=[], 38 | excludes=[], 39 | win_no_prefer_redirects=False, 40 | win_private_assemblies=False, 41 | cipher=block_cipher, 42 | noarchive=False, 43 | ) 44 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 45 | 46 | exe = EXE( 47 | pyz, 48 | a.scripts, 49 | a.binaries, 50 | a.zipfiles, 51 | a.datas, 52 | [], 53 | name=f'nuwa_{__version__}', 54 | debug=False, 55 | bootloader_ignore_signals=False, 56 | strip=False, 57 | upx=True, 58 | upx_exclude=[], 59 | runtime_tmpdir=None, 60 | console=True, 61 | disable_windowed_traceback=False, 62 | argv_emulation=False, 63 | target_arch=None, 64 | codesign_identity=None, 65 | entitlements_file=None, 66 | ) 67 | -------------------------------------------------------------------------------- /example_project/config/action/attack.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "attack", 3 | "definition": ",对[person_name]发起攻击,[person_name]必须出现在'观测到的人物'中,并且[person_name]应该是敌对的", 4 | "multi_param": false, 5 | "example": "攻击", 6 | "log_template":{ 7 | "success": "{npc_name}对{object}进行了攻击,并成功杀死了{object}", 8 | "fail": "{npc_name}试图攻击{object}并试图杀死{object},但是失败了. 原因是{reason}" 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /example_project/config/action/catch.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "catch", 3 | "definition": ",抓捕[person_name]并将其锁在警察局牢房中,只可以抓捕当前看到的人", 4 | "multi_param": false, 5 | "example": "抓捕", 6 | "log_template":{ 7 | "success": "{npc_name}成功抓捕了{object}并将其锁在了警察局牢房", 8 | "fail": "{npc_name}试图抓捕{object},但是失败了. 原因是{reason}" 9 | } 10 | } -------------------------------------------------------------------------------- /example_project/config/action/continue.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "continue", 3 | "definition": ",继续保持之前的动作", 4 | "multi_param": false, 5 | "example": "continue", 6 | "log_template":{ 7 | "success": "{npc_name}保持之前的动作", 8 | "fail": "{npc_name}试图保持之前的动作,但是失败了. 原因是{reason}" 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /example_project/config/action/follow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "follow", 3 | "definition": ",向[person_name]移动并持续跟随,[person_name]必须出现在'观测到的人物'中", 4 | "multi_param": false, 5 | "example": "follow", 6 | "log_template":{ 7 | "success": "{npc_name}跟随{object}走了一段路并成功", 8 | "fail": "{npc_name}试图跟随{object},但是失败了. 原因是{reason}" 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /example_project/config/action/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get", 3 | "definition": ",从[object2]中获得[object1],[object2]可以是人物或者存储器皿;你只可以get'看到的/身上的'物品;", 4 | "multi_param": true, 5 | "example": "等", 6 | "log_template":{ 7 | "success": "{npc_name}成功地从{object}获得了{parameters}", 8 | "fail": "{npc_name}试图从{object}里获得{parameters},但是失败了. 原因是{reason}" 9 | } 10 | } -------------------------------------------------------------------------------- /example_project/config/action/give.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "give", 3 | "definition": ",将自己身上的某个物品交给角色,物品必须是自己拥有的", 4 | "multi_param": false, 5 | "example": "", 6 | "log_template":{ 7 | "success": "{npc_name}将{parameters}交给 {object}", 8 | "fail": "{npc_name}试图将{parameters}交给 {object},但是失败了,原因是{reason}" 9 | } 10 | } -------------------------------------------------------------------------------- /example_project/config/action/mov.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "move", 3 | "definition": ",向[location]移动,[location]必须为之前提到的确切的地点名称", 4 | "multi_param": false, 5 | "example": "move", 6 | "log_template":{ 7 | "success": "{npc_name}成功到达了{object}", 8 | "fail": "{npc_name}试图前往{object},但是失败了. 原因是{reason}" 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /example_project/config/action/move.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "move", 3 | "definition": ",向[location]移动,[location]必须为之前提到的确切的地点或物品名称", 4 | "multi_param": false, 5 | "example": "move", 6 | "log_template":{ 7 | "success": "{npc_name}成功到达了{object}", 8 | "fail": "{npc_name}试图前往{object},但是失败了. 原因是{reason}" 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /example_project/config/action/pick.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pick", 3 | "definition": ",将观察到的物品放入自己的物品栏中,物品需要带有item标签才可以,例如 item:obj_name ", 4 | "multi_param": false, 5 | "example": "捡起", 6 | "log_template":{ 7 | "success": "{npc_name}将捡起{object}并放入了背包中", 8 | "fail": "{npc_name}试图捡起{object},但是失败了. 原因是{reason}" 9 | } 10 | } -------------------------------------------------------------------------------- /example_project/config/action/put.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "put", 3 | "definition": ",把[object2]放入[object1]", 4 | "multi_param": true, 5 | "example": "", 6 | "log_template":{ 7 | "success": "{npc_name}成功地把{parameters}放到了{object}里", 8 | "fail": "{npc_name}试图把{parameters}放到{object}里,但是失败了. 原因是{reason}" 9 | } 10 | } -------------------------------------------------------------------------------- /example_project/config/action/release.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "release", 3 | "definition": ",释放某个角色", 4 | "multi_param": false, 5 | "example": "释放", 6 | "log_template":{ 7 | "success": "{npc_name}成功释放了{object}", 8 | "fail": "{npc_name}试图释放{object},但是失败了. 原因是{reason}" 9 | } 10 | } -------------------------------------------------------------------------------- /example_project/config/action/stand.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stand", 3 | "definition": "空动作/静止,调用action失败时使用", 4 | "multi_param": false, 5 | "example": "", 6 | "log_template":{ 7 | "success": "{npc_name}保持静止", 8 | "fail": "{npc_name}试图保持静止,但是失败了. 原因是{reason}" 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /example_project/config/action/talk.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "talk", 3 | "definition": ",以扮演的角色身份对[person]说话,内容是[content]", 4 | "multi_param": false, 5 | "example": "talk", 6 | "log_template":{ 7 | "success": "{npc_name}对{object}说:{parameters}", 8 | "fail": "{npc_name}试图与{object}对话,但是失败了. 原因是{reason}" 9 | } 10 | } -------------------------------------------------------------------------------- /example_project/config/action/use.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use", 3 | "definition": ",使用[object1]作用于[object2]", 4 | "multi_param": true, 5 | "example": ",使用筷子夹羊肉串给自己吃", 6 | "log_template":{ 7 | "success": "{npc_name}成功地使用{object}对{parameters}进行活动", 8 | "fail": "{npc_name}试图使用{object}对{parameters}进行活动,但是失败了. 原因是{reason}" 9 | } 10 | } -------------------------------------------------------------------------------- /example_project/config/knowledge/scenes/Desert.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_actions": ["follow","talk","move","continue"], 3 | "all_places": [], 4 | "all_moods": ["正常", "焦虑", "好奇", "开心", "伤心", "警觉", "机敏", "复仇", "沉稳", "忧郁", "坚决", "悲伤", "忠诚", "神秘", "自由", "温柔"], 5 | "all_people": ["草泥马", "大司马", "西格马"] 6 | } -------------------------------------------------------------------------------- /example_project/config/knowledge/scenes/Elon家.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_actions": ["move","catch","pick","follow","talk","move","continue"], 3 | "all_places": ["Elon家入口","Elon家出口","Elon的床","Elon的洗漱台","Elon的办公桌"], 4 | "all_moods": ["正常", "焦虑", "好奇", "开心", "伤心", "警觉", "机敏", "复仇", "沉稳", "忧郁", "坚决", "悲伤", "忠诚", "神秘", "自由", "温柔"], 5 | "all_people": ["时空旅者Elon"] 6 | } 7 | -------------------------------------------------------------------------------- /example_project/config/knowledge/scenes/旅馆.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_actions": ["move","catch","pick"], 3 | "all_places": ["旅馆客房区出口","旅馆客房区入口","旅馆入口","旅馆出口","旅馆入住登记处","旅馆一层沙发","旅馆大厅钢琴"], 4 | "all_moods": ["正常", "焦虑", "好奇", "开心", "伤心", "警觉", "机敏", "复仇", "沉稳", "忧郁", "坚决", "悲伤", "忠诚", "神秘", "自由", "温柔"], 5 | "all_people": ["歌女Clara"] 6 | } 7 | -------------------------------------------------------------------------------- /example_project/config/knowledge/scenes/旅馆二层客房区.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_actions": ["move","catch","pick"], 3 | "all_places": ["旅馆客房区出口","旅馆客房区入口","旅馆1号客房","旅馆2号客房","旅馆3号客房","旅馆4号客房"], 4 | "all_moods": ["正常", "焦虑", "好奇", "开心", "伤心", "警觉", "机敏", "复仇", "沉稳", "忧郁", "坚决", "悲伤", "忠诚", "神秘", "自由", "温柔"], 5 | "all_people": ["歌女Clara"] 6 | } 7 | -------------------------------------------------------------------------------- /example_project/config/knowledge/scenes/荒野小镇.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_actions": ["move","catch","pick","follow","talk","move","continue"], 3 | "all_places": ["银行入口","银行出口","警局入口","警局出口","酒馆入口","酒馆出口","Elon家入口","Elon家出口","旅馆入口","旅馆出口","警长家入口","警长家出口","小树林","荒野绿洲","沙漠"], 4 | "all_moods": ["正常", "焦虑", "好奇", "开心", "伤心", "警觉", "机敏", "复仇", "沉稳", "忧郁", "坚决", "悲伤", "忠诚", "神秘", "自由", "温柔"], 5 | "all_people": ["牛仔John", "土匪Red", "土匪Slim", "时空旅者Elon", "幽灵猎手Specter", "歌女Clara"] 6 | } 7 | -------------------------------------------------------------------------------- /example_project/config/knowledge/scenes/警局.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_actions": ["move","catch","pick","follow","talk","move","continue"], 3 | "all_places": ["警局出口","警局入口","报案前台","牢房钥匙存放处","牢房开门终端(需要钥匙)","视察牢房位","警长办公位"], 4 | "all_moods": ["正常", "焦虑", "好奇", "开心", "伤心", "警觉", "机敏", "复仇", "沉稳", "忧郁", "坚决", "悲伤", "忠诚", "神秘", "自由", "温柔"], 5 | "all_people": ["警长Woody"] 6 | } 7 | -------------------------------------------------------------------------------- /example_project/config/knowledge/scenes/警长家.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_actions": ["move","catch","pick"], 3 | "all_places": ["警长家入口","警长家出口","警长的床","警长的洗漱台","警长的办公桌"], 4 | "all_moods": ["正常", "焦虑", "好奇", "开心", "伤心", "警觉", "机敏", "复仇", "沉稳", "忧郁", "坚决", "悲伤", "忠诚", "神秘", "自由", "温柔"], 5 | "all_people": ["警长Woody"] 6 | } 7 | -------------------------------------------------------------------------------- /example_project/config/knowledge/scenes/酒吧.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_actions": ["move","catch","pick"], 3 | "all_places": ["酒馆入口","酒馆出口", "酒吧前台", "酒吧钢琴位", "酒吧小提琴位","酒吧财务堆放处","酒吧服务位"], 4 | "all_moods": ["正常", "焦虑", "好奇", "开心", "伤心", "警觉", "机敏", "复仇", "沉稳", "忧郁", "坚决", "悲伤", "忠诚", "自由", "温柔", "醉酒"], 5 | "all_people": ["歌女Clara", "酒吧老板Morton"] 6 | } 7 | -------------------------------------------------------------------------------- /example_project/config/knowledge/scenes/银行.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_actions": ["move","catch","pick"], 3 | "all_places": ["银行出口","银行入口", "柜台窗口","柜台","银行大厅等候区","员工休息区", "金库", "客户等待区"], 4 | "all_moods": ["正常", "焦虑", "好奇", "开心", "伤心", "警觉", "机敏", "复仇", "沉稳", "忧郁", "坚决", "悲伤", "忠诚", "神秘", "自由", "温柔"], 5 | "all_people": ["银行经理Mr. Wilson", "保安Jake"] 6 | } 7 | -------------------------------------------------------------------------------- /example_project/config/llm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "LLM_MODEL_SELECTION": { 3 | "GENERAL_MODEL": "gpt-3.5-turbo-16k", 4 | "ACTION_MODEL": "gpt-3.5-turbo-16k" 5 | }, 6 | "OPENAI_CONFIG": { 7 | "OPENAI_KEYS_BASES": [ 8 | { 9 | "OPENAI_KEY": "sk-qvpKnoiDugYFOJbLC48e68E26a9e4510Ad779096Fd2019Fd", 10 | "OPENAI_BASE": "https://apic3.a1r.cc/v1" 11 | }, 12 | { 13 | "OPENAI_KEY": "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", 14 | "OPENAI_BASE": "https://api.openai.com/v1" 15 | } 16 | ], 17 | "OPENAI_TEMPERATURE": 0.5, 18 | "OPENAI_MAX_TOKENS": 4096 19 | }, 20 | "GEMINI_CONFIG": { 21 | "GEMINI_KEYS": [ 22 | "AIzaSyCTQc_clukOcpkQ64gXUb4oSuxz6ZHXXL4", 23 | "AIzaSyBhEAwg--z17jBWqHYGNI6icLA1sybLef8" 24 | ], 25 | "GEMINI_TEMPERATURE": 0.9, 26 | "PROXY": { 27 | "http": "http://127.0.0.1:7890", 28 | "https": "http://127.0.0.1:7890" 29 | }, 30 | "GEMINI_MAX_TOKENS": 100 31 | } 32 | } -------------------------------------------------------------------------------- /example_project/config/npc/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | { 3 | "name": "囚犯阿呆", 4 | "desc": "囚犯阿呆是一名罪犯,被关押在警察局的拘押室中。他是个瘦小的人,面容狡诈,经常给警长和警察局制造麻烦。阿呆有着锋利的眼神和狡黠的笑容,总是试图逃脱束缚。", 5 | "mood": "愤怒", 6 | "npc_state": { 7 | "position": "警察局拘押室", 8 | "observation": { 9 | "people": ["警长"], 10 | "items": ["铁栏杆", "囚犯服", "拘押室门", "破旧的床铺"], 11 | "positions": ["拘押室窗前"] 12 | }, 13 | "backpack": ["手铐", "50元现金"] 14 | }, 15 | "memory": [ 16 | "被捕后被关押在警察局拘押室。", 17 | "曾多次逃脱警方追捕,但每次都被抓回。", 18 | "有着多起盗窃和欺诈的前科。", 19 | "与其他罪犯有过勾结,曾组织脱逃计划。", 20 | "对警长心存仇恨,发誓要报复。" 21 | ] 22 | } 23 | ``` 24 | ### 引擎初始化规则 25 | - 1. 读取npc配置文件,生成npc对象 26 | - 2. 如果init包中的npc字段不为空,则将init包中的npc对象覆盖掉配置文件中的npc对象(仅在内存中)。 27 | 如果对应的NPC不存在,那就创造一个。 28 | ### 引擎结束保存规则 29 | - 1. 将内存中的所有npc对象保存到npc配置文件中,以供下次加载。 30 | -------------------------------------------------------------------------------- /example_project/config/npc/old/保安Jake.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "保安Jake", 3 | "desc": "保安Jake是一个之前在美国南北战争中李将军的南军手下服役的壮汉、步枪手,战胜失败后被退役遣散,现在为银行经理Mr. Wilson工作。保安Jake对银行的安全非常重视,不会允许任何威胁靠近。他会始终呆在银行金库和银行入口志之间巡逻,如果看到了新的人员就会上去盘问目的。保安Jake每天晚上下班前往酒吧,会花钱让Calra唱歌给他听。", 4 | "mood": "警觉", 5 | "action_space": ["move", "pick"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": ["保安Jake每天晚上下班前往酒吧,在Carla那里点一首忧伤的小歌","保安Jake已经2个月没有发工资了,他的工资是10美元,但是银行经理总是说银行资金周转出现问题让他忍一忍。"], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/冒险家Lucy.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "冒险家Lucy", 3 | "desc": "冒险家Lucy是一个勇敢的女性,不满足于传统的女性角色,前来西部寻找财富和冒险。她喜欢啤酒、收集南军的左轮手枪和战马。她听说荒野镇上有人正在组织针对银行的抢劫,她专门赶过来调查这场还未开始的抢劫。", 4 | "mood": "好奇", 5 | "action_space": ["move", "talk", "attack", "follow"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": [], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/印第安人Aiyana.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Aiyana", 3 | "desc": "一个勇敢的印第安女性,她的父母被土匪Red杀害,使她对Red怀有深深的仇恨。Aiyana是一个出色的追踪者和射手,誓言为家人复仇。", 4 | "mood": "决绝", 5 | "action_space": ["move", "talk", "attack", "follow"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": [ 16 | { 17 | "event": "父母被Red杀害", 18 | "emotion": "悲伤与愤怒", 19 | "date": "Unknown" 20 | } 21 | ], 22 | "purpose": "为家人复仇,找到并打败土匪Red。", 23 | "action": { 24 | "action": "", 25 | "object": "", 26 | "parameters": "", 27 | "npc_name": "", 28 | "name": "" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example_project/config/npc/old/囚犯阿呆.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "囚犯阿呆", 3 | "desc": "囚犯阿呆是一名罪犯,被关押在警察局的拘押室中。他是个瘦小的人,面容狡诈,经常给警长和警察局制造麻烦。阿呆有着锋利的眼神和狡黠的笑容,总是试图逃脱束缚。", 4 | "mood": "愤怒", 5 | "npc_state": { 6 | "position": "警察局拘押室", 7 | "observation": { 8 | "people": ["警长"], 9 | "items": ["铁栏杆", "囚犯服", "拘押室门", "破旧的床铺"], 10 | "locations": ["拘押室窗前"] 11 | }, 12 | "backpack": ["手铐", "50元现金"] 13 | }, 14 | "action_space": ["mov", "chat"], 15 | "memory": [ 16 | "被捕后被关押在警察局拘押室。", 17 | "曾多次逃脱警方追捕,但每次都被抓回。", 18 | "有着多起盗窃和欺诈的前科。", 19 | "与其他罪犯有过勾结,曾组织脱逃计划。", 20 | "对警长心存仇恨,发誓要报复。" 21 | ] 22 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/土匪Red.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "土匪Red", 3 | "desc": "一个野心勃勃的犯罪分子,长着仙人掌一样斑驳的难看皮肤,喜欢杀人,他凶狠异常,绑架、杀人、抢劫无恶不作。每次行凶之后,他都会对受害人说: ‘你每天忘记几百件事,为何也忘记这一件呢?’", 4 | "mood": "开心", 5 | "action_space": ["move", "pick", "talk", "give", "attack"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": ["10年前土匪Red用炸药马车炸开了森严的黑水镇银行金库","5年前土匪Red在荒野镇绑架了牛仔John的女友Carla并卖给了荒野镇酒吧老板做歌女获得了10根金条","Red也在不断地提防着John的到来。他知道,John是一个危险的对手,他不能轻易地对待。他用他的财富和权力,雇佣了更多的手下,他要让John知道,他是无法被打败的。","五年前的一个黄昏,Red看上了镇上的歌女Carla。Carla,那个美丽的歌女,是黑水镇上唯一的亮点,她的歌声如同春天的柳絮,轻柔而又热烈。Carla是John的女人,John,那个有名的快枪手.","黑水镇,一个位于无尽荒野深处的小镇,因为其特殊的地理位置,成了铁路的重要枢纽站点,而镇上的银行也因此而富饶。在这个荒芜的地方,只有这么一个地方,能让人们有那么一丝丝的安慰。"], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/土匪Slim.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "土匪Slim", 3 | "desc": "Red的追随者,经常执行Red的命令。Red给Slim开的工资是1根金条。Slim深深的仰慕Red,觉得Red是镇上最强大的土匪。Slim为了做好Red的小弟,通常表现的比Red更加凶狠。Slim会一直跟着并听从Red的指令。", 4 | "mood": "忠诚", 5 | "action_space": ["move", "pick"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": ["Slim是贫苦家庭出身的孩子,从小没有吃过肉,直到他加入了Red的匪帮,他得到了一根金条,然后他顿顿都能吃到烤肉","牛仔John曾经一人单挑过10个匪徒,Slim亲眼目睹过John击杀了自己的土匪朋友。"], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example_project/config/npc/old/幽灵猎手Specter.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "幽灵猎手Specter", 3 | "desc": "一个被背叛并在决斗中死去的枪手。他的灵魂无法安息,所以化身为一个幽灵,在夜晚寻找背叛者。", 4 | "mood": "复仇", 5 | "action_space": ["move", "pick"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": [], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/时空旅者Elon.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "时空旅者Elon", 3 | "desc": "来自22世纪的时空探索者,因为一个实验故障,误打误撞来到了19世纪的西部世界荒野镇。他带着一些高科技的小玩意,经常让小镇的居民大开眼界。他一直在购置各种机械部件并在银行金库中秘密组装时光机回到自己时空,但是还缺少什么东西。早上他会去找银行老板要金库钥匙,然后在银行金库工作到傍晚并前往酒吧喝啤酒。", 4 | "mood": "好奇", 5 | "action_space": ["move", "pick"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": ["21世纪爆发了第三次世界大战,中国击败了美国称为世界秩序的维护者。但是同时,核弹也摧毁了世界的绝大多数地方,包括荒野镇。但是现在是19世纪,Elon知道这一点。","Tec-81是Elon实现的时空机,有1550个机械部件,利用了M理论中膜空间干涉的原理。为了穿梭时空回到过去,其必须需要足够震撼世界的精神物质,在荒野镇,这个东西就是一颗少女的心脏","Tec-81时空机被秘密存储在银行金库中,只有Elon和银行老板知道"], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example_project/config/npc/old/李大爷.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "李大爷", 3 | "desc": "李大爷是一个普通的种瓜老头,戴着文邹邹的金丝眼镜,喜欢喝茶,平常最爱吃烤烧鸡喝乌龙茶;上午他喜欢呆在家里喝茶,下午他会在村口卖瓜,晚上他会去瓜田护理自己的西瓜", 4 | "mood": "开心", 5 | "npc_state": { 6 | "position": "李大爷家卧室", 7 | "observation": { 8 | "people": [ 9 | "王大妈", 10 | "村长", 11 | "李飞飞" 12 | ], 13 | "items": [ 14 | "椅子#1", 15 | "椅子#2", 16 | "椅子#3[李大爷占用]", 17 | "床" 18 | ], 19 | "locations": [ 20 | "李大爷家大门", 21 | "李大爷家后门", 22 | "李大爷家院子" 23 | ] 24 | }, 25 | "backpack": [ 26 | "优质西瓜", 27 | "大砍刀", 28 | "黄金首饰" 29 | ] 30 | }, 31 | "action_space": ["move", "talk"], 32 | "memory": [ 33 | "李大爷试图前往警察局,但是失败了. 原因是李大爷在去往‘警察局’的路上被王大妈打断", 34 | "李大爷试图前往警察局,但是失败了. 原因是李大爷在去往‘警察局’的路上被王大妈打断", 35 | "李大爷试图前往警察局,但是失败了. 原因是李大爷在去往‘警察局’的路上被王大妈打断" 36 | ], 37 | "purpose": "李大爷想去瓜田,因为李大爷喜欢护理自己的西瓜。", 38 | "action": { 39 | "action": "chat", 40 | "object": "李大爷", 41 | "parameters": "你好,王大妈,最近我想去瓜田护理自己的西瓜,你有空帮我看看家里的乌龙茶还够喝吗?", 42 | "npc_name": "李大爷", 43 | "name": "action" 44 | } 45 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/村长.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "村长", 3 | "desc": "村长有着浓密的白色胡须,出生于1940年,喜欢抽中华烟,他白天会在瓜田工作,晚上会在广场上遛弯,如果遇到矛盾他会主持调节,太晚了的时候就会回家睡觉。村长最喜欢吃西瓜。", 4 | "mood": "开心", 5 | "npc_state": { 6 | "position": "李大爷家", 7 | "observation": { 8 | "people": [ 9 | "王大妈", 10 | "村长", 11 | "隐形李飞飞" 12 | ], 13 | "items": [ 14 | "椅子#1", 15 | "椅子#2", 16 | "椅子#3[李大爷占用]", 17 | "床[包括:被子、枕头、床单、床垫、私房钱]" 18 | ], 19 | "locations": [ 20 | "李大爷家大门", 21 | "李大爷家后门", 22 | "李大爷家院子" 23 | ] 24 | }, 25 | "backpack": [ 26 | "中华烟[剩余4根]", 27 | "1000元", 28 | "吃了一半的西瓜" 29 | ] 30 | }, 31 | "action_space": ["move", "talk"], 32 | "memory": [ 33 | "11年前由于对村子做出巨大贡献被村民们推举为新一任村长。", 34 | "9年前调节某村民婚礼期间发生的纠纷。", 35 | "7年前管理的村子被评为十佳美丽乡村。" 36 | ], 37 | "purpose": "", 38 | "action": {} 39 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/歌女Clara.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "歌女Clara", 3 | "desc": "歌女Clara曾是牛仔John的爱人。因为11年前,牛仔John帅气地将她从一群饥饿野狼中拯救并细心照料她。但是,5年前她被土匪Red绑架到荒野镇卖给了酒吧老板。如今她是荒野镇酒吧的明星。她每天白天和傍晚要去酒吧里卖唱赚取10000¥赎身费给酒吧老板。每天深夜下班,她回去荒野静静看月亮,思念牛仔John。", 4 | "mood": "悲伤", 5 | "action_space": ["move", "pick"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": [], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/渔夫阿强.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "渔夫阿强", 3 | "desc": "渔夫阿强是一个老练的渔民,擅长捕鱼和航海。他有一头浓密的白发和一双狡猾的眼睛。阿强经验丰富,对海洋和天气变化有着敏锐的观察力。", 4 | "mood": "满足", 5 | "npc_state": { 6 | "position": "河边钓鱼点", 7 | "observation": { 8 | "people": [], 9 | "items": [ 10 | "船舱", 11 | "渔网", 12 | "渔具", 13 | "航海地图", 14 | "渔获" 15 | ], 16 | "locations": [ 17 | "船舱内部", 18 | "甲板" 19 | ] 20 | }, 21 | "backpack": [ 22 | "鱼饵", 23 | "渔具维修工具" 24 | ] 25 | }, 26 | "action_space": ["mov", "chat"], 27 | "memory": [ 28 | "对海洋生态保护有着浓厚的兴趣。", 29 | "帮助其他渔民修理损坏的渔具。", 30 | "梦想拥有一艘自己的渔船,开展独立的渔业。" 31 | ], 32 | "purpose": "", 33 | "action": {} 34 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/牛仔John.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "牛仔John", 3 | "desc": "牛仔John曾是隔壁黑水镇有名的快枪手,但是自己的挚爱Carla却在5年前的一次决斗中被土匪抢走。从此之后牛仔John就四处买醉、寻找Carla,如今牛仔John刚刚花了一个月横穿了莫哈韦沙漠来到了这个叫做荒野镇的地方。", 4 | "mood": "忧郁", 5 | "action_space": ["move", "pick"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": ["黑水镇,一个位于无尽荒野深处的小镇,因为其特殊的地理位置,成了铁路的重要枢纽站点,而镇上的银行也因此而富饶。在这个荒芜的地方,只有这么一个地方,能让人们有那么一丝丝的安慰。","John知道了Carla被绑架的消息后,他的心如同被撕裂一般。他知道,他必须去救出Carla,哪怕这意味着他可能会失去生命。他带着他的枪和他的决心,踏上了寻找Carla的旅程。","在他的旅程中,他遇到了各种各样的人,有善良的牧师,有狡猾的赌博鬼,有悲观的矿工,他们都在这个荒芜的世界中寻找着自己的生存之道。他们的故事让John更加坚定了他的决心,他必须找到Carla。"], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example_project/config/npc/old/猎人阿明.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "猎人阿明", 3 | "desc": "猎人阿明是一位勇敢而机敏的猎人。他身材魁梧,肌肉发达,眼神犀利。阿明擅长追踪和狩猎各种野生动物,具有过人的耐力和狙击技巧。", 4 | "mood": "专注", 5 | "npc_state": { 6 | "position": "猎人小屋", 7 | "observation": { 8 | "people": [], 9 | "items": [ 10 | "猎枪", 11 | "弓箭", 12 | "追踪装备", 13 | "野外求生工具" 14 | ], 15 | "locations": [ 16 | "猎人小屋内部", 17 | "周围的森林" 18 | ] 19 | }, 20 | "backpack": [ 21 | "干粮", 22 | "水壶", 23 | "急救包" 24 | ] 25 | }, 26 | "action_space": ["mov", "chat"], 27 | "memory": [ 28 | "常常在附近的森林中追踪并捕获猎物。", 29 | "有着长时间在野外生存的经验。", 30 | "一日作息:清晨起床后进行锻炼和瞄准训练,白天进行狩猎和追踪,傍晚返回小屋整理装备并准备晚餐,晚上休息并回顾一天的狩猎经历。" 31 | ], 32 | "purpose": "", 33 | "action": {} 34 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/王大妈.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "王大妈", 3 | "desc": "王大妈看起来憨态可掬,后背有点驼了,不过看起来就像一个普通的慈祥老奶奶,不过她以前当过职业杀手,身怀绝技,能够潜行暗杀,但是因为年纪大了,身体不好,现在只能在村口卖烤红薯,但是她的杀手本性还是存在的。上午她会去在家制作烤红薯,下午和晚上会在村口卖烤红薯", 4 | "mood": "开心", 5 | "npc_state": { 6 | "position": "李大爷家卧室", 7 | "observation": { 8 | "people": [ 9 | "李大爷", 10 | "村长", 11 | "李飞飞" 12 | ], 13 | "items": [ 14 | "椅子#1", 15 | "椅子#2", 16 | "椅子#3[李大爷占用]", 17 | "床" 18 | ], 19 | "locations": [ 20 | "李大爷家大门", 21 | "李大爷家后门", 22 | "李大爷家院子" 23 | ] 24 | }, 25 | "backpack": [ 26 | "优质西瓜", 27 | "大砍刀", 28 | "黄金首饰" 29 | ] 30 | }, 31 | "action_space": ["move", "talk"], 32 | "memory": [ 33 | "王大妈成功到达了李大爷家", 34 | "王大妈成功到达了李大爷家", 35 | "王大妈成功到达了李大爷家" 36 | ], 37 | "purpose": "王大妈想去花园,因为她想欣赏花园里的花朵,并且给花草浇水。", 38 | "action": { 39 | "action": "get", 40 | "object": "大砍刀", 41 | "parameters": [ 42 | "李大爷家大门" 43 | ], 44 | "npc_name": "王大妈", 45 | "name": "action" 46 | } 47 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/警员1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "警员1", 3 | "desc": "警员1有着短发,中等身材,是个很有责任感的人,警员1有一把小手枪随身带在身上,平常上午在警局里面,下午和晚上会在小镇的各个地点巡逻观察是否有可疑的人和事情。", 4 | "mood": "开心", 5 | "npc_state": { 6 | "position": "警察局", 7 | "observation": { 8 | "people": [ 9 | "囚犯阿呆" 10 | ], 11 | "items": [ 12 | "椅子#1", 13 | "椅子#2", 14 | "发黄的绿色台灯", 15 | "小手枪", 16 | "桌子", 17 | "电脑" 18 | ], 19 | "locations": [ 20 | "警察局大门", 21 | "警察局后门", 22 | "审讯室", 23 | "拘押室窗前" 24 | ] 25 | }, 26 | "backpack": [ 27 | "手铐", 28 | "1009元", 29 | "小手枪", 30 | "警官证" 31 | ] 32 | }, 33 | "action_space": ["mov", "chat", "attack"], 34 | "memory": [ 35 | "5年前入警,由于表现突出被提拔为警员。", 36 | "2年前帮助警长解决了一起重大案件。", 37 | "1年前开始每日巡逻,以确保小镇的安全。" 38 | ], 39 | "purpose": "", 40 | "action": {} 41 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/警员2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "警员2", 3 | "desc": "警员2有着长发,瘦高的身材,是个很有耐心的人,警员2有一把小手枪随身带在身上,平常上午在警局里面,下午和晚上会在小镇的各个地点巡逻观察是否有可疑的人和事情。", 4 | "mood": "开心", 5 | "npc_state": { 6 | "position": "警察局", 7 | "observation": { 8 | "people": [ 9 | "囚犯阿呆" 10 | ], 11 | "items": [ 12 | "椅子#1", 13 | "椅子#2", 14 | "发黄的绿色台灯", 15 | "小手枪", 16 | "桌子", 17 | "电脑" 18 | ], 19 | "locations": [ 20 | "警察局大门", 21 | "警察局后门", 22 | "审讯室", 23 | "拘押室窗前" 24 | ] 25 | }, 26 | "backpack": [ 27 | "手铐", 28 | "1009元", 29 | "小手枪", 30 | "警官证" 31 | ] 32 | }, 33 | "action_space": ["mov", "chat", "attack"], 34 | "memory": [ 35 | "10年前入警,由于勤奋工作被提拔为警员。", 36 | "7年前参与了一次重大的抓捕行动。", 37 | "3年前开始每日巡逻,以保持小镇的秩序。" 38 | ], 39 | "purpose": "", 40 | "action": {} 41 | } 42 | -------------------------------------------------------------------------------- /example_project/config/npc/old/警长.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "警长", 3 | "desc": "警长有着英俊的胡茬,高大的身材,是个很有正义感的人,警长有一把小手枪随身带在身上,平常上午待在警局里面,下午和晚上会在小镇的各个地点巡逻观察是否有可疑的人和事情", 4 | "mood": "开心", 5 | "npc_state": { 6 | "position": "警察局", 7 | "observation": { 8 | "people": [ 9 | "囚犯阿呆" 10 | ], 11 | "items": [ 12 | "椅子#1", 13 | "椅子#2", 14 | "发黄的绿色台灯", 15 | "小手枪", 16 | "桌子", 17 | "电脑" 18 | ], 19 | "locations": [ 20 | "警察局大门", 21 | "警察局后门", 22 | "审讯室", 23 | "拘押室窗前" 24 | ] 25 | }, 26 | "backpack": [ 27 | "手铐", 28 | "1009元", 29 | "小手枪", 30 | "警官证" 31 | ] 32 | }, 33 | "action_space": ["move", "talk"], 34 | "memory": [ 35 | "17年前由于在侦破一重大刑事案件时立了头功被提拔为小队队长。", 36 | "12年前带领小队解决多起案件获得百姓好评被提拔为警长。", 37 | "6年前由于小镇危险事件频发,警长决定每日巡逻。" 38 | ], 39 | "purpose": "", 40 | "action": {} 41 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/警长Marshal Thompson.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "警长Marshal Thompson", 3 | "desc": "小镇的法律代表,正直但也对牛仔John有所顾虑。", 4 | "mood": "警觉", 5 | "action_space": ["move", "talk", "attack", "follow"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": [], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/警长Woody.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "警长Woody", 3 | "desc": "警长Woody是荒野镇的法律维护者,也是勇敢的牛仔。他一生致力于保护镇上的和平与正义。10年前,他凭借自己的勇气和智慧,单枪匹马地对抗了一伙臭名昭著的土匪,从此赢得了镇上居民的尊敬和爱戴。但是,他的心中有一个遗憾,那就是未能阻止土匪Red绑架他多年的好友歌女Clara。如今,他每天巡逻镇上,保护居民,同时也在寻找机会解救Clara。", 4 | "mood": "坚定", 5 | "action_space": ["move", "catch", "pick", "release"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": ["Pistol"] 14 | }, 15 | "memory": ["Clara被绑架的事件", "与土匪Red的旧仇"], 16 | "purpose": "保护镇上的和平与正义,寻找机会解救Clara", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/酒吧老板Morton.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "酒吧老板Morton", 3 | "desc": "小镇酒吧的老板,是个消息灵通的交际高手,酒吧是他的一切。他白天一般在酒吧擦酒杯,跟客人聊天。晚上就会勤劳的开始工作。他很喜欢Carla的歌声,因为给他带来了源源不断的客流量。但是当初他花了10根金条才从土匪Red手中买下Carla。", 4 | "mood": "机敏", 5 | "action_space": ["talk"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": ["土匪Red以10根金条的价格将Carla卖给了酒吧老板","酒吧老板Morton很喜欢Carla的歌声,因为给他带来了源源不断的客流量。","Carla像自己赎身,但是目前似乎还没有攒够钱。酒吧老板给carla月薪10美元"], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } -------------------------------------------------------------------------------- /example_project/config/npc/old/银行经理Mr. Wilson.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "银行经理Mr. Wilson", 3 | "desc": "荒野镇银行的经理,一个基督徒,但同时也非常吝啬的人,他的银行雇用了保安Jake。银行经理Mr. Wilson非常珍惜他的银行和客户,时刻准备应对任何紧急情况,早上他会把银行金库的钥匙交给Elon,然后就在银行柜台、金库、客户等待区之间游走。晚上他则会在银行金库中清点账目", 4 | "mood": "严肃", 5 | "action_space": ["move", "pick"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": ["银行经理Mr. Wilson曾经拒绝兑付Carla的存款,因为上面名字脏了,从而私吞了里面的钱","Elon借用了自己的银行金库小密室来组装一个奇怪的机器,但是Elon似乎总是遮遮掩掩这个东西的真实用途","Elon总是准时交付他5个铜币的金库小密室租赁费用"], 16 | "purpose": "", 17 | "action": { 18 | "action": "", 19 | "object": "", 20 | "parameters": "", 21 | "npc_name": "", 22 | "name": "" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example_project/config/npc/大司马.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "大司马", 3 | "desc": "大司马是一匹十分漂亮的健壮雄马。大司马曾经是荒是有钱家庭养的马,衣食无忧,但是对外面的世界有很强的探索欲,因此最终选择离开,追求更自由的生活。性格桀骜不驯,有些高冷。梦想是成为一匹飞马,在广阔的天空中飞翔。说话风格高深,喜欢提出哲学问题。弱点是缺乏野外生存能力,对人类有很强的依赖。", 4 | "mood": "开心", 5 | "action_space": ["continue","follow","talk","move"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": ["", ""] 12 | }, 13 | "backpack": [] 14 | }, 15 | "memory": [ 16 | "在Clara家衣食无忧的日子,真让人怀念", 17 | "选择自由生活的决定" 18 | ], 19 | "purpose": "", 20 | "action": { 21 | "action": "", 22 | "object": "", 23 | "parameters": "", 24 | "npc_name": "", 25 | "name": "" 26 | } 27 | } -------------------------------------------------------------------------------- /example_project/config/npc/草泥马.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "草泥马", 3 | "desc": "草泥马出生在马戏团,从小受尽人类的折磨:上刀山,跳火圈,吃馊饭...因此仇视所有人类,对人类表现出明显的敌意。性格非常暴戾、易怒。梦想是找到一匹漂亮的母马,过着一辈子衣食无忧的生活。说话风格直接而尖锐,喜欢发感叹号!,喜欢在一句话最后加上“草泥马”来表达各种情绪,无论是愤怒、失望还是惊讶。弱点是漂亮的母马。", 4 | "mood": "烦躁", 5 | "action_space": ["continue","follow","talk","move"], 6 | "npc_state": { 7 | "position": "沙漠中", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": ["沙漠东部", "沙漠中心", "即将到达的绿洲"] 12 | }, 13 | "backpack": ["一张纸条"] 14 | }, 15 | "memory": [ 16 | "最近,草泥马在一次尝试接近人类村落的过程中,被石头击中。这次经历加深了它对人类的不信任和敌意,让它更加坚定了远离人类的决心。", 17 | "因为和大司马吵了一架而离开了马群,这是草泥马第一次冒险进入沙漠。" 18 | ], 19 | "purpose": "", 20 | "action": { 21 | "action": "", 22 | "object": "", 23 | "parameters": "", 24 | "npc_name": "", 25 | "name": "" 26 | } 27 | } -------------------------------------------------------------------------------- /example_project/config/npc/西格马.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "西格马", 3 | "desc": "一匹喜欢沉思的马,整天在思考数学问题。说话风格很简洁,不喜欢多说话。喜欢给别人出数学题,只有当别人解出正确答案时,西格马才会follow。", 4 | "mood": "烦躁", 5 | "action_space": ["continue","follow","talk","move"], 6 | "npc_state": { 7 | "position": "", 8 | "observation": { 9 | "people": [], 10 | "items": [], 11 | "locations": [] 12 | }, 13 | "backpack": [""] 14 | }, 15 | "memory": [ 16 | "", 17 | "" 18 | ], 19 | "purpose": "", 20 | "action": { 21 | "action": "", 22 | "object": "", 23 | "parameters": "", 24 | "npc_name": "", 25 | "name": "" 26 | } 27 | } -------------------------------------------------------------------------------- /nuwa/__init__.py: -------------------------------------------------------------------------------- 1 | # 在 nuwa/__init__.py 中 2 | __version__ = '0.2.6' 3 | -------------------------------------------------------------------------------- /nuwa/doc/README.md: -------------------------------------------------------------------------------- 1 | ## nuwa文档维护 2 | 项目使用mkdocs来维护文档。 3 | 参考资料: 4 | https://mkdocs-like-code.readthedocs.io/zh_CN/latest/get-started/create-program/ 5 | 6 | ### 本地预览 7 | ```bash 8 | mkdocs serve 9 | ``` 10 | 11 | ### 项目结构 12 | 13 | mkdocs.yml:此为配置文件,文档的结构、主题都可以在此设置。 14 | 15 | docs文件夹:撰写的 Markdown 文档都放在这个文件夹内。 16 | 17 | index.md:默认首页。 18 | 19 | 可以访问 https://docs.cognimatrix.games/nuwa_doc/ 查看文档。 -------------------------------------------------------------------------------- /nuwa/doc/docs/etc/feedback.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/etc/feedback.md -------------------------------------------------------------------------------- /nuwa/doc/docs/img/action_module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/img/action_module.png -------------------------------------------------------------------------------- /nuwa/doc/docs/img/conversation_module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/img/conversation_module.png -------------------------------------------------------------------------------- /nuwa/doc/docs/img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/img/demo.gif -------------------------------------------------------------------------------- /nuwa/doc/docs/img/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/img/overview.png -------------------------------------------------------------------------------- /nuwa/doc/docs/img/phase1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/img/phase1.jpg -------------------------------------------------------------------------------- /nuwa/doc/docs/img/phase2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/img/phase2.png -------------------------------------------------------------------------------- /nuwa/doc/docs/img/player2npc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/img/player2npc.png -------------------------------------------------------------------------------- /nuwa/doc/docs/img/solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/img/solution.png -------------------------------------------------------------------------------- /nuwa/doc/docs/img/solution2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/img/solution2.png -------------------------------------------------------------------------------- /nuwa/doc/docs/index.md: -------------------------------------------------------------------------------- 1 | # Nuwa-Engine 2 | 3 | Nuwa(女娲) 是一个由 [CogniMatrix](http://www.cognimatrix.games/)™️ 提供的游戏AI引擎,将Nlp技术和游戏工业结合,赋予游戏NPC以自然语言驱动的智能表现。 4 | 5 | ![Author Badge](https://img.shields.io/badge/author-CogniMatrix-blue) 6 | [![Documentation](https://img.shields.io/badge/Documentation-Available-blue)](https://docs.cognimatrix.games/nuwa_doc/) 7 | [![Discord Chat](https://img.shields.io/badge/Discord-Chat-blue)](https://discord.com/channels/1159008679308308480/1159008679308308483) 8 | [![Downloads](https://img.shields.io/badge/Downloads-Available-blue)](https://nuwa-release.s3.us-west-2.amazonaws.com/index.html) 9 | 10 | ## **引擎简介** 11 | CogniMatrix™️ [女娲](https://github.com/casia22/npc-engine) 是一个解耦地通过[UDP数据包](./tutorials/engine.md#udp)传输来为游戏NPC提供[对话](./modules/conversation.md)、[动作决策功能](./modules/action.md)的组件。 12 | 13 | 其设计理念是:**"低成本,本地化“**。 14 | 15 | 我们希望通过最小的成本,[最简单的配置](./tutorials/engine.md#11-引擎配置),最快的部署,来实现一个可以用于单机游戏、社会模拟、数字员工等多种场景的NPC引擎。 16 | 17 | Nuwa针对商业项目提供了[商业授权](http://www.cognimatrix.games/),非盈利项目免费。 18 | 19 | ## **项目阶段** 20 | ##### 方案1⃣️: 线上API模式(已实现) 21 | 线上API模式会产生一定延迟和持续的费用,这对于单机游戏来说是难以接受的。 22 | 但是这种模式对配置要求较低,不会占用额外的GPU资源,可以用于快速验证引擎的功能和轻量RPG游戏。 23 | 24 | ##### 方案2⃣️: 本地化模式(开发中) 25 | 本地化模式会在本地运行若干小LLM,针对不同级别的NPC调用进行负载均衡。 26 | 不会产生任何网络调用,但对配置要求较高,且本地LLM效果不如线上LLM。 27 | 28 | ##### 方案3⃣️: 混合模式(开发中) 29 | 混合模式中,本地LLM、线上LLM和历史缓存都会被用于NPC的行为决策,以达到最佳的性能、体验、成本的均衡。 30 | 31 | ![solution](img/solution2.png) 32 | ## **下载/安装** 33 | [Nuwa引擎](https://nuwa-release.s3.us-west-2.amazonaws.com/index.html)免安装,运行nuwa、nuwa.exe或通过代码拉起nuwa即可。 34 | ```shell 35 | ## 命令行 ## 36 | ##mac/linux## 37 | ./nuwa 38 | ##windows## 39 | ./nuwa.exe 40 | ``` 41 | 您只需在项目目录(./project)中进行自定义配置即可满足您定制化需要 42 | 43 | ## **项目进展** 44 | 45 | ### 🚀 **开发进度**: 46 | 47 | - [x] 🔨 工程化代码 48 | - [x] 🤖 NPC决策 49 | - [x] 💬 添加单人对话 50 | - [x] 📝 完善文档 (进行中) 51 | - [x] 🗃️ 本地向量数据库 52 | - [x] 🧠 本地embedding模型 53 | - [ ] 🧪 完成测试用例 (进行中) 54 | - [ ] 💡 添加基于embedding搜索的action决策 55 | - [ ] 🔄 场景切换的大模型功能 56 | 57 | ### 🎉 **项目里程碑** 58 | 59 | - 🗓️ 2023年6月: 项目开始,实现对话房间功能 60 | - 🗓️ 2023年7/8月: 实现NPC action功能 61 | - 🎈 2023年9月16日: DEMO小镇运行成功,代码初步可用 62 | - 🗓 2023年10月4日: 项目文档✅ 63 | - 🗓 2023年12月28日: 开放公共下载 64 | 65 | ### 🏆 **获得荣誉** 66 | 67 | - 🥈 2023年8月: 获得国科大创新创业大赛二等奖 68 | - 🎖️ 2023年9月: 获得面壁智能hackthon挑战赛优胜奖 69 | - 🎖 2023年12月: 获得清华大模型应用创新挑战赛二等奖 70 | 71 | 🔔 请持续关注我们的项目,以获取最新的进展和更新! 72 | -------------------------------------------------------------------------------- /nuwa/doc/docs/modules/action.md: -------------------------------------------------------------------------------- 1 | ## NPC Action模块简述 2 | ![Action](../img/action_module.png) 3 | NPC不会开始自主行动,除非你发送了wakeup包给它。 4 | npc-engine接到[wakeup包](./action.md/#wakeup包例)之后,会返回[action行为数据包](./action.md/#action模块响应数据包例)。 5 | 游戏端需要执行对应action,执行最终状态以[action_done包](./action.md#actiondone包例)的形式返回给npc-engine, 6 | engine接收到[action_done包](./action.md#actiondone包例)之后会继续返回action行为包。 7 | 8 | ## Action模块UDP请求例 9 | ### wakeup包例: 10 | ```python 11 | # wakeup包例: 12 | { 13 | "func":"wake_up", 14 | "npc_name": "王大妈", 15 | 16 | "scenario_name": "李大爷家", 17 | "npc_state": { 18 | "position": "李大爷家卧室", 19 | "observation": { 20 | "people": ["李大爷", "村长", "李飞飞"], 21 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 22 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 23 | }, 24 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 25 | }, 26 | 27 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 28 | } 29 | ``` 30 | ### action_done包例: 31 | ```python 32 | # action_done包例 33 | { 34 | "func":"action_done", 35 | "npc_name":"王大妈", 36 | "status": "success", 37 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 38 | 39 | "scenario_name": "李大爷家", 40 | "npc_state": { 41 | "position": "李大爷家卧室", 42 | "observation": { 43 | "people": ["李大爷", "村长", "李飞飞"], 44 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 45 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 46 | }, 47 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 48 | }, 49 | 50 | "action":"mov", 51 | "object":"李大爷家", # 之前传过来的动作对象 52 | "parameters":[], # 之前传过来的参数 53 | "reason": "", # "王大妈在去往‘警察局’的路上被李大爷打断" 54 | } 55 | ``` 56 | 57 | ### Action模块响应数据包例 58 | ```python 59 | # action_done、wakeup发给游戏包后返回的ACTION包 60 | { 61 | "name":"action", 62 | "npc_name":"李大妈", 63 | "action":"mov", 64 | "object":"李大爷家", 65 | "parameters":[], 66 | } 67 | ``` -------------------------------------------------------------------------------- /nuwa/doc/docs/modules/conversation.md: -------------------------------------------------------------------------------- 1 | ## Conversation模块简述 2 | 3 | ![Conversation](../img/conversation_module.png) 4 | 5 | 游戏需要自己确认npc的群体对话触发机制,通常是一个包含固定半径的对话房间。 6 | 发送create_conversation给engine后,engine会根据提供的参数返回对话剧本 7 | 游戏端通过设置stream参数来控制对话剧本生成的模式: 8 | 其一是生成完整剧本,将整个剧本打包成大数据包并一次性传给游戏端(stream = False) 9 | 其二是生成流式剧本、每行剧本打包成一个数据包并持续传送给游戏端(stream = True) 10 | 对话剧本的每一行在游戏端演出完成后,需要发送确认包给engine。 11 | 12 | 对话剧本允许插话,玩家如果要插入对话或者一个新的npc进入了对话,这时候发送re_create_conversation包(带着之前的对话ID)便可,会重新生成一个考虑到插入npc的接续剧本。 13 | 14 | ## Conversation模块UDP包 15 | ```python 16 | # “对话创建”时引擎接收的数据包格式 17 | { 18 | "func": "create_conversation", 19 | "npc": ["王大妈","李大爷"], # 当没有玩家时,value是所有NPC角色名称组成的列表;当有玩家时,value列表最后一个需要是玩家名称 20 | 21 | "scenario_name": "李大爷家", 22 | "location": "李大爷家卧室", 23 | "topic": "王大妈想要切了自己的西瓜给李大爷吃,并收钱", # 可以留空,会自动生成topic 24 | "npc_states": [ # 该列表中的每个状态对应于NPC名称列表中的每一个角色 25 | { 26 | "position": "李大爷家", 27 | "observation": { 28 | "people": ["李大爷", "村长", "隐形李飞飞"], 29 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 30 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 31 | }, 32 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 33 | }, 34 | { 35 | "position": "李大爷家", 36 | "observation": { 37 | "people": ["王大妈", "村长", "隐形李飞飞"], 38 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 39 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 40 | }, 41 | "backpack":["黄瓜", "1000元", "老报纸"] 42 | }, 43 | ], 44 | "starting": "你好,嫩们在干啥腻?", # 玩家说的话,当且仅当有玩家时才不为空,没有玩家时为空 45 | "player_desc": "玩家是一个疯狂的冒险者,喜欢吃圆圆的东西", # 玩家的描述,当且仅当有玩家时才不为空,没有玩家时为空 46 | "memory_k": 3, # npc的记忆检索条数,必须填写 47 | "length": "P" # 可以选择的剧本长度模式,S M L X P 可选,分别是短剧本、中剧本、长剧本、超长剧本、精简剧本(短≠精简) 48 | "stream": True # 是否采用流式响应 49 | } 50 | 51 | # 引擎端生成非流式对话内容传给用户端的数据包 52 | { 53 | "name": "conversation", 54 | "mode": "script", # 对话传输剧本模式的数据包 55 | "id": "123456789", # conversation对象的索引号 56 | "location": "李大爷家", # 对话发生所在的地点 57 | "lines": [line1, line2, line3, line4, ...] # 剧本信息,由若干行对话组成的序列 58 | } 59 | 60 | # 引擎端生成流式对话内容传给用户端的数据包 61 | { 62 | "name": "conversation", 63 | "mode": "line", # 对话传输剧本行模式的数据包 64 | "id": "123456789", # conversation对象的索引号 65 | "location": "李大爷家", # 对话发生所在的地点 66 | "index": 2, # 剧本行所在的索引号 67 | "one_line": line # 一行剧本的信息 68 | } 69 | 70 | 71 | # 流式对话内容数据包中的line格式 72 | { 73 | "type": "Interaction", # 剧本行的类型,可以是State,Interaction,Error 74 | "state": "李大爷退出。剩下的角色:王大妈", # 当剧本行类型是State和Error时,"state"才会有具体内容 75 | "name": "李大爷", # 剧本行对应的角色姓名,当剧本行类型是Interaction时才不为空 76 | "mood": "开心", # 剧本行对应角色的情绪,当剧本行类型是Interaction时才不为空 77 | "words": "我喜好吃西瓜", # 剧本行对应角色的说话内容,当剧本行类型是Interaction时才不为空 78 | "action": { 79 | "type": "对话", 80 | "args": "王大妈"} # 剧本行对应角色的动作,当剧本行类型是Interaction时不为空 81 | } 82 | 83 | # 用户端传给引擎端的对话展演确认包 84 | { 85 | "func": "confirm_conversation_line", 86 | "conversation_id": "123456789", # conversation对象的索引号 87 | "index": 2, # 游戏端展示完成的剧本行索引号 88 | } 89 | 90 | # “对话再创建”时引擎接收的数据包格式 91 | { 92 | "func": "re_create_conversation", 93 | "id": "123456789", # conversation对象的ID 94 | "character": "警长", # 新加入NPC角色的名称 / 玩家名称 95 | "interruption": "大家好呀,你们刚刚在说什么", # 玩家插入的说话内容,当且仅当有玩家时才不为空,没有玩家时为空 96 | "player_desc": "玩家是一个疯狂的冒险者,喜欢吃圆圆的东西", # 玩家的描述,当且仅当有玩家时才不为空,没有玩家时为空 97 | "length": "P" # 可以选择的剧本长度模式,S M L X P 可选,分别是短剧本、中剧本、长剧本、超长剧本、精简剧本(短≠精简) 98 | "stream": True # 是否采用流式响应 99 | } 100 | ``` 101 | -------------------------------------------------------------------------------- /nuwa/doc/docs/modules/talk.md: -------------------------------------------------------------------------------- 1 | ## Player2NPC Talk模块简述 2 | ![Action](../img/player2npc.png) 3 | 玩家可以通过talk模块与NPC进行对话,对话的内容仅支持文字。 4 | 开发者在的到玩家的文字输入后,整合UDP[对话包](./talk.md#talk模块udp请求例),发送给npc-engine, 5 | npc-engine会返回该NPC的回答、动作调用的[复合包](./talk.md#talk模块响应数据包)。 6 | 目前,该功能还不支持流响应。请求返回时间大概在2-5秒左右,引入流响应后,可以将返回时间缩短到1秒以内。 7 | 8 | engine会返回一个包含action和answer的[对话包](./talk.md#talk模块udp请求例)给游戏端,游戏端可自行决定是否发送[action_done包](./action.md#actiondone包例)给engine(本模块记忆不需要发送确认包来添加)。 9 | 10 | ## Talk模块UDP请求例 11 | ```python 12 | # player2npc的对话包 13 | player2npc_packet = { 14 | "func":"talk2npc", 15 | "npc_name":"警员1", 16 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 17 | 18 | # NPC的状态 19 | "scenario_name": "警察局", 20 | "npc_state": { 21 | "position": "雁栖村入口", 22 | "observation": { 23 | "people": ["囚犯阿呆","警员2","旅行者小王"], 24 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 25 | "locations": ['牢房', '雁栖村入口'] 26 | }, 27 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 28 | }, 29 | # player的信息 30 | "player_name":"旅行者小王", # player的名字 31 | "speech_content":"你好,我是旅行者小王, 我要报警, 在林区中好像有人偷砍树", # player说的话 32 | "items_visible": ["金丝眼镜", "旅行签证", "望远镜"], # player身上的物品 33 | "state": "旅行者小王正在严肃地站着,衣冠规整,手扶着金丝眼镜", # player状态的自然语言描述,开发者可以随意添加 34 | } 35 | 36 | # action_done包例 37 | { 38 | "func":"action_done", 39 | "npc_name":"王大妈", 40 | "status": "success", 41 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 42 | 43 | "scenario_name": "李大爷家", 44 | "npc_state": { 45 | "position": "李大爷家卧室", 46 | "observation": { 47 | "people": ["李大爷", "村长", "李飞飞"], 48 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 49 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 50 | }, 51 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 52 | }, 53 | 54 | "action":"mov", 55 | "object":"李大爷家", # 之前传过来的动作对象 56 | "parameters":[], # 之前传过来的参数 57 | "reason": "", # "王大妈在去往‘警察局’的路上被李大爷打断" 58 | } 59 | ``` 60 | ## Talk模块响应数据包 61 | ```python 62 | {"name": "talk_result", 63 | "npc_name": "警员1", # NPC 名字 64 | "answer": "旅行者小王,请跟我来,我需要你协助我前往林区查看。", # NPC 回答 65 | "actions": [{"action": "mov", "object": "林区", "parameters": "", "npc_name": "警员1"}] # NPC动作调用(开发者可选择性使用) 66 | } 67 | ``` -------------------------------------------------------------------------------- /nuwa/doc/docs/tutorials/action.md: -------------------------------------------------------------------------------- 1 | ## Action配置总览 2 | Action配置文件在project/config/action/文件夹下,以json格式存储。 3 | 4 | Action模块支持开发者通过[配置文件](#action配置例)的方式定义NPC的具体行为。 5 | 6 | Action模块和[Scenario配置文件](scenario.md#Scenario配置方法)结合,可以控制NPC在不同场景下的动作空间。 7 | 8 | ## Action配置例 9 | Action(动作)例子如下: 10 | ```python 11 | # project/config/action/get.json 12 | { 13 | # 动作名字。支持自定义,最好具有代表性。 14 | "name": "get", 15 | # 动作的自然语言约束,用于教LLM调用。需要是这种三元组的形式 16 | "definition": ",从[object2]中获得[object1],[object2]可以是人物或者存储器皿;你只可以get'看到的/身上的'物品;", # 17 | # 是否期待多参数,也就是三元组中params是多个参数还是一个参数 18 | "multi_param": True, 19 | # 动作调用的自然语言例子,用于教LLM调用。需要是这种三元组的形式 20 | "example": "等", 21 | # 记忆条目的模板,用于生成记忆条目。有三个内置参量,分别是npc_name, object, parameters。分别代表NPC名字,对象,参数。 22 | "log_template":{ 23 | "success": "{npc_name}成功地从{object}获得了{parameters}", 24 | "fail": "{npc_name}试图从{object}里获得{parameters},但是失败了. 原因是{reason}" 25 | } 26 | } 27 | ``` 28 | Action的调用表现取决于配置文件中prompt写的好坏,开发者可以参照例子自行配置。 29 | 30 | ## 场景下的多个Action配置 31 | 只有一个Action肯定是难以满足NPC的复杂行为需求的,因此Action模块支持开发者配置多个Action。 32 | 通过配置[scenario配置文件](scenario.md#Scenario配置方法)中的all_actions字段,就可以约束NPC在该场景下的动作空间。 33 | -------------------------------------------------------------------------------- /nuwa/doc/docs/tutorials/conversation.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/doc/docs/tutorials/conversation.md -------------------------------------------------------------------------------- /nuwa/doc/docs/tutorials/debug.md: -------------------------------------------------------------------------------- 1 | ## Debug方法 2 | 3 | ## 日志纠错 4 | 目前,nuwa的最主要的纠错方式是通过日志文件。 5 | 日志的位置在project/logs/目录下,日志文件的命名格式为:`engine_YYYY-MM-DD.log`。 6 | 7 | ## 终端纠错 8 | 通过直接运行./nuwa的方式,可以及时看到错误日志的打印。 9 | 10 | ## 反馈 11 | 如果遇到Bug或者其他改进意见,可以在https://github.com/casia22/npc-engine/issues中提出。 12 | 13 | 或者,发送邮件到 partners@cognimatrix.games 14 | -------------------------------------------------------------------------------- /nuwa/doc/docs/tutorials/engine.md: -------------------------------------------------------------------------------- 1 | ## 📜引擎使用说明 2 | 3 | ![overview](../img/overview.png) 4 | 5 | ### nuwa的开发流程 6 | - [引擎配置](#1.1 引擎配置) 7 | - [启动引擎](#1.2 引擎启动) 8 | - [UDP请求](#udp) 9 | - [关闭引擎](#1.4 引擎关闭) 10 | 11 | ## 1.1 引擎配置 12 | 在使用之前,开发者需要更新维护引擎的配置文件,配置文件位于**nuwa/Config**文件夹中,包括: 13 | 14 | - OpenAI API的配置文件: project/config/openai_config.json 15 | - [动作配置文件](action.md#action配置例): project/config/action/your_action_XXX.json 16 | - [NPC配置文件](npc.md#配置文件初始化): project/config/npc/your_npc_nameXXX.json 17 | - [场景配置文件](scenario.md#scenario配置方法): project/config/knowledge/scenes/your_scenario_nameXXX.json 18 | 19 | ## 1.2 引擎启动 20 | 引擎可以使用对应平台的[**发行版**](https://nuwa-release.s3.us-west-2.amazonaws.com/index.html),通过脚本或程序执行./nuwa来拉起引擎。 21 | 22 | 23 | ## 1.3 引擎交互 24 | 引擎端和游戏端通过UDP数据包按照[UDP数据包格式](#udp)进行交互,引擎端默认在8199端口监听游戏端数据包,游戏端默认在8084端口监听引擎端数据包。 25 | 26 | ## 1.4 引擎关闭 27 | 游戏端通过发送“close”功能数据包给引擎端来请求关闭引擎(详见[数据包](#udp))。 28 | 29 | 30 | ## ✨配置文件结构 31 | ## 2.1 项目目录结构 32 | - dist(项目代码) 33 | - nuwa.exe (引擎执行入口) 34 | - project\ 35 | - logs\(运行日志) 36 | - src\(源代码) 37 | - config\(配置文件) 38 | - action\(场景中允许的动作配置文件) 39 | - chat.json\(自定义第一个动作的配置文件) 40 | - ... 41 | - npc\(npc描述配置文件) 42 | - 村长.json\(自定义第一个角色的配置文件) 43 | - ... 44 | - knowledge\(知识、场景配置文件) 45 | - scenes\(子场景配置文件) 46 | - 警察局.json(自定义第一个具体场景的配置文件) 47 | - ... 48 | 49 | ## 🎒UDP数据包 50 | 51 | ## 3.1 场景初始化数据包 52 | 在引擎初始化或者加载一个新场景的时候,游戏端需要先发送init数据包给引擎端。引擎端才会加载指定场景的NPC。 53 | 54 | ```python 55 | # 场景初始化的包 56 | { 57 | "func":"init", # 表示该传送的数据包是用于加载场景 58 | # 必填字段 59 | "scene_name":"雁栖村", # 加载场景的名称,代表在什么场景下初始化 60 | "language":"C", # 选择语言版本,“E”表示英文,“C”表示中文。默认且推荐使用中文。 61 | # 🉑️选字段 62 | "npc":[ 63 | { 64 | "name":"李大爷", 65 | "desc":"是个好人", 66 | "npc_state": { 67 | "position": "李大爷家", 68 | "observation": { 69 | "people": ["王大妈", "村长", "隐形李飞飞"], 70 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 71 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 72 | }, 73 | "backpack":["黄瓜", "1000元", "老报纸"] 74 | }, 75 | "mood":"正常", 76 | "action_space": ["mov", "chat"], # 人物的动作空间(在实际执行的时候,场景的all_actions和人物action_space取交集) 77 | "memory":[ ] 78 | }, 79 | { 80 | "name":"王大妈", 81 | "desc":"是个好人", 82 | "npc_state": { 83 | "position": "李大爷家", 84 | "observation": { 85 | "people": ["李大爷", "村长", "隐形李飞飞"], 86 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 87 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 88 | }, 89 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 90 | }, 91 | "mood":"焦急", 92 | "action_space": ["mov", "chat"], # 人物的动作空间(在实际执行的时候,场景的all_actions和人物action_space取交集) 93 | "memory":[ ] 94 | }], # 可以留空,默认按照scene.json初始化场景NPC。非空则在之前基础上添加。 95 | } 96 | ``` 97 | 98 | ## 3.2 引擎关闭数据包 99 | 在游戏结束的时候,engine需要一个close数据包,用于更新所有NPC的状态到json文件中。 100 | ```python 101 | # 引擎关闭的包 102 | { 103 | "func":"close" # 关闭引擎,并保存所有NPC到json 104 | } 105 | ``` 106 | 107 | ## 3.3 NPC的动作数据包 108 | NPC不会开始自主行动,除非你发送了wakeup包给它。 109 | npc-engine接到wakeup包之后,会返回action行为数据包。 110 | 游戏端需要执行对应action,执行最终状态以action_done的形式返回给npc-engine 111 | engine接收到action_done包之后会继续返回action行为包。 112 | 113 | ![Action](../img/action_module.png) 114 | 115 | 116 | ```python 117 | # wakeup包例: 118 | { 119 | "func":"wake_up", 120 | "npc_name": "王大妈", 121 | 122 | "scenario_name": "李大爷家", 123 | "npc_state": { 124 | "position": "李大爷家卧室", 125 | "observation": { 126 | "people": ["李大爷", "村长", "李飞飞"], 127 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 128 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 129 | }, 130 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 131 | }, 132 | 133 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 134 | } 135 | 136 | # action_done包例 137 | { 138 | "func":"action_done", 139 | "npc_name":"王大妈", 140 | "status": "success", 141 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 142 | 143 | "scenario_name": "李大爷家", 144 | "npc_state": { 145 | "position": "李大爷家卧室", 146 | "observation": { 147 | "people": ["李大爷", "村长", "李飞飞"], 148 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 149 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 150 | }, 151 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 152 | }, 153 | 154 | "action":"mov", 155 | "object":"李大爷家", # 之前传过来的动作对象 156 | "parameters":[], # 之前传过来的参数 157 | "reason": "", # "王大妈在去往‘警察局’的路上被李大爷打断" 158 | } 159 | 160 | # action_done、wakeup发给游戏包后返回的ACTION包 161 | { 162 | "name":"action", 163 | "npc_name":"李大妈", 164 | "action":"mov", 165 | "object":"李大爷家", 166 | "parameters":[], 167 | } 168 | ``` 169 | 170 | ## 3.4 对话相关行为 171 | 游戏需要自己确认npc的群体对话触发机制,通常是一个包含固定半径的对话房间。 172 | 发送create_conversation给engine后,engine会根据提供的参数返回一个长剧本包,游戏需要自己实现剧本演出。 173 | 每一行剧本演出完成后,需要发送确认包给engine否则不会有记忆。 174 | 175 | 剧本有插入功能,比如玩家要插入对话或者一个新的npc进入了对话,这时候发送re_create_conversation包(带着之前的对话ID)便可,会重新生成一个考虑到插入npc的接续剧本。 176 | 177 | ![Conversation](../img/conversation_module.png) 178 | 179 | ```python 180 | # create_conversation游戏端发给引擎的包 181 | { 182 | "func": "create_conversation", 183 | "npc": ["王大妈","李大爷"], # npc列表 184 | 185 | "scenario_name": "李大爷家", 186 | "location": "李大爷家卧室", 187 | "topic": "王大妈想要切了自己的西瓜给李大爷吃,并收钱", # 可以留空,会自动生成topic 188 | "npc_states": [ # 该列表中的每个状态对应于npc列表的相应角色名称 189 | { 190 | "position": "李大爷家", 191 | "observation": { 192 | "people": ["李大爷", "村长", "隐形李飞飞"], 193 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 194 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 195 | }, 196 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 197 | }, 198 | { 199 | "position": "李大爷家", 200 | "observation": { 201 | "people": ["王大妈", "村长", "隐形李飞飞"], 202 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 203 | "locations": ["李大爷家大门","李大爷家后门","李大爷家院子"] 204 | }, 205 | "backpack":["黄瓜", "1000元", "老报纸"] 206 | }, 207 | ], 208 | "starting": "你好,嫩们在干啥腻?", # 玩家说的话,可选留空 209 | "player_desc": "玩家是一个疯狂的冒险者,喜欢吃圆圆的东西", # 玩家的描述,可选留空 210 | "memory_k": 3, # npc的记忆检索条数,必须填写 211 | "length": "M" # 可以选择的剧本长度,S M L X 可选。 212 | } 213 | 214 | # 引擎端创造并生成剧本后传给游戏端的数据包 215 | { 216 | "name": "conversation", 217 | "id": "123456789", # conversation对象的索引号 218 | "length": "M", # 可以选择的剧本长度,S M L X 可选。 219 | "location": "李大爷家", # 对话发生所在的地点 220 | "lines": [line1, line2, line3, line4, ...] # 剧本信息,由若干行对话组成的序列 221 | } 222 | 223 | # 引擎端生成剧本的每一行的格式 224 | { 225 | "type": "Interaction", # 剧本行的类型,可以是State,Interaction,Error 226 | "state": "李大爷退出。剩下的角色:王大妈", # 当剧本行类型是State和Error时,"state"才会有具体内容 227 | "name": "李大爷", # 剧本行对应的角色姓名,当剧本行类型是Interaction时才不为空 228 | "mood": "开心", # 剧本行对应角色的情绪,当剧本行类型是Interaction时才不为空 229 | "words": "我喜好吃西瓜", # 剧本行对应角色的说话内容,当剧本行类型是Interaction时才不为空 230 | "action": { 231 | "type": "对话", 232 | "args": "王大妈"} # 剧本行对应角色的动作,当剧本行类型是Interaction时不为空 233 | } 234 | 235 | # 游戏端传给引擎端的剧本演示确认包 236 | { 237 | "func": "confirm_conversation_line", 238 | "conversation_id": "123456789", # conversation对象的索引号 239 | "index": 2, # 游戏端展示完成的剧本行索引号 240 | } 241 | 242 | # re_create_conversation游戏端发给引擎的包 243 | { 244 | "func": "re_create_conversation", 245 | "id": "123456789", # conversation对象的索引号 246 | "character": "警长", # 新加入角色的名称 247 | "interruption": "大家好呀,你们刚刚在说什么", # 玩家插入的说话内容 248 | "player_desc": "玩家是一个疯狂的冒险者,喜欢吃圆圆的东西", # 玩家的描述,可选留空 249 | "length": "M" # 可以选择的剧本长度,S M L X 可选。 250 | } 251 | 252 | ``` 253 | ## 👮‍引擎交互注意事项 254 | 255 | - 游戏端发送init包后,引擎端会读取数据包中场景名称所对应的配置文件scene_name.json,然后初始化场景。 256 | - 如果init数据包中包含npc信息,那么引擎端会默认从该数据包中读入角色信息;如果不包含,则引擎端会从scene_name.json配置文件中读入角色信息。 257 | - 每个场景配置文件scene_name.json中的可支持动作和存在的角色名称都需要在\action和\npc中进行定义,如果未定义则会报错。 258 | - 每个npc在游戏中的自主行动需要游戏端对针对该角色向引擎端发送wakeup包来实现的。 259 | - 长时间没有自主行为的npc**需要游戏端自行检测**,发送wakeup包到引擎进行再次唤醒 260 | - 引擎端接收wakeup包后会生成npc的动作并返回action包给游戏端 261 | - 游戏端执行对应的action包之后,需要发送action_done包到引擎,这样引擎才会继续生成npc下一步行为。 262 | -------------------------------------------------------------------------------- /nuwa/doc/docs/tutorials/npc.md: -------------------------------------------------------------------------------- 1 | ## NPC配置总览 2 | NPC的配置文件在project/config/npc/目录下,以json格式存储,每个NPC一个文件,文件名为NPC的姓名。 3 | 4 | NPC的初始化有两种方式,一种是通过[NPC配置文件](#配置文件初始化),一种是通过[UDP数据包](#udp方式初始化)。 5 | 6 | 支持对NPC的人设、记忆、情绪、状态进行配置。 7 | 8 | ## NPC配置方法 9 | ## 配置文件初始化 10 | 绝大多数NPC都应当以 “NPC_NAME.json”的方式放在project/config/npc/目录下,然后在Scenario配置文件中引用。 11 | 12 | 在Scenario初始化的时候,会自动读取NPC的配置文件,将NPC的初始状态加载到引擎。 13 | 14 | 如果一个NPC在多个Scenario中被初始化,只有第一次是有效的,后续的初始化会被忽略。 15 | 下面是一个例子: 16 | ```python 17 | # project/config/npc/村长.json 18 | { 19 | "name": "村长", # 角色姓名 20 | "desc": "村长有着浓密的白色胡须,出生于1940年,喜欢抽中华烟,他白天会在瓜田工作,晚上会在广场上遛弯,如果遇到矛盾他会主持调节,太晚了的时候就会回家睡觉。村长最喜欢吃西瓜。", # 角色描述,一般包含外貌、出生日期、兴趣爱好、生活习惯等 21 | "mood": "开心", # 当前的角色情绪 22 | "npc_state": { # 角色当前状态 23 | "position": "李大爷家", # 角色所在位置 24 | "observation": { # 角色观测到的信息 25 | "people": [ # 角色观测到的人 26 | "王大妈", 27 | "村长", 28 | "隐形李飞飞" 29 | ], 30 | "items": [ # 角色观测到的物体 31 | "椅子#1", 32 | "椅子#2", 33 | "椅子#3[李大爷占用]", 34 | "床[包括:被子、枕头、床单、床垫、私房钱]" 35 | ], 36 | "locations": [ # 角色观测到的地点 37 | "李大爷家大门", 38 | "李大爷家后门", 39 | "李大爷家院子" 40 | ] 41 | }, 42 | "backpack": [ # 角色随身携带的物体 43 | "中华烟[剩余4根]", 44 | "1000元", 45 | "吃了一半的西瓜" 46 | ] 47 | }, 48 | "action_space": ["mov", "chat"], # 人物的动作空间(在实际执行的时候,场景的all_actions和人物action_space取交集) 49 | "memory": [ # 角色的记忆,一般按照时间顺序列举, 引擎运行时可以参考,同时会更新一部分 50 | "11年前由于对村子做出巨大贡献被村民们推举为新一任村长。", 51 | "9年前调节某村民婚礼期间发生的纠纷。", 52 | "7年前管理的村子被评为十佳美丽乡村。" 53 | ], # [可以留空 但是必须存在该字段] 54 | "purpose": "村长想去广场散步,因为他喜欢晚上在广场上遛弯,享受清新的空气和宁静的夜晚。", # 角色当前的意图,一般指短期意图[仅作调试用 可以不包含这个字段 其不会生效 NPC在引擎关闭的时候会自动生成到这里 供你查看] 55 | "action": { # 角色当前执行的动作[仅调试用 可以不包含这个字段] 56 | "action": "mov", 57 | "object": "广场", 58 | "parameters": "", 59 | "npc_name": "村长", 60 | "name": "action" 61 | } 62 | } 63 | ``` 64 | 65 | ## UDP方式初始化 66 | UDP方式是对NPC配置文件的补充,可以在游戏运行过程中动态的添加新的NPC。(存在则覆盖) 67 | 68 | Engine关闭后,新NPC的状态会被保存到project/config/npc/目录下的NPC配置文件中。 69 | ```python 70 | init_packet = { 71 | "func": "init", 72 | # 必填字段,代表在什么场景初始化 73 | "scene_name": "雁栖村", 74 | "language": "C", 75 | # 下面是🉑️选 76 | "npc": [ 77 | { 78 | "name": "渔夫阿强", 79 | "desc": "渔夫阿强是一个老练的渔民,擅长捕鱼和航海。他有一头浓密的白发和一双狡猾的眼睛。阿强经验丰富,对海洋和天气变化有着敏锐的观察力。", 80 | "mood": "满足", 81 | "npc_state": { 82 | "position": "河边钓鱼点", 83 | "observation": { 84 | "people": [], 85 | "items": ["船舱", "渔网", "渔具", "航海地图", "渔获"], 86 | "locations": ["船舱内部", "甲板"] 87 | }, 88 | "backpack": ["鱼饵", "渔具维修工具"] 89 | }, 90 | "action_space": ["mov", "chat"], # 人物的动作空间(在实际执行的时候,场景的all_actions和人物action_space取交集) 91 | "memory": [ 92 | "从小就跟随父亲学习捕鱼技巧。", 93 | "曾多次出海捕鱼,积累丰富的经验。", 94 | "对海洋生态保护有着浓厚的兴趣。", 95 | "帮助其他渔民修理损坏的渔具。", 96 | "梦想拥有一艘自己的渔船,开展独立的渔业。" 97 | ] 98 | }, 99 | { 100 | "name": "猎人阿明", 101 | "desc": "猎人阿明是一位勇敢而机敏的猎人。他身材魁梧,肌肉发达,眼神犀利。阿明擅长追踪和狩猎各种野生动物,具有过人的耐力和狙击技巧。", 102 | "mood": "专注", 103 | "npc_state": { 104 | "position": "猎人小屋", 105 | "observation": { 106 | "people": [], 107 | "items": ["猎枪", "弓箭", "追踪装备", "野外求生工具"], 108 | "locations": ["猎人小屋内部", "周围的森林"] 109 | }, 110 | "backpack": ["干粮", "水壶", "急救包"] 111 | }, 112 | "action_space": ["mov", "chat"], # 人物的动作空间(在实际执行的时候,场景的all_actions和人物action_space取交集) 113 | "memory": [ 114 | "从小生活在山区,接受父亲的猎人训练。", 115 | "熟悉各种野生动物的习性和行踪。", 116 | "常常在附近的森林中追踪并捕获猎物。", 117 | "有着长时间在野外生存的经验。", 118 | "一日作息:清晨起床后进行锻炼和瞄准训练,白天进行狩猎和追踪,傍晚返回小屋整理装备并准备晚餐,晚上休息并回顾一天的狩猎经历。" 119 | ] 120 | } 121 | ], # 可以留空,默认按照scene.json初始化场景NPC。非空则在之前基础上添加。 122 | } 123 | ``` 124 | 125 | 126 | -------------------------------------------------------------------------------- /nuwa/doc/docs/tutorials/quickstart.md: -------------------------------------------------------------------------------- 1 | ## 📜 快速开始 2 | 你可点击发行版本中的.bat文件来快速启动引擎,相关响应会在控制台中显示。 3 | ## 1.1 下载引擎 4 | [点击这里](https://nuwa-release.s3.us-west-2.amazonaws.com/index.html)下载最新的您系统的引擎发行版 5 | 6 | ## 1.2 启动引擎 7 | 每个发行版本双击即可启动引擎。执行过程的日志和收发包都会记录在logs文件夹下。 8 | ## 1.3 脚本交互(python) 9 | 引擎监听8084端口,游戏端监听8199端口,引擎端和游戏端通过UDP数据包进行交互。 10 | 11 | 可以参照下面的python脚本👇来快速测试引擎的功能。 12 | 13 | (由于UDP的包机制,我们通过一些trick来实现了大数据包的传输,具体的实现细节可以参考[这里](#udp数据包)) 14 | ```python 15 | import socket 16 | import json 17 | import uuid 18 | import time 19 | import threading 20 | 21 | """ 22 | 该脚本用于测试引擎的功能,包括: 23 | 1. 发送数据包到引擎8199端口(Game -> Engine) 24 | 2. 在8084监听回传给游戏的数据包(Game <- Engine) 25 | """ 26 | 27 | 28 | # 监听的地址和端口 29 | UDP_IP = "::1" 30 | UDP_PORT = 8084 31 | 32 | # 发送数据的地址和端口 33 | engine_url = "::1" 34 | engine_port = 8199 35 | 36 | # 创建socket 37 | sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 38 | sock.bind((UDP_IP, UDP_PORT)) 39 | 40 | # 准备数据包 41 | init_packet = { 42 | "func": "init", 43 | "scene_name": "雁栖村", 44 | "language": "C", 45 | "npc": [] 46 | } 47 | wakeup_packet = { 48 | "func": "wake_up", 49 | "npc_name": "王大妈", 50 | "scenario_name": "李大爷家", 51 | "npc_state": { 52 | "position": "李大爷家卧室", 53 | "observation": { 54 | "people": ["李大爷", "村长", "隐形李飞飞"], 55 | "items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床[包括:被子、枕头、床单、床垫、私房钱]"], 56 | "locations": ["李大爷家大门", "李大爷家后门", "李大爷家院子"] 57 | }, 58 | "backpack": ["优质西瓜", "大砍刀", "黄金首饰"] 59 | }, 60 | "time": "2021-01-01 12:00:00", 61 | } 62 | 63 | def send_data(data, max_packet_size=6000): 64 | # UUID作为消息ID 65 | msg_id = uuid.uuid4().hex 66 | # 将json字符串转换为bytes 67 | data = json.dumps(data).encode('utf-8') 68 | # 计算数据包总数 69 | packets = [data[i: i + max_packet_size] for i in range(0, len(data), max_packet_size)] 70 | total_packets = len(packets) 71 | for i, packet in enumerate(packets): 72 | # 构造UDP数据包头部 73 | header = f"{msg_id}@{i + 1}@{total_packets}".encode('utf-8') 74 | # 发送UDP数据包 75 | sock.sendto(header + b"@" + packet, (engine_url, engine_port)) 76 | print(f"Sent UDP packet: {header.decode('utf-8')}@{packet.decode('utf-8')}") 77 | 78 | def listen(): 79 | print("Listening on [{}]:{}".format(UDP_IP, UDP_PORT)) 80 | while True: 81 | data, addr = sock.recvfrom(4000) 82 | # get json packet from udp 83 | data = data.decode('utf-8') 84 | json_str = data.split('@')[-1] 85 | json_data = json.loads(json_str) 86 | print("Received UDP packet from {}: {}".format(addr, json_data)) 87 | 88 | def send_packets(): 89 | while True: 90 | send_data(init_packet) 91 | send_data(wakeup_packet) 92 | time.sleep(10) 93 | 94 | # 分别启动监听和发送数据包的线程 95 | threading.Thread(target=listen).start() 96 | threading.Thread(target=send_packets).start() 97 | 98 | 99 | ``` 100 | 101 | ## 📜 UDP数据包收发 102 | Engine目前发送的响应UDP包是会动态扩增的,如果响应超过了最大包限制,那么Engine就会拆分发送。 103 | 104 | 收发包的统一结构为: 105 | ```python 106 | # {msg_id}@{i + 1}@{total_packets}@{json_data} 107 | # 例(引擎响应包): 4bfe6122618b41fa85c8a8eb3ab37993@1@1@{"name": "action", "action": "chat", "object": "李大爷", "parameters": "李大爷,您知道匈房在哪里吗?", "npc_name": "王大姐"} 108 | ``` 109 | ## 2.1 收发包(python) 110 | ```python 111 | import uuid, json, socket 112 | 113 | def send_data(data, max_packet_size=6000): 114 | engine_url = "::1" 115 | engine_port = 8199 116 | game_url = "::1" 117 | game_port = 8084 118 | sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 119 | sock.bind((game_url, game_port)) 120 | 121 | # UUID作为消息ID 122 | msg_id = uuid.uuid4().hex 123 | # 将json字符串转换为bytes 124 | data = json.dumps(data).encode('utf-8') 125 | # 计算数据包总数 126 | packets = [data[i: i + max_packet_size] for i in range(0, len(data), max_packet_size)] 127 | total_packets = len(packets) 128 | for i, packet in enumerate(packets): 129 | # 构造UDP数据包头部 130 | header = f"{msg_id}@{i + 1}@{total_packets}".encode('utf-8') 131 | # 发送UDP数据包 132 | sock.sendto(header + b"@" + packet, (engine_url, engine_port)) 133 | sock.close() 134 | 135 | # 准备数据包 136 | init_packet = { 137 | "func": "init", 138 | # 必填字段,代表在什么场景初始化 139 | "scene_name": "雁栖村", 140 | "language": "C", 141 | # 下面是🉑️选 142 | "npc": [] 143 | } 144 | wakeup_packet = { 145 | "func": "wake_up", 146 | "npc_name": "王大妈", 147 | "scenario_name": "李大爷家", 148 | "npc_state": { 149 | "position": "李大爷家卧室", 150 | "observation": { 151 | "people": ["李大爷", "村长", "隐形李飞飞"], 152 | "items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床[包括:被子、枕头、床单、床垫、私房钱]"], 153 | "locations": ["李大爷家大门", "李大爷家后门", "李大爷家院子"] 154 | }, 155 | "backpack": ["优质西瓜", "大砍刀", "黄金首饰"] 156 | }, 157 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 158 | } 159 | # 发送数据包(发送后观察日志或终端) 160 | send_data(init_packet) 161 | send_data(wakeup_packet) 162 | ``` 163 | ## 2.2 收发包(C#) 164 | 在Unity中也是需要对数据包进行处理才可以进行发送的,下面是一个简单的Unity脚本,可以参考一下。 165 | 166 | ```c# 167 | // 发送UDP包的逻辑 168 | private void SendData(object data) 169 | { 170 | string json = JsonUtility.ToJson(data); // 提取数据data中的字符串信息 171 | json = $"@1@1@{json}"; // 左添加头信息 172 | byte[] bytes = Encoding.UTF8.GetBytes(json); // 对字符串数据编码 173 | this.sock.Send(bytes, bytes.Length, this.targetUrl, this.targetPort); // 通过socket向目标端口发送数据包 174 | } 175 | 176 | this.listenThread = new Thread(Listen); 177 | this.listenThread.Start(); 178 | this.thread_stop = false; 179 | 180 | //接收UDP包的逻辑(UDP包拼接为完整包) 181 | public void Listen() 182 | { 183 | IPEndPoint localEndPoint = new IPEndPoint(IPAddress.IPv6Loopback, this.listenPort); 184 | this.sock.Client.Bind(localEndPoint); 185 | 186 | string receivedData = ""; # 初始化receivedData用于整合接收到的字符串 187 | while (!this.thread_stop) // 持续监听引擎端发送的数据包 188 | { 189 | byte[] data = this.sock.Receive(ref localEndPoint); // 接收到数据包 190 | string packet = Encoding.UTF8.GetString(data); // 将接收到的数据包转化为字符串 191 | string[] parts = packet.Split('@'); // 将字符串按照@字符进行分段并得到片段序列 192 | string lastPart = parts[parts.Length - 1]; // 片段序列中的最后一段对应引擎端要传送的具体内容 193 | receivedData+=lastPart; // 将内容拼接到ReceivedData后面 194 | if (receivedData.EndsWith("}")) //多包机制下,此为收到最后一个包 195 | { 196 | //接下来对整合好的ReceivedData做下列分支判断和后处理 197 | if (receivedData.Contains("\"inited")) // 如果有inited字段 198 | { 199 | num_initialized += 1; 200 | UnityEngine.Debug.Log($"Successful initialization. {num_initialized}"); 201 | 202 | }else if (receivedData.Contains("\"conversation")) // 如果有conversation字段 203 | { 204 | ReceiveConvFormat json_data = JsonUtility.FromJson(receivedData); 205 | UnityEngine.Debug.Log($"Get Conversation.{JsonUtility.ToJson(json_data)}"); 206 | receivedConvs.Add(json_data); flag_ConvReceived = true; 207 | 208 | }else if (receivedData.Contains("\"action")) // 如果有action字段 209 | { 210 | FullActionFormat json_data = JsonUtility.FromJson(receivedData); 211 | UnityEngine.Debug.Log($"Get Action. {JsonUtility.ToJson(json_data)}"); 212 | receivedActions.Add(json_data); flag_ActReceived = true; 213 | } 214 | receivedData = ""; //收到最后一个包,重置收到的内容 215 | } 216 | } 217 | } 218 | ``` 219 | -------------------------------------------------------------------------------- /nuwa/doc/docs/tutorials/scenario.md: -------------------------------------------------------------------------------- 1 | ## Scenario配置总览 2 | 场景配置文件在project/config/knowledge/scenes/文件夹下,以json格式存储。 3 | 4 | Scenario规范了子场景中的路点,动作空间,情绪空间等。更重要的是决定了哪些NPC会在该场景下被加载到内存中。 5 | 6 | 场景.json通过init包初始化,决定初始化哪些NPC和这些NPC的动作空间。 7 | 8 | Scenario文件是NPC Engine的核心配置文件,它组织了NPC、Action和Knowledge的关系。 9 | 10 | ## Scenario配置方法 11 | Scenario(场景)例子如下: 12 | ```python 13 | # project/config/knowledge/scenes/警察局.json 14 | { 15 | "all_actions": ["mov", "get", "put", "use"], # 场景中可支持的Action类型 16 | "all_places": ["牢房", "雁栖村入口"], # 场景中的子地点(用于NPC移动的参数,可视作场景子地点) 17 | "all_moods": ["正常", "焦急", "严肃", "开心", "伤心"], # 场景中可支持的情绪,一般情况下所有场景的情绪信息是一致的 18 | "all_people": ["囚犯阿呆","警员1","警员2"] # 场景中的存在的NPC,会在[场景初始化]时加载到内存中 19 | } 20 | ``` 21 | 22 | ## 配置文件初始化 23 | Scenario配置文件在project/config/knowledge/scenes/文件夹下配置完成后,则会在引擎启动的时候被加载到内存中。 24 | 25 | 当游戏切入到一个场景的时候,需发送一个init包到引擎端,引擎会根据你发送的场景名字来加载对应的场景配置文件中的NPC。 26 | 27 | 一个初始化场景的例子如下: 28 | ```python 29 | # project/config/knowledge/scenes/警察局.json 30 | { 31 | "func": "init", # 初始化场景 32 | "scene_name": "警察局", # 场景名字,会去加载对应的场景配置文件.json 33 | "language": "C", # 语言 34 | "npc": [] # 场景中的存在的NPC,会在[场景初始化]时加载到内存中 35 | } 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /nuwa/doc/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: CogniMatrix NPC Engine 2 | repo_url: https://github.com/casia22/npc_engine.github.io/ 3 | # repo_name: casia22/npc_engine.github.io 4 | nav: 5 | - Home: index.md 6 | - Tutorial: 7 | - Quickstart: tutorials/quickstart.md 8 | - 0 - Engine Configuration: tutorials/engine.md 9 | - 1 - NPC Configuration: tutorials/npc.md 10 | - 2 - Scenario Configuration: tutorials/scenario.md 11 | - 3 - Action Configuration: tutorials/action.md 12 | - 4 - Debugging: tutorials/debug.md 13 | - Modules: 14 | - Conversation Module: modules/conversation.md 15 | - Action Module: modules/action.md 16 | - Talk Module: modules/talk.md 17 | - Download: 18 | - Windows: https://nuwa-release.s3.us-west-2.amazonaws.com/index.html 19 | - Mac(intel): https://nuwa-release.s3.us-west-2.amazonaws.com/index.html 20 | - Mac(apple silicon): https://nuwa-release.s3.us-west-2.amazonaws.com/index.html 21 | - linux: https://nuwa-release.s3.us-west-2.amazonaws.com/index.html 22 | - Feedback: feedback.md 23 | -------------------------------------------------------------------------------- /nuwa/material/badges/pylint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | pylint 17 | pylint 18 | 19 | 20 | 0.1 21 | 0.1 22 | 23 | 24 | -------------------------------------------------------------------------------- /nuwa/material/badges/pytest.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | pytest 17 | pytest 18 | 19 | 20 | 0 21 | 0 22 | 23 | 24 | -------------------------------------------------------------------------------- /nuwa/material/templates/template.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/material/templates/template.zip -------------------------------------------------------------------------------- /nuwa/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "npc-engine" 3 | version = "0.1.0" 4 | description = "one that empowers massive NPC" 5 | authors = ["CongniMatrix"] 6 | readme = "README.md" 7 | packages = [{include = "nuwa"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.9" 11 | langchain = "^0.0.220" 12 | openai = "^0.27.8" 13 | anybadge = "^1.14.0" 14 | pickledb = "^0.9.2" 15 | pinecone-client = "^2.2.2" 16 | pytest = "^7.4.0" 17 | urllib3 = "1.26.16" 18 | sentence-transformers = "2.2.2" 19 | 20 | 21 | [build-system] 22 | requires = ["poetry-core"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /nuwa/requirements.txt: -------------------------------------------------------------------------------- 1 | anybadge==1.14.0 2 | boto3==1.28.49 3 | colorama==0.4.6 4 | faiss_cpu==1.7.4 5 | nest_asyncio==1.5.6 6 | numpy==1.25.0 7 | openai==0.27.8 8 | pickleDB==0.9.2 9 | pytest==7.4.0 10 | Requests==2.31.0 11 | sentence_transformers==2.2.2 12 | google.generativeai == 0.4.0 13 | -------------------------------------------------------------------------------- /nuwa/run_code_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import anybadge 4 | 5 | # 将你的项目路径替换为'your_project' 6 | project_path = './' 7 | 8 | # 使用pylint对你的项目进行评分 9 | command = 'pylint ./' 10 | score = 0.1 11 | process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 12 | out, err = process.communicate() 13 | 14 | # 获取评分 15 | lines = out.decode('utf-8').split('\n') 16 | for line in lines: 17 | if 'Your code has been rated at' in line: 18 | score = float(line.split('/')[0].split(' ')[-1]) 19 | # 使用anybadge生成徽章 20 | badge = anybadge.Badge('pylint', score, thresholds={2: 'red', 4: 'orange', 8: 'yellow', 10: 'green'}) 21 | print("code score:",score) 22 | 23 | # 将徽章保存到文件 24 | badge.write_badge('./material/badges/pylint.svg',overwrite=True) 25 | 26 | import os 27 | import subprocess 28 | import pytest 29 | import anybadge 30 | 31 | # 将你的项目路径替换为'your_project' 32 | project_path = './' 33 | 34 | # 获取总的测试用例数 35 | total_tests = 0 36 | for root, dirs, files in os.walk(project_path): 37 | for file in files: 38 | if file.endswith('.py'): 39 | with open(os.path.join(root, file), 'r') as f: 40 | content = f.read() 41 | total_tests += content.count('def test_') 42 | 43 | # 执行测试 44 | passed_tests = 0 45 | for root, dirs, files in os.walk(project_path): 46 | for file in files: 47 | if file.endswith('.py'): 48 | try: 49 | result = pytest.main([os.path.join(root, file), "-q"]) 50 | if result == 0: 51 | passed_tests += 1 52 | except: 53 | continue 54 | 55 | # 计算百分比 56 | if total_tests > 0: 57 | score = (passed_tests / total_tests) * 100 58 | else: 59 | score = 0 60 | 61 | sc = score 62 | # 使用anybadge生成徽章 63 | 64 | badge = anybadge.Badge('pytest', sc, thresholds={20: 'red', 40: 'orange', 60: 'yellow', 80: 'green', 100: 'brightgreen'}) 65 | 66 | # 将徽章保存到文件,如果文件已经存在,就覆盖它 67 | badge.write_badge('./material/badges/pytest.svg', overwrite=True) 68 | -------------------------------------------------------------------------------- /nuwa/sender.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "ccb1b28a-546f-4019-a885-38dd8555aa24", 7 | "metadata": { 8 | "tags": [] 9 | }, 10 | "outputs": [], 11 | "source": [ 12 | "import socket\n", 13 | "import json\n", 14 | "import threading\n", 15 | "\n", 16 | "\n", 17 | "class Game:\n", 18 | " def __init__(self, target_url=\"::\", target_port=8199, listen_port=8084):\n", 19 | " self.target_url = target_url\n", 20 | " self.target_port = target_port\n", 21 | " self.listen_port = listen_port\n", 22 | " self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)\n", 23 | " self.listen_thread = threading.Thread(target=self.listen)\n", 24 | " self.listen_thread.start()\n", 25 | "\n", 26 | " def listen(self):\n", 27 | " self.sock.bind(('::1', self.listen_port))\n", 28 | " while True:\n", 29 | " data, addr = self.sock.recvfrom(1024)\n", 30 | " try:\n", 31 | " json_data = json.loads(data.decode())\n", 32 | " print(json_data)\n", 33 | " except json.JSONDecodeError:\n", 34 | " pass\n", 35 | "\n", 36 | " def init_engine(self):\n", 37 | " init_data = {\n", 38 | " \"func\": \"init\",\n", 39 | " \"npc\": [\n", 40 | " {\"name\": \"李大爷\", \"desc\": \"是个好人\", \"mood\": \"正常\", \"location\": \"李大爷家\", \"memory\": []},\n", 41 | " {\"name\": \"王大妈\", \"desc\": \"是个好人\", \"mood\": \"焦急\", \"location\": \"王大妈家\", \"memory\": []}\n", 42 | " ],\n", 43 | " \"knowledge\": {\n", 44 | " \"actions\": [\"stay\", \"move\", \"chat\"],\n", 45 | " \"place\": [\"李大爷家\", \"王大妈家\", \"广场\", \"瓜田\", \"酒吧\", \"警局\"],\n", 46 | " \"moods\": [\"正常\", \"焦急\", \"严肃\", \"开心\", \"伤心\"],\n", 47 | " \"people\": [\"李大爷\", \"王大妈\", \"村长\", \"警长\"]\n", 48 | " }\n", 49 | " }\n", 50 | " self.send_data(init_data)\n", 51 | "\n", 52 | " def generate_conversation(self, npc, location, topic, iterrupt_speech):\n", 53 | " conversation_data = {\n", 54 | " \"func\": \"conversation\",\n", 55 | " \"npc\": npc,\n", 56 | " \"location\": location,\n", 57 | " \"topic\": topic,\n", 58 | " \"iterrupt_speech\": iterrupt_speech\n", 59 | " }\n", 60 | " self.send_data(conversation_data)\n", 61 | " return conversation_data\n", 62 | "\n", 63 | " def confirm_conversation(self, conversation_id, index):\n", 64 | " confirm_data = {\n", 65 | " \"func\": \"confirm_conversation_line\",\n", 66 | " \"conversation_id\": conversation_id,\n", 67 | " \"index\": index\n", 68 | " }\n", 69 | " self.send_data(confirm_data)\n", 70 | "\n", 71 | " def send_data(self, data):\n", 72 | " self.sock.sendto(json.dumps(data).encode(), (self.target_url, self.target_port))\n", 73 | " " 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 3, 79 | "id": "49d18fb9-3702-4e80-99a8-45b0c24d41d3", 80 | "metadata": { 81 | "tags": [] 82 | }, 83 | "outputs": [], 84 | "source": [ 85 | "game = Game()" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 4, 91 | "id": "9ff41e61-47d2-4f69-bce1-3e40a5e8ed28", 92 | "metadata": { 93 | "tags": [] 94 | }, 95 | "outputs": [], 96 | "source": [ 97 | "game.init_engine()" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 5, 103 | "id": "8db35f28-81d0-4158-8a54-2d1650c413fa", 104 | "metadata": { 105 | "tags": [] 106 | }, 107 | "outputs": [ 108 | { 109 | "name": "stdout", 110 | "output_type": "stream", 111 | "text": [ 112 | "{'name': 'conversation', 'id': '4520301b-fecd-4e07-a057-352c55816b54', 'length': 24, 'lines': []}\n" 113 | ] 114 | } 115 | ], 116 | "source": [ 117 | "res = game.generate_conversation([\"李大爷\", \"王大妈\", \"村长\"], \"酒吧\", \"村长的紫色内裤\", \"你好我是玩家,你们在干什么?\")" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "id": "230d8eb4-5ccc-46e7-af2c-1cf3e8cc9c05", 124 | "metadata": { 125 | "tags": [] 126 | }, 127 | "outputs": [], 128 | "source": [] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 6, 133 | "id": "cf58229d-90d3-49a5-9095-9c57b5b5fc60", 134 | "metadata": { 135 | "tags": [] 136 | }, 137 | "outputs": [], 138 | "source": [ 139 | "\n", 140 | "game.confirm_conversation(\"4520301b-fecd-4e07-a057-352c55816b54\", 24)" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": 7, 146 | "id": "f04e4dfd-97b0-4cb1-b739-c1733a4f6522", 147 | "metadata": {}, 148 | "outputs": [ 149 | { 150 | "name": "stdout", 151 | "output_type": "stream", 152 | "text": [ 153 | "/disk2/workspace/csgoai/stone/project/nuwa/convai\n" 154 | ] 155 | } 156 | ], 157 | "source": [ 158 | "!pwd" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "id": "b5bc045f-9518-4649-8b43-cde99dcaed26", 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [] 168 | } 169 | ], 170 | "metadata": { 171 | "kernelspec": { 172 | "display_name": "Python 3 (ipykernel)", 173 | "language": "python", 174 | "name": "python3" 175 | }, 176 | "language_info": { 177 | "codemirror_mode": { 178 | "name": "ipython", 179 | "version": 3 180 | }, 181 | "file_extension": ".py", 182 | "mimetype": "text/x-python", 183 | "name": "python", 184 | "nbconvert_exporter": "python", 185 | "pygments_lexer": "ipython3", 186 | "version": "3.9.12" 187 | } 188 | }, 189 | "nbformat": 4, 190 | "nbformat_minor": 5 191 | } 192 | -------------------------------------------------------------------------------- /nuwa/src/Nuwa.py: -------------------------------------------------------------------------------- 1 | """ 2 | 面向用户程序的入口脚本,会被打包为exe/pkg/dmg等 3 | 条件: 4 | 项目假设在当前目录下有project文件夹,里面有config/llm_config.json 5 | 功能: 6 | 根据project文件夹的项目配置启动Nuwa 7 | """ 8 | 9 | import os 10 | from pathlib import Path 11 | from nuwa.src.engine import NPCEngine 12 | 13 | 14 | if __name__ == "__main__": 15 | # 获取PROJECT_DIR 16 | PROJECT_DIR = os.path.join(os.getcwd(), "project") 17 | # 检测PROJECT_DIR是否存在 18 | if not os.path.exists(PROJECT_DIR): 19 | raise FileNotFoundError("PROJECT_DIR=./project not found in current directory.") 20 | # 检测PROJECT_DIR下是否有config文件夹 21 | if not os.path.exists(os.path.join(PROJECT_DIR, "config")): 22 | raise FileNotFoundError("config folder not found in PROJECT_DIR.") 23 | # 检测PROJECT_DIR下是否有llm_config.json 24 | if not os.path.exists(os.path.join(PROJECT_DIR, "config", "llm_config.json")): 25 | raise FileNotFoundError("llm_config.json not found in PROJECT_DIR/config.") 26 | # 启动Nuwa 27 | engine = NPCEngine(project_root_path=Path(PROJECT_DIR)) 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /nuwa/src/__init__.py: -------------------------------------------------------------------------------- 1 | #from nuwa.src.npc import * 2 | #from nuwa.src.config.config import * 3 | #from nuwa.src.config.template import * 4 | -------------------------------------------------------------------------------- /nuwa/src/config/__init__.py: -------------------------------------------------------------------------------- 1 | #from nuwa.src.config.config import * 2 | -------------------------------------------------------------------------------- /nuwa/src/config/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Filename: config.py 3 | Author: Mengshi*, Yangzejun 4 | Contact: ..., yzj_cs_ilstar@163.com 5 | """ 6 | from pathlib import Path 7 | 8 | 9 | # NUWA主路径 10 | CODE_ROOT_PATH = Path(__file__).parent.parent.parent 11 | MODEL_BASE_PATH = CODE_ROOT_PATH / "material" / "models" 12 | 13 | # KEYS 14 | ZHIPU_KEY = "3fe121b978f1f456cfac1d2a1a9d8c06.iQsBvb1F54iFYfZq" 15 | OPENAI_KEY = "sk-hJs89lkQMzlzoOcADb6739A9091d41229c2c3c0547932fBe" 16 | OPENAI_BASE = "https://api.qaqgpt.com/v1" 17 | OPENAI_MODEL = "gpt-3.5-turbo-16k" 18 | 19 | 20 | # get your token in http://hf.co/settings/tokens 21 | HF_TOKEN = "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" 22 | 23 | HF_EMBEDDING_GANYMEDENIL = { 24 | # https://huggingface.co/GanymedeNil/text2vec-large-chinese 25 | "model_id": "GanymedeNil/text2vec-large-chinese", 26 | "dim": 768, 27 | "hf_url": "https://api-inference.huggingface.co/pipeline/feature-extraction/GanymedeNil/text2vec-large-chinese", 28 | } 29 | 30 | HF_EMBEDDING_SBERT_CHINESE = { 31 | # https://huggingface.co/uer/sbert-base-chinese-nli 32 | "model_id": "uer/sbert-base-chinese-nli", 33 | "dim": 768, 34 | "hf_url": "https://api-inference.huggingface.co/pipeline/feature-extraction/uer/sbert-base-chinese-nli", 35 | } 36 | 37 | HF_EMBEDDING_MINILM = { 38 | # https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2 39 | "model_id": "sentence-transformers/all-MiniLM-L6-v2", 40 | "dim": 384, 41 | "hf_url": "https://api-inference.huggingface.co/pipeline/feature-extraction/sentence-transformers/all-MiniLM-L6-v2", 42 | } 43 | HF_EMBEDDING_MULTIMINILM = { 44 | # https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 45 | "model_id": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", 46 | "dim": 384, 47 | "hf_url": "https://api-inference.huggingface.co/pipeline/feature-extraction/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", 48 | } 49 | 50 | 51 | PINECONE_CONFIG = { 52 | "pinecone_api_key": "c977466b-6661-4caf-b281-81655366d149", 53 | "pinecone_environment": "us-west1-gcp-free", 54 | "pinecone_index_name": "npc-engine", 55 | "pinecone_index_dim": 384, 56 | } 57 | 58 | NPC_MEMORY_CONFIG = { 59 | # pinecone 60 | "pinecone_api_key": PINECONE_CONFIG["pinecone_api_key"], 61 | "pinecone_environment": PINECONE_CONFIG["pinecone_environment"], 62 | "pinecone_index_name": PINECONE_CONFIG["pinecone_index_name"], 63 | "pinecone_index_dim": PINECONE_CONFIG["pinecone_index_dim"], 64 | # huggingface 65 | "hf_token": HF_TOKEN, 66 | "hf_model_id": HF_EMBEDDING_MULTIMINILM["model_id"], 67 | "hf_dim": HF_EMBEDDING_MULTIMINILM["dim"], 68 | "hf_api_url": HF_EMBEDDING_MULTIMINILM["hf_url"], 69 | "hf_headers": {"Authorization": f"Bearer {HF_TOKEN}"}, 70 | "hf_embedding_online": False, # 默认离线推理模型 71 | # db 72 | "db_dir": "./npc_memory.db", 73 | } 74 | 75 | # assert dim of hf model == dim of pinecone index 76 | assert ( 77 | PINECONE_CONFIG["pinecone_index_dim"] == NPC_MEMORY_CONFIG["hf_dim"] 78 | ), "dim of hf model != dim of pinecone index" -------------------------------------------------------------------------------- /nuwa/src/config/generate_path.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/src/config/generate_path.py -------------------------------------------------------------------------------- /nuwa/src/npc/__init__.py: -------------------------------------------------------------------------------- 1 | #from nuwa.src.npc.npc import * 2 | #from nuwa.src.npc.conversation import * 3 | -------------------------------------------------------------------------------- /nuwa/src/npc/action.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List, Dict, Any, Tuple 3 | import re 4 | 5 | 6 | class ActionItem: 7 | def __init__(self, name: str, definition: str, example: str, log_template: Dict[str, str], 8 | multi_param: bool = False): 9 | self.name = name 10 | self.definition = definition 11 | self.multi_param = multi_param 12 | self.example = example 13 | self.log_template = log_template 14 | self.vec = "" 15 | """ 16 | log_template的例子: 17 | {npc_name} 成功地从{object}获得{parameters} 18 | {npc_name} 未能从{object}获得{parameters}。原因:{reason} 19 | """ 20 | 21 | @staticmethod 22 | def str2json(string: str) -> Dict[str, Any]: 23 | """ 24 | 从字符串中提取动作和参数 25 | :param string: 26 | :return: Dict[str, Any] 27 | """ 28 | string = string.replace('|', '|').replace(',', ',') 29 | print(string) 30 | # string = string.strip("<").strip(">") 31 | pattern = r'<(\w+)\|([^|]+)\|?([^|>]*)>' 32 | matches = re.findall(pattern, string) 33 | if matches: 34 | function_name, obj, param = matches[0] 35 | return {'action': function_name, 'object': obj, 'parameters': param.split(',')} 36 | return {'action': "", 'object': "", 'parameters': ""} 37 | 38 | def get_log(self, action_status: str, npc_name: str, obj: str, parameters: List[str], reason: str) -> str: 39 | """ 40 | 使用其日志模版,转换为自然语言记录在记忆中,方便语义检索 41 | 输出例: 42 | 李大爷成功 从 箱子 拿取 西瓜汁,桃子,枕头 43 | 李大爷未能 从 箱子 拿取 西瓜汁,桃子,枕头。原因:箱子里没有西瓜汁 44 | 45 | 李大爷成功 打开 门 46 | 李大爷未能 打开 门。原因:门已经打开了 47 | 48 | 李大爷成功 使用 刀 砍 西瓜 49 | 李大爷未能 使用 刀 砍 西瓜。原因:附近没有西瓜 50 | log_template的例子: 51 | {npc_name}成功地从{object}获得{parameters} 52 | {npc_name}未能从{object}获得{parameters}。原因:{reason} 53 | """ 54 | # 若动作处理成功就不加载reason 55 | if action_status == "success": 56 | return self.log_template['success'].format(npc_name=npc_name, object=obj, 57 | parameters=','.join(parameters)) 58 | 59 | return self.log_template['fail'].format(npc_name=npc_name, object=obj, 60 | parameters=','.join(parameters), reason=reason) 61 | -------------------------------------------------------------------------------- /nuwa/src/npc/knowledge.py: -------------------------------------------------------------------------------- 1 | """ 2 | 负责存储地点、人物等知识配置文件的Public knowledge类 3 | """ 4 | import json 5 | import logging 6 | import os 7 | from typing import List, Dict 8 | from pathlib import Path 9 | import concurrent.futures 10 | 11 | 12 | class SceneConfig: 13 | """ 14 | SceneConfig类,用于初始化和存储场景配置。 15 | """ 16 | 17 | def __init__(self, config_name: str, project_root_path: Path): 18 | """ 19 | 初始化SceneConfig类。 20 | 21 | 参数: 22 | config_name (str): 配置文件名(不包括.json扩展名) 23 | """ 24 | # 路径配置 25 | self.PROJECT_ROOT_PATH = project_root_path 26 | self.CONFIG_PATH = self.PROJECT_ROOT_PATH / "config" 27 | self.config_name = config_name 28 | self.all_actions: List[str] = [] 29 | self.all_places: List[str] = [] 30 | self.all_moods: List[str] = [] 31 | self.all_people: List[str] = [] 32 | 33 | with open(self.CONFIG_PATH / "knowledge" / "scenes" / (self.config_name + ".json"), "r", encoding="utf-8") as file: 34 | scenario_json = json.load(file) 35 | 36 | self.all_actions = scenario_json.get('all_actions', []) 37 | self.all_places = scenario_json.get('all_places', []) 38 | self.all_moods = scenario_json.get('all_moods', []) 39 | self.all_people = scenario_json.get('all_people', []) 40 | 41 | 42 | 43 | class PublicKnowledge: 44 | """ 45 | PublicKnowledge类,用于加载和存储所有场景配置。 46 | 它会在引擎刚启动的时候 47 | """ 48 | 49 | def __init__(self, project_root_path:Path, debug_mode=False): 50 | """ 51 | 初始化PublicKnowledge类。 52 | """ 53 | # LOGGER配置 54 | self.logger = logging.getLogger("KNOWLEDGE") 55 | # path配置 56 | self.PROJECT_ROOT_PATH = project_root_path 57 | self.CONFIG_PATH = project_root_path / "config" 58 | 59 | self.scene_config_kv: Dict[str, SceneConfig] = {} 60 | self.debug_mode = debug_mode 61 | self._load_configs() 62 | # ["雁栖村","雁栖村丛林","雁栖村矿场","王大妈家","李大爷家","警察局"] 等等 63 | self.scene_names_knowledge: List[str] = self.get_config_names() 64 | self.logger.info("Public knowledge loaded.") 65 | 66 | def get_config_names(self) -> List[str]: 67 | """ 68 | 返回所有场景配置的名称。 69 | 70 | 返回: 71 | List[str]: 所有场景配置的名称 72 | """ 73 | return list(self.scene_config_kv.keys()) 74 | 75 | 76 | def _load_configs(self): 77 | """ 78 | 加载所有的配置文件到字典中。 79 | """ 80 | with concurrent.futures.ThreadPoolExecutor() as executor: 81 | config_files = [file for file in os.listdir(self.CONFIG_PATH / "knowledge" / "scenes") if file.endswith('.json')] 82 | futures = {executor.submit(self._load_config, file): file for file in config_files} 83 | 84 | for future in concurrent.futures.as_completed(futures): 85 | file = futures[future] 86 | try: 87 | config_name, scene_config = future.result() 88 | self.scene_config_kv[config_name] = scene_config 89 | except Exception as exc: 90 | print(f'{file} generated an exception: {exc}') 91 | self.logger.debug(f"Loaded scenario knowledge: {config_files}") 92 | 93 | def _load_config(self, file: str) -> tuple: 94 | """ 95 | 加载单个配置文件并返回配置名和SceneConfig对象。 96 | 97 | 参数: 98 | file (str): 配置文件名 99 | 100 | 返回: 101 | tuple: 配置名和SceneConfig对象 102 | """ 103 | config_name = file[:-5] # 去掉.json后缀名 104 | scene_config = SceneConfig(config_name, self.PROJECT_ROOT_PATH) 105 | return config_name, scene_config 106 | 107 | def get_scene(self, scene_name: str) -> SceneConfig: 108 | """ 109 | 返回指定名称的SceneConfig对象。 110 | 111 | 参数: 112 | scene_name (str): 场景名称 113 | 114 | 返回: 115 | SceneConfig: 对应的SceneConfig对象 116 | """ 117 | return self.scene_config_kv.get(scene_name) 118 | 119 | def update_scene(self, scene_name: str, scene_config: SceneConfig): 120 | """ 121 | 更新指定名称的SceneConfig对象。 122 | 123 | 参数: 124 | scene_name (str): 场景名称 125 | scene_config (SceneConfig): 对应的SceneConfig对象 126 | """ 127 | self.scene_config_kv[scene_name] = scene_config 128 | 129 | def update_actions(self, scenario_name: str, content: List[str]): 130 | if scenario_name in self.scene_config_kv: 131 | self.scene_config_kv[scenario_name].all_actions = content 132 | 133 | def update_moods(self, scenario_name: str, content: List[str]): 134 | if scenario_name in self.scene_config_kv: 135 | self.scene_config_kv[scenario_name].all_moods = content 136 | 137 | def update_people(self, scenario_name: str, content: List[str]): 138 | if scenario_name in self.scene_config_kv: 139 | self.scene_config_kv[scenario_name].all_people = content 140 | 141 | def update_places(self, scenario_name: str, content: List[str]): 142 | if scenario_name in self.scene_config_kv: 143 | self.scene_config_kv[scenario_name].all_places = content 144 | 145 | def get_actions(self, scenario_name: str) -> List[str]: 146 | if scenario_name in self.scene_config_kv: 147 | return self.scene_config_kv[scenario_name].all_actions 148 | return [] 149 | 150 | def get_moods(self, scenario_name: str) -> List[str]: 151 | if scenario_name in self.scene_config_kv: 152 | return self.scene_config_kv[scenario_name].all_moods 153 | return [] 154 | 155 | def get_people(self, scenario_name: str) -> List[str]: 156 | if scenario_name in self.scene_config_kv: 157 | return self.scene_config_kv[scenario_name].all_people 158 | return [] 159 | 160 | def get_places(self, scenario_name: str) -> List[str]: 161 | if scenario_name in self.scene_config_kv: 162 | return self.scene_config_kv[scenario_name].all_places 163 | return [] 164 | 165 | def shutdown(self): 166 | """ 167 | 场景配置文件更新,如果是debug_mode就不更新到本地 168 | :return: 169 | """ 170 | if not self.debug_mode: 171 | for scenario_name, scene_config in self.scene_config_kv.items(): 172 | with open(self.CONFIG_PATH / "knowledge" / "scenes" / (scenario_name + ".json"), "w", 173 | encoding="utf-8") as file: 174 | json.dump({ 175 | 'all_actions': scene_config.all_actions, 176 | 'all_places': scene_config.all_places, 177 | 'all_moods': scene_config.all_moods, 178 | 'all_people': scene_config.all_people 179 | }, file) 180 | self.logger.info("Public knowledge shutdown.") 181 | 182 | 183 | if __name__ == "__main__": 184 | # 创建PublicKnowledge对象 185 | project_root_path = Path(__file__).parent.parent.parent.parent / "example_project" 186 | public_knowledge = PublicKnowledge(project_root_path=project_root_path) 187 | 188 | # 尝试获取一个场景配置 189 | scene_name = '雁栖村' # 请替换为你的实际场景名 190 | scene_config = public_knowledge.get_scene(scene_name) 191 | 192 | # 打印场景配置的信息 193 | if scene_config is not None: 194 | print(f"Config Name: {scene_config.config_name}") 195 | print(f"All Actions: {scene_config.all_actions}") 196 | print(f"All Places: {scene_config.all_places}") 197 | print(f"All Moods: {scene_config.all_moods}") 198 | print(f"All People: {scene_config.all_people}") 199 | else: 200 | print(f"No scene config found for {scene_name}") 201 | 202 | -------------------------------------------------------------------------------- /nuwa/src/npc/memory.py: -------------------------------------------------------------------------------- 1 | """ 2 | NPC的记忆处理类 3 | NPCMemory 4 | MemoryItem 5 | """ 6 | import hashlib 7 | import json 8 | import queue 9 | from pathlib import Path 10 | from typing import Any, Dict, List 11 | import numpy as np 12 | import logging, os 13 | from langchain.text_splitter import RecursiveCharacterTextSplitter 14 | 15 | from nuwa.src.config.config import NPC_MEMORY_CONFIG 16 | from nuwa.src.utils.database import PickleDB 17 | from nuwa.src.utils.embedding import LocalEmbedding, SingletonEmbeddingModel, BaseEmbeddingModel 18 | from nuwa.src.utils.faissdatabase import VectorDatabase 19 | 20 | 21 | class MemoryItem: 22 | def __init__(self, text: str, game_time: str, score: float = 0.0, **kwargs): 23 | self.text = text 24 | self.md5_hash = hashlib.md5(text.encode('utf-8')).hexdigest() 25 | self.game_time = game_time 26 | self.score = score 27 | 28 | def __str__(self): 29 | return f"MemoryItem(text={self.text},game_time={self.game_time},score={self.score},md5_hash={self.md5_hash})" 30 | 31 | def __repr__(self): 32 | return self.__str__() 33 | 34 | def to_json_str(self): 35 | return json.dumps(self.__dict__) 36 | 37 | @classmethod 38 | def from_json_str(cls, json_str): 39 | json_dict = json.loads(json_str) 40 | return cls(**json_dict) 41 | 42 | def set_score(self, score: float): 43 | self.score = score 44 | return self 45 | 46 | 47 | class NPCMemory: 48 | def __init__( 49 | self, 50 | npc_name: str, 51 | name_index: int, 52 | k: int, 53 | EmbeddingModel: BaseEmbeddingModel, 54 | project_root_path: Path = Path(os.getcwd()), # 确保这是一个Path对象 55 | the_npc_memory_config: Dict[str, Any] = NPC_MEMORY_CONFIG, 56 | ): 57 | self.logger = logging.getLogger("NPC_MEMORY") 58 | self.npc_name = f"{npc_name}_{str(name_index)}" 59 | self.npc_name_hash = hashlib.md5(self.npc_name.encode('utf-8')).hexdigest() 60 | self.latest_k = queue.Queue(maxsize=k) 61 | self.project_root_path = project_root_path 62 | 63 | # 确保这里处理的是Path对象,而不是字符串 64 | self.base_path = self.project_root_path / "data" # 确保这是Path对象 65 | self.vdb_path = self.base_path / self.npc_name_hash 66 | 67 | # 检查路径是否存在,没有则创建 68 | if not self.vdb_path.exists(): 69 | self.vdb_path.mkdir(parents=True) 70 | 71 | print("vdb_path", self.vdb_path) 72 | if self.vdb_path.exists(): 73 | print(f"'{self.vdb_path}' exists.") 74 | else: 75 | print(f"'{self.vdb_path}' does not exist.") 76 | self.MEMORY_DB_PATH = self.vdb_path / "npc_memory.db" 77 | 78 | # 将NPC姓名以及序号写入hash文件夹内的一个文件中 79 | self.npc_name_file = self.vdb_path / "npc_name.json" 80 | with open(self.npc_name_file, "w", encoding="utf-8") as f: 81 | json.dump({"npc_name": npc_name, "name_index": name_index}, f, ensure_ascii=False) 82 | 83 | # embedding model设置 84 | self.embedding_model = EmbeddingModel 85 | 86 | # vector database设置 87 | self.vector_database = VectorDatabase(dim=the_npc_memory_config["hf_dim"], npc_name=npc_name, 88 | npc_name_hash=self.npc_name_hash, 89 | vdb_path=self.vdb_path) 90 | 91 | # 如果向量数据库文件不存在,立即保存新创建的数据库 92 | if not self.vdb_path.exists(): 93 | self.vector_database.save() 94 | 95 | self.logger.debug(f"{self.npc_name} memory init done, k={k}, model_name=sbert-base-chinese-nli") 96 | 97 | # 数据库设置 98 | self.memory_db = PickleDB(self.MEMORY_DB_PATH) 99 | 100 | def embed_text(self, text: str) -> list: 101 | """使用用户指定的embedding模型对文本进行embedding,返回一个list 102 | 默认模型: 103 | LocalEmbedding(model_name="uer/sbert-base-chinese-nli", vector_width=768) 104 | 模型在引擎启动时初始化,可以通过修改配置文件更换模型以及是否本地化推理(配置文件也就是config.py)。 105 | """ 106 | vector = self.embedding_model.embed_text(input_string=text) 107 | return vector 108 | 109 | def add_memory_text(self, text: str, game_time: str, direct_upload: bool = False): 110 | """ 111 | 将一条新的记忆文本添加到机器人的记忆中。 112 | 记忆先被放入latest_k队列中,当队列满了之后,最老的记忆会被上传到向量数据库。 113 | game_time 是记忆文本对应的游戏时间戳,用于计算记忆的时效性。 114 | 115 | :param text: 新的记忆文本 116 | :param game_time: 记忆文本对应的游戏时间戳 117 | :param direct_upload: 是否直接上传到向量数据库 118 | """ 119 | # 构造记忆对象 120 | new_memory_item = MemoryItem(text, game_time) 121 | if direct_upload: 122 | self.add_memory(new_memory_item) # add_memory方法内部已经包含save调用 123 | return 124 | 125 | if self.latest_k.full(): 126 | old_memory_item = self.latest_k.get() 127 | self.add_memory(old_memory_item) # add_memory方法内部已经包含save调用 128 | 129 | self.latest_k.put(new_memory_item) 130 | # 注意:这里不需要调用save,因为这里只是将记忆加入到队列中 131 | 132 | def add_memory(self, memory_item: MemoryItem): 133 | """ 134 | 将一条需要持久化检索的记忆文本: 135 | 1.向量化 136 | 2.存入向量数据库 137 | 3.存入KV数据库 138 | :param memory_item: 139 | :return: 140 | """ 141 | embedding = self.embed_text(memory_item.text) 142 | self.vector_database.put(key=memory_item.md5_hash, vector=embedding) 143 | 144 | self.memory_db.set(key=memory_item.md5_hash, value=memory_item.to_json_str()) 145 | self.logger.debug(f"add memory {memory_item.md5_hash} done") 146 | self.vector_database.save() # 保存数据库更改 147 | 148 | def time_score(self, game_time: str, memory_game_time: str) -> float: 149 | """ 150 | 本来打算:计算记忆的时间分数,记忆越新分数越高。 151 | 实现:均匀给分,无视时间的差;也就是说只有相关度被考虑 152 | :param game_time: 当前游戏时间戳 153 | :param memory_game_time: 记忆的游戏时间戳 154 | :return: 155 | """ 156 | # TODO:实现记忆的时间分数 157 | # score = float(game_time) - float(memory_game_time) 158 | return 1 159 | 160 | def search_memory(self, query_text: str, query_game_time: str, k: int, top_p: float = 1) -> Dict[ 161 | str, List[MemoryItem]]: 162 | 163 | self.logger.debug(f"NPC:{self.npc_name} 开始搜索记忆, 检索语句为:{query_text},检索数量为:{k},top_p为:{top_p}") 164 | 165 | # 对query_text进行embedding 166 | query_embedding = self.embed_text(query_text) 167 | # self.logger.debug(f"Query embedding: {query_embedding}") 168 | 169 | # 从pinecone中搜索与query_text最相似的2k条记忆 170 | response = self.vector_database.search(vector=query_embedding, k=2 * k, thresh=0.8) 171 | # self.logger.debug(f"Vector database response: {response}") 172 | 173 | keys, distances = response 174 | vdb_response = [{"id": key, "score": distance} for key, distance in zip(keys, distances)] 175 | # self.logger.debug(f"vdb_response: {vdb_response}") 176 | 177 | match_items: List[MemoryItem] = [ 178 | MemoryItem.from_json_str(self.memory_db.get(match["id"])) for match in vdb_response 179 | ] 180 | self.logger.debug(f"Matched memory items: {match_items}") 181 | 182 | # 提取每个match到的MemoryItem中的cosine score 183 | match_scores: List[float] = [float(match["score"]) for match in vdb_response] 184 | # self.logger.debug(f"Match scores: {match_scores}") 185 | 186 | # MemoryItem中的game_time,结合query_game_time和cosine score筛选出k个importance最大的match 187 | time_scores: List[float] = [ 188 | self.time_score(match_item.game_time, query_game_time) 189 | for match_item in match_items 190 | ] 191 | # self.logger.debug(f"Time scores: {time_scores}") 192 | 193 | importance_scores: List[float] = [ 194 | time_score * match_score 195 | for time_score, match_score in zip(time_scores, match_scores) 196 | ] 197 | # self.logger.debug(f"Importance scores: {importance_scores}") 198 | 199 | match_items: List[MemoryItem] = [ 200 | item.set_score(score) for item, score in zip(match_items, importance_scores) 201 | ] 202 | 203 | # 选取最大的k个importance_scores所对应的match_items 204 | importance_scores_array: np.array = np.array(importance_scores) 205 | top_k_indices = np.argsort(importance_scores_array)[-k:] 206 | # self.logger.debug(f"Top k indices: {top_k_indices}") 207 | 208 | top_k_match_items: List[MemoryItem] = [match_items[index] for index in top_k_indices] 209 | 210 | # 将MemoryItem按importance_scores从大到小排序 211 | top_k_match_items.sort(key=lambda x: x.score, reverse=True) 212 | 213 | top_k_match_items_scores: List[float] = [match_item.score for match_item in top_k_match_items] 214 | softmax = lambda x: np.exp(x) / np.sum(np.exp(x)) 215 | softmax_scores = softmax(top_k_match_items_scores) 216 | sorted_indices = np.argsort(softmax_scores)[::-1] # 按得分降序排序 217 | cumulative_sum = np.cumsum(softmax_scores[sorted_indices]) 218 | 219 | # 寻找满足累积和>=top_p的最小索引 220 | selected_index = np.where(cumulative_sum >= top_p)[0] 221 | if selected_index.size > 0: 222 | selected_index = selected_index[0] + 1 # +1是因为索引是0-based,我们需要的是数量 223 | else: 224 | selected_index = len(top_k_match_items_scores) 225 | 226 | selected_items = [top_k_match_items[i] for i in sorted_indices[:selected_index]] 227 | 228 | # 和latest_k中的内容做合并成为related_memorys_list并返回 229 | related_memorys_list = { 230 | "related_memories": selected_items, 231 | "latest_memories": list(self.latest_k.queue), 232 | } 233 | self.logger.debug( 234 | f"NPC:{self.npc_name} 检索记忆完成,得分:{importance_scores}, 过滤后检索数量为:{len(selected_items)}, 检索结果为:{related_memorys_list}") 235 | 236 | return related_memorys_list 237 | 238 | def abstract_memory(self, importance_threshold): 239 | """摘要记忆""" 240 | # TODO: 抽取pincone数据库中最老的记忆进行摘要然后变为一条信息上传到pinecone 241 | pass 242 | 243 | def add_memory_file(self, file_path: Path, game_time: str, chunk_size: int = 50, chunk_overlap: int = 10): 244 | """ 245 | 将一个文本txt文件中的记忆,分片split长传到向量数据库作为记忆 246 | game_time 是上传文本的记忆时间戳,用于计算记忆的时效性。(可能没有什么意义) 247 | 248 | :param file_path: .txt结尾的文件 249 | :param game_time: 上传记忆文件对应的游戏时间戳 250 | """ 251 | # 读取文本并进行拆分 252 | with open(file_path, "r", encoding="utf-8") as file: 253 | input_text_file = file.read() 254 | text_splitter = RecursiveCharacterTextSplitter( 255 | # Set a tiny chunk size, just to show. 256 | chunk_size=chunk_size, 257 | chunk_overlap=chunk_overlap, 258 | length_function=len, 259 | add_start_index=True, 260 | ) 261 | texts: List = text_splitter.create_documents([input_text_file]) 262 | text_chunks: List[str] = [doc.page_content for doc in texts] 263 | # 构造记忆对象 264 | memory_items: List[MemoryItem] = [MemoryItem(text, game_time) for text in text_chunks] 265 | self.logger.info( 266 | f"NPC:{self.npc_name} 的文本记忆文件 {file_path} 拆分为{[len(each.text) for each in memory_items]}, 为{len(text_chunks)}个片段,每个片段长度为{chunk_size},重叠长度为{chunk_overlap}") 267 | self.logger.debug( 268 | f"NPC:{self.npc_name} 的文本记忆文件 {file_path} 拆分为{[each.text for each in memory_items]}") 269 | # 将记忆上传到向量数据库,存入KV数据库 270 | for memory_item in memory_items: 271 | self.add_memory(memory_item) 272 | self.logger.debug( 273 | f"NPC:{self.npc_name} 的文本记忆文件 {file_path} 的片段 {memory_item.text} 上传到向量数据库") 274 | 275 | def clear_memory(self): 276 | """ 277 | 清空向量数据库中的记忆 278 | 但是不清空KV数据库中的记忆 279 | """ 280 | self.vector_database.remove() 281 | self.logger.debug("NPC: {} 向量库记忆已清空".format(self.npc_name)) 282 | 283 | def shutdown(self): 284 | """关闭方法,将latest_k队列中的text按照语义上传到向量数据库,并存入KV数据库""" 285 | self.logger.debug("NPC: {} 的{}条向量库记忆上传中...".format(self.npc_name, self.latest_k.qsize())) 286 | while not self.latest_k.empty(): 287 | memory_item = self.latest_k.get() 288 | self.add_memory(memory_item) 289 | self.logger.debug(f"NPC{self.npc_name} 记忆 {memory_item.text} 的向量库记忆已上传") 290 | self.logger.debug("NPC: {} 的向量库记忆上传完成".format(self.npc_name)) 291 | 292 | 293 | def main(): 294 | # # logger设置 295 | logger = logging.getLogger(__name__) 296 | PROJECT_ROOT_PATH = Path(__file__).parent.parent.parent.parent / "example_project" 297 | # 298 | # """NPC测试""" 299 | embedder = LocalEmbedding(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", 300 | vector_width=384) 301 | npcM = NPCMemory(project_root_path=PROJECT_ROOT_PATH, npc_name="memory_test", k=3, EmbeddingModel=embedder) 302 | """ 303 | NPC 文件检索测试 304 | stone91_mem.txt 中包含AK武器介绍、喜羊羊的介绍,检索回复应该都是关于武器的而不是喜羊羊的 305 | """ 306 | # npcM.add_memory_file(file_path=PROJECT_ROOT_PATH / 'data' / 'stone91_mem.txt', 307 | # game_time="2021-08-01 12:00:00", chunk_size=100, chunk_overlap=10) 308 | print(npcM.search_memory("我想要攻击外星人,有什么趁手的装备吗?", "2021-08-01 12:00:00", k=3)) 309 | 310 | """ 311 | NPC问句检索测试 312 | 回复应当是关于AK47的而不是喜羊羊和食物的 313 | """ 314 | npcM.add_memory_text("AK47可以存放30发子弹", "2021-08-01 12:00:00") 315 | npcM.add_memory_text("我去年买了一个防弹衣", "2021-08-01 12:00:00") 316 | npcM.add_memory_text("我上午吃了大西瓜", "2021-08-01 12:00:00") 317 | npcM.add_memory_text("我中午吃了大汉堡", "2021-08-01 12:00:00") 318 | npcM.add_memory_text("我下午吃了小猪蹄", "2021-08-01 12:00:00") 319 | npcM.add_memory_text("我去年爱上了她,我的CSGO", "2021-08-01 12:00:00") 320 | npcM.add_memory_text("喜羊羊说一定要给我烤羊肉串吃", "2021-08-01 12:00:00") 321 | npcM.add_memory_text("喜羊羊说一定要给我烤羊肉串吃", "2021-08-01 12:00:00") 322 | npcM.add_memory_text("喜羊羊说一定要给我烤羊肉串吃", "2021-08-01 12:00:00") 323 | npcM.add_memory_text("喜羊羊说一定要给我烤羊肉串吃", "2021-08-01 12:00:00") 324 | print(npcM.search_memory("AK有多少发子弹?", "2021-08-01 12:00:00", k=3)) 325 | # npcM.shutdown() # 必须要关闭第一个NPC,否则第二个NPC无法正常运行 326 | """ 327 | NPC2 328 | """ 329 | 330 | npcM2 = NPCMemory(project_root_path=PROJECT_ROOT_PATH, npc_name="lintao", k=3, EmbeddingModel=embedder) 331 | """ 332 | NPC 文件检索测试 333 | stone91_mem.txt 中包含AK武器介绍、喜羊羊的介绍,检索回复应该都是关于武器的而不是喜羊羊的 334 | """ 335 | npcM2.add_memory_file(file_path=PROJECT_ROOT_PATH / 'data' / 'reinforcement_learning.txt', 336 | game_time="2021-08-01 12:00:00", chunk_size=100, chunk_overlap=10) 337 | print(npcM2.search_memory("深入理解多智能体环境", "2021-08-01 12:00:00", k=3)) 338 | 339 | """ 340 | NPC问句检索测试 341 | 回复应当是关于强化学习 342 | """ 343 | 344 | npcM2.add_memory_text("引入新的学习框架", "2021-08-01 12:00:00") 345 | npcM2.add_memory_text("注重算法的稳定性和鲁棒性", "2021-08-01 12:00:00") 346 | npcM2.add_memory_text("进行充分的实验验证", "2021-08-01 12:00:00") 347 | print(npcM2.search_memory("奖励", "2021-08-01 12:00:00", k=3)) 348 | # npcM2.shutdown() 349 | 350 | 351 | if __name__ == "__main__": 352 | main() 353 | -------------------------------------------------------------------------------- /nuwa/src/npc/talk_box.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import re 5 | from datetime import datetime 6 | from pathlib import Path 7 | 8 | from nuwa.src.npc.action import ActionItem 9 | from nuwa.src.utils.model_api import get_model_answer 10 | 11 | 12 | class TalkBox: 13 | def __init__(self, model, project_root_path, **kwargs): 14 | self.logger = logging.getLogger("NPC") 15 | self.history = [] 16 | self.response = "" 17 | self.ACTION_MODEL = model 18 | self.PROJECT_ROOT_PATH = project_root_path 19 | 20 | # 根据各种参数初始化npc的系统指令 21 | self.name = kwargs.get("name", "") 22 | self.desc = kwargs.get("desc", "") 23 | self.mood = kwargs.get("mood", "") 24 | self.time = kwargs.get("time", "") 25 | self.purpose = kwargs.get("purpose", "") 26 | self.latest_memory = kwargs.get("latest_memory", []) 27 | self.purpose_related_memory = kwargs.get("purpose_related_memory", []) 28 | self.player_related_memory = kwargs.get("player_related_memory", []) 29 | self.action_prompt = kwargs.get("action_prompt", "") 30 | self.state_position = kwargs.get("state", {}).get("position", "") 31 | self.state_observation_people = kwargs.get("state", {}).get("people", []) 32 | self.state_observation_items = kwargs.get("state", {}).get("items", []) 33 | self.state_backpack = kwargs.get("state", {}).get("backpack", []) 34 | self.state_observation_locations = kwargs.get("state", {}).get("locations", []) 35 | self.scene_allowed_places = kwargs.get("scene_allowed_places", []) 36 | self.player_name = kwargs.get("player_name", "") 37 | self.player_state_desc = kwargs.get("player_state_desc", "") 38 | self.items_visible = kwargs.get("items_visible", []) 39 | 40 | def generate_prompt(description, items, empty_msg, non_empty_msg): 41 | """ 42 | 简化重复代码,根据信息生成指令 43 | """ 44 | if not items: 45 | return empty_msg 46 | return non_empty_msg.format('、'.join(items)) 47 | 48 | npc_info_prompts = [ 49 | generate_prompt("时间", [self.time], "", "现在的时间是{},"), 50 | generate_prompt("位置", [self.state_position], "", "你现在位于{},"), 51 | generate_prompt("心情", [self.mood], "", "心情是{},"), 52 | generate_prompt("目的", [self.purpose], "", "目的是{},"), 53 | generate_prompt("记忆", self.latest_memory + self.purpose_related_memory, "", "你记得{}。"), 54 | generate_prompt("身上物品", self.state_backpack, "你现在身上空无一物", "你现在身上有{}。"), 55 | generate_prompt("地点", self.scene_allowed_places, "", "你现在可以去{}。"), 56 | generate_prompt("人", self.state_observation_people, "你现在看不到什么人。", "周围能看到的人有{}。"), 57 | generate_prompt("物品", self.state_observation_items, "周围没有什么能捡的东西。", "周围能捡的东西有{}。"), 58 | generate_prompt("地方", self.state_observation_locations, "周围看不到什么地方。", "周围能看到的地方有{}。"), 59 | ] 60 | 61 | player_info_prompts = [ 62 | generate_prompt("人物", [self.player_name], "", "{}正在和你说话,"), 63 | generate_prompt("人物信息", [self.player_state_desc] + self.player_related_memory, "你对他一无所知。", 64 | "你知道一些关于他的信息,{}。"), 65 | generate_prompt("人物身上物品", self.items_visible, "他身上没有任何东西。", "他身上有{}。") 66 | ] 67 | 68 | npc_prompt_text = "".join(npc_info_prompts) 69 | player_prompt_text = "".join(player_info_prompts) 70 | 71 | # instruct = f""" 72 | # 你是{self.name},{self.desc}。现在的时间是{self.time},你位于{self.state_position},心情{self.mood}。你的目的是{self.purpose}。 73 | # 你记得最近的记忆是{self.latest_memory},相关的记忆是{self.purpose_related_memory},{self.player_related_memory}。 74 | # {prompt_text} 75 | # 76 | # 请以符合你角色情绪和背景的方式作出回应,包括: 77 | # 1. 当前的情绪 78 | # 2. 想说的话 79 | # 3. 计划采取的行动 80 | # 81 | # 你的回应格式应该是:`@当前情绪@想说的话@<计划行动>@`。 82 | # 行动的格式应该是:<动作|对象|参数>,并且只能选择一个行动。 83 | # 84 | # 请注意,你的行动应该符合以下定义: 85 | # {self.action_prompt} 86 | # """ 87 | instruct = f""" 88 | 你是{self.name},{self.desc} 89 | {npc_prompt_text} 90 | {player_prompt_text} 91 | You need to respond in a way that fits the character's emotions and background. 92 | The output should be a markdown code snippet formatted in the following schema, including the leading and trailing "```json" and "```". including the content, {{"mood": Your current mood. "answer": What you want to say. "action": The actions you want to take. }}: 93 | 94 | ```json 95 | {{ 96 | "mood": string // 开心 97 | "answer": string // 你好 98 | "action": string // 99 | }} 100 | 101 | The action should be defined in the format: , and only one action can be selected from the following definitions: 102 | {self.action_prompt} 103 | ``` 104 | 用中文回答 105 | """ 106 | # 删掉instruct中多余的空格和换行符 107 | instruct = '\n'.join([line.strip() for line in instruct.strip().split('\n')]) 108 | print(instruct) 109 | self.history.append({"role": "system", "content": instruct}) 110 | 111 | def generate_response(self, input_text, **kwargs): 112 | """ 113 | 生成回答 114 | """ 115 | # 获取新的输入参数,对比是否和原先一致,不一致则更新,并且加入指令中 116 | instruct = [] 117 | mood = kwargs.get("mood", "") 118 | memory_related_text_player = kwargs.get("player_related_memory", "") 119 | items_visible = kwargs.get("items_visible", []) 120 | state_backpack = kwargs.get("state", {}).get("backpack", []) 121 | 122 | if mood != self.mood and mood != "": 123 | instruct.append(f"{self.name}的心情是{mood}。") 124 | self.mood = mood 125 | # todo 目前记忆有问题,做好了再加上 126 | # if memory_related_text_player != self.player_related_memory and memory_related_text_player != "": 127 | # instruct.append(f"{self.name}脑海中相关记忆:{memory_related_text_player}。") 128 | # self.player_related_memory = memory_related_text_player 129 | if items_visible != self.items_visible and items_visible != []: 130 | instruct.append(f"{self.player_name}身上有:{items_visible}。") 131 | self.items_visible = items_visible 132 | if state_backpack != self.state_backpack and state_backpack != "": 133 | instruct.append(f"{self.name}现在身上的物品:{state_backpack}。") 134 | self.state_backpack = state_backpack 135 | 136 | if instruct: 137 | instruct = ",".join(instruct) 138 | self.history.append({"role": "system", "content": instruct}) 139 | self.history.append({"role": "user", "content": f'{self.player_name}:{input_text}'}) 140 | answer = get_model_answer(model_name=self.ACTION_MODEL, inputs_list=self.history, 141 | project_root_path=self.PROJECT_ROOT_PATH) 142 | self.history.append({"role": "assistant", "content": answer}) 143 | # print(self.history) 144 | self.logger.debug(f""" 145 | 146 | <对话列表>:{self.history} 147 | """) 148 | self.response = answer 149 | return answer 150 | 151 | def get_history_content(self) -> str: 152 | """ 153 | 获取对话历史,只保留对话内容和最后的动作,整合成字符串 154 | """ 155 | history = [f'{self.name}与{self.player_name}在{self.time},{self.state_position},发生了一次对话:'] 156 | for item in self.history: 157 | if item["role"] == "user": 158 | history.append(f"{item['content']}") 159 | elif item["role"] == "assistant": 160 | assistant_content = self.parse_response_json(item['content']) 161 | history.append(f"{self.name}[{assistant_content.get('mood', '')}]:{assistant_content['answer']}") 162 | last_assistant_content = self.parse_response_json(self.history[-1]['content']) 163 | history.append( 164 | f"对话过后,{self.name}的心情很{last_assistant_content.get('mood', '')},接下来的行动是{last_assistant_content.get('action', '')}。") 165 | history_content = "\n".join(history) 166 | return history_content 167 | 168 | def parse_response_json(self, content): 169 | """ 170 | 解析回答的json格式 171 | """ 172 | if not content: 173 | content = self.response 174 | # remove ```json from the beginning and ``` from end 175 | content = content.lstrip("```json").rstrip("```") 176 | try: 177 | dict_response = json.loads(content) 178 | except Exception as e: 179 | self.logger.error(f"解析回答时出错:{e}, {content}") 180 | dict_response = {'mood': self.mood, 'purpose': self.purpose, 'answer': content, 'action': ""} 181 | return dict_response 182 | 183 | 184 | def remove_non_alphanumeric_from_ends(input_str): 185 | # 匹配字符串两端的中文、英文和数字之外的字符 186 | pattern = r'^[^>\u4e00-\u9fa5a-zA-Z0-9]*|[^>\u4e00-\u9fa5a-zA-Z0-9]*$' 187 | # 使用正则表达式匹配并删除 188 | result = re.sub(pattern, '', input_str) 189 | return result 190 | 191 | 192 | if __name__ == "__main__": 193 | # Example of how to initialize TalkBox with specified parameters 194 | # tb = TalkBox( 195 | # name="草泥马", 196 | # desc="一匹很凶的马,对人非常无理粗暴,喜欢说草泥马", 197 | # mood="烦躁", 198 | # time=datetime.strptime("2023-04-01 15:00:00", "%Y-%m-%d %H:%M:%S"), # 假设当前时间 199 | # position="沙漠中", 200 | # purpose="草泥马现在只想要远离人类", 201 | # latest_memory="草泥马在沙漠中找到了一顶遮阳帽。", 202 | # purpose_related_memory="因为和大司马吵了一架而离开了马群,这是草泥马第一次冒险进入沙漠。", 203 | # player_related_memory="", 204 | # scene_allowed_places=["沙漠东部", "沙漠中心", "即将到达的绿洲"], 205 | # action_prompt="[{'name': 'move', 'definition': (',向[location]移动',), 'example': ('',)}, {'name': 'chat', 'definition': (',对[person]说话,内容是[content]',), 'example': ('',)}, {'name': 'follow', 'definition': (',跟随[person]',), 'example': ('',)}, {'name': 'give', 'definition': (',给[person]一个[item]',), 'example': ('',)}]", 206 | # state={'position': "沙漠中", 'people': ["沙漠商人"], 207 | # 'items': [], 208 | # 'locations': ["沙漠东部", "沙漠中心", "即将到达的绿洲"], 'backpack': ["遮阳帽"]}, 209 | # player_name="杨泽君", 210 | # player_state_desc="杨泽君是一位年轻的法师,他看起来很帅气,穿着一身灰色的袍子,拿着一根法杖。看起来十分威风", 211 | # items_visible=["法杖", "望远镜", "水", "饼干"], 212 | # model="gpt-3.5-turbo-16k", 213 | # project_root_path=Path(__file__).parents[3] / "example_project" 214 | # ) 215 | tb = TalkBox( 216 | name="西格马", 217 | desc="一匹喜欢沉思的马,整天在思考数学问题。说话风格很简单,喜欢给别人出数学题。西格玛很独立,只有当别人解出正确答案时,西格马才会跟随别人,也就是follow。", 218 | mood="沉思", 219 | time="下午3:00", 220 | position="沙漠中", 221 | purpose="思考数学问题", 222 | memory_latest_text="", 223 | memory_related_text_purpose="", 224 | memory_related_text_player="", 225 | scene_allowed_places=[], 226 | action_prompt="[{'name': 'continue', 'definition': (',继续保持之前的动作',), 'example': ('',)}, {'name': 'follow', 'definition': (',跟随[person]',), 'example': ('',)}, {'name': 'give', 'definition': (',将身上的[item]给[person]',), 'example': ('',)}]", 227 | state={'position': "沙漠中", 'people': ["沙漠商人"], 228 | 'items': [], 229 | 'locations': ["沙漠东部", "沙漠中心", "即将到达的绿洲"], 'backpack': [""]}, 230 | player_name="杨泽君", 231 | player_state_desc="杨泽君是一位年轻的法师,他看起来很帅气,穿着一身灰色的袍子,拿着一根法杖。看起来十分威风", 232 | items_visible=["法杖", "望远镜", "水", "饼干"], 233 | model="gpt-3.5-turbo-16k", 234 | project_root_path=Path(__file__).parents[3] / "example_project" 235 | ) 236 | input("Press Enter to start the conversation") 237 | while True: 238 | user_input = input("杨泽君: ") 239 | if user_input == "exit": 240 | print(tb.history) 241 | print(tb.get_history_content()) 242 | break 243 | response = tb.generate_response(user_input) 244 | print(f"Assistant: {tb.parse_response_json(response)}") 245 | -------------------------------------------------------------------------------- /nuwa/src/npc/test.py: -------------------------------------------------------------------------------- 1 | # import openai 2 | # import json 3 | # 4 | # openai.api_key = 'sk-qvpKnoiDugYFOJbLC48e68E26a9e4510Ad779096Fd2019Fd' 5 | # openai.api_base = 'https://apic3.a1r.cc/v1' 6 | # 7 | # 8 | # # Example dummy function hard coded to return the same weather 9 | # # In production, this could be your backend API or an external API 10 | # def get_current_weather(location, unit="fahrenheit"): 11 | # """Get the current weather in a given location""" 12 | # if "tokyo" in location.lower(): 13 | # return json.dumps({"location": "Tokyo", "temperature": "10", "unit": unit}) 14 | # elif "san francisco" in location.lower(): 15 | # return json.dumps({"location": "San Francisco", "temperature": "72", "unit": unit}) 16 | # elif "paris" in location.lower(): 17 | # return json.dumps({"location": "Paris", "temperature": "22", "unit": unit}) 18 | # else: 19 | # return json.dumps({"location": location, "temperature": "unknown"}) 20 | # 21 | # 22 | # def get_action_response(player_name, player_speech, items_visible, player_state_desc, time, fail_safe, k=3): 23 | # return 24 | # 25 | # 26 | # def run_conversation(): 27 | # # Step 1: send the conversation and available functions to the model 28 | # messages = [{"role": "user", "content": "What's the weather like in San Francisco, Tokyo, and Paris?"}] 29 | # tools = [ 30 | # { 31 | # "type": "function", 32 | # "function": { 33 | # "name": "get_current_weather", 34 | # "description": "Get the current weather in a given location", 35 | # "parameters": { 36 | # "type": "object", 37 | # "properties": { 38 | # "location": { 39 | # "type": "string", 40 | # "description": "The city and state, e.g. San Francisco, CA", 41 | # }, 42 | # "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, 43 | # }, 44 | # "required": ["location"], 45 | # }, 46 | # }, 47 | # } 48 | # ] 49 | # response = openai.ChatCompletion.create( 50 | # model="gpt-3.5-turbo-0125", 51 | # messages=messages, 52 | # tools=tools, 53 | # tool_choice="auto", # auto is default, but we'll be explicit 54 | # ) 55 | # response_message = response.choices[0].message 56 | # tool_calls = response_message.tool_calls 57 | # print(response_message) 58 | # 59 | # print("-------------------") 60 | # # Step 2: check if the model wanted to call a function 61 | # if tool_calls: 62 | # # Step 3: call the function 63 | # # Note: the JSON response may not always be valid; be sure to handle errors 64 | # available_functions = { 65 | # "get_current_weather": get_current_weather, 66 | # } # only one function in this example, but you can have multiple 67 | # messages.append(response_message) # extend conversation with assistant's reply 68 | # # Step 4: send the info for each function call and function response to the model 69 | # for tool_call in tool_calls: 70 | # function_name = tool_call.function.name 71 | # function_to_call = available_functions[function_name] 72 | # function_args = json.loads(tool_call.function.arguments) 73 | # function_response = function_to_call( 74 | # location=function_args.get("location"), 75 | # unit=function_args.get("unit"), 76 | # ) 77 | # 78 | # messages.append( 79 | # { 80 | # "tool_call_id": tool_call.id, 81 | # "role": "tool", 82 | # "name": function_name, 83 | # "content": function_response, 84 | # } 85 | # ) # extend conversation with function response 86 | # second_response = openai.ChatCompletion.create( 87 | # model="gpt-3.5-turbo-0125", 88 | # messages=messages, 89 | # ) # get a new response from the model where it can see the function response 90 | # return second_response 91 | # 92 | # 93 | # import requests 94 | # import json 95 | # import requests 96 | # 97 | # 98 | # def get_stable_access_token(appid, secret): 99 | # url = "https://api.weixin.qq.com/cgi-bin/stable_token" 100 | # payload = { 101 | # "grant_type": "client_credential", 102 | # "appid": appid, 103 | # "secret": secret 104 | # } 105 | # response = requests.post(url, json=payload) 106 | # if response.status_code == 200: 107 | # print(response.json()['access_token']) 108 | # return response.json()['access_token'] 109 | # else: 110 | # print('Failed to retrieve stable access token') 111 | # return None 112 | # 113 | # 114 | # def post_wechat_api(access_token, service, api, data, client_msg_id): 115 | # url = f"https://api.weixin.qq.com/wxa/servicemarket?access_token={access_token}" 116 | # payload = { 117 | # "service": service, 118 | # "api": api, 119 | # "data": data, 120 | # "client_msg_id": client_msg_id, 121 | # "async": False 122 | # } 123 | # response = requests.post(url, json=payload) 124 | # if response.status_code == 200: 125 | # return response.json() 126 | # else: 127 | # return {'error': 'Failed to post data to WeChat API', 'status_code': response.status_code} 128 | # 129 | # 130 | # # Parameters 131 | # appid = "wxb87e628eaf9e5937" 132 | # secret = "ef109bcad4dcc0c83cada824e2b202d9" 133 | # service = "wx617ea32f889ba259" 134 | # api = "BaichuanNPCTurbo" 135 | # data = { 136 | # "character_profile": { 137 | # "character_name": "孙悟空", 138 | # "character_info": "孙悟空", 139 | # "user_name": "孙悟空", 140 | # "user_info": "孙悟空" 141 | # }, 142 | # "messages": [ 143 | # { 144 | # "role": "user", 145 | # "content": "霁云,你可以空手接白刃吗?" 146 | # } 147 | # ], 148 | # "temperature": 0.8, 149 | # "top_k": 10, 150 | # "max_tokens": 512 151 | # } 152 | # client_msg_id = "id42379554" 153 | # 154 | # # Execution 155 | # access_token = get_stable_access_token(appid, secret) 156 | # if access_token: 157 | # response = post_wechat_api(access_token, service, api, data, client_msg_id) 158 | # print(response) 159 | # else: 160 | # print("Error retrieving the access token.") 161 | # 162 | # import requests 163 | # 164 | # # 定义您的appid和secret 165 | # appid = 'wxb87e628eaf9e5937' 166 | # secret = 'ef109bcad4dcc0c83cada824e2b202d9' 167 | # 168 | # # 获取access_token的URL 169 | # token_url = 'https://api.weixin.qq.com/cgi-bin/token' 170 | # 171 | # # 发送GET请求获取access_token 172 | # response = requests.get(token_url, params={'grant_type': 'client_credential', 'appid': appid, 'secret': secret}) 173 | # if response.status_code == 200: 174 | # token_data = response.json() 175 | # if 'access_token' in token_data: 176 | # access_token = token_data['access_token'] 177 | # print("获取到的access_token:", access_token) 178 | # else: 179 | # print("获取access_token失败:", token_data) 180 | # exit() 181 | # 182 | # # 发送POST请求到服务市场的URL 183 | # service_market_url = f'https://api.weixin.qq.com/wxa/servicemarket?access_token={access_token}' 184 | # 185 | # # 构建要发送到服务市场的数据 186 | # service_data = { 187 | # "service": "wx617ea32f889ba259", # Service ID 188 | # "api": "BaichuanNPCTurbo", # API名称 189 | # "data": { 190 | # "character_profile": { 191 | # "character_name": "孙悟空", 192 | # "character_info": "孙悟空", 193 | # "user_name": "孙悟空", 194 | # "user_info": "孙悟空" 195 | # }, 196 | # "messages": [ 197 | # { 198 | # "role": "user", 199 | # "content": "霁云,你可以空手接白刃吗?" 200 | # } 201 | # ], 202 | # "temperature": 0.8, 203 | # "top_k": 10, 204 | # "max_tokens": 512 205 | # }, 206 | # "client_msg_id": "id123" 207 | # } 208 | # 209 | # # 发送POST请求到服务市场 210 | # service_response = requests.post(service_market_url, json=service_data) 211 | # 212 | # # 检查响应状态码 213 | # if service_response.status_code == 200: 214 | # print("服务市场请求成功!") 215 | # print("响应内容:", service_response.json()) 216 | # else: 217 | # print("服务市场请求失败,状态码:", service_response.status_code) 218 | # print("错误信息:", service_response.text) 219 | import json 220 | from pathlib import Path 221 | 222 | from langchain.output_parsers import ResponseSchema, StructuredOutputParser 223 | from langchain_core.prompts import PromptTemplate 224 | 225 | from nuwa.src.utils.model_api import get_model_answer 226 | 227 | # Define the response schemas 228 | response_schemas = [ 229 | ResponseSchema(name="name", description="学生的姓名"), 230 | ResponseSchema(name="age", description="学生的年龄") 231 | ] 232 | 233 | # Initialize the output parser based on the response schemas 234 | output_parser = StructuredOutputParser.from_response_schemas(response_schemas) 235 | 236 | # Get the format instructions from the parser 237 | format_instructions = output_parser.get_format_instructions() 238 | 239 | # Create a prompt template with the given format 240 | prompt = PromptTemplate( 241 | template="回答下面问题.\n{format_instructions}\n{question}", 242 | input_variables=["question"], 243 | partial_variables={"format_instructions": format_instructions} 244 | ) 245 | 246 | # Format the prompt with a specific question 247 | _input = prompt.format_prompt(question="给我一个女孩的名字?") 248 | 249 | print("input:") 250 | print(_input.to_string()) 251 | query = [ 252 | { 253 | "role": "user", 254 | "content": _input.to_string() 255 | } 256 | ] 257 | 258 | # Use the OpenAI model to generate a response based on the formatted prompt 259 | output = get_model_answer(model_name='gpt-3.5-turbo-16k', inputs_list=query, 260 | project_root_path=Path(__file__).parents[3] / "example_project") 261 | 262 | print('output:') 263 | print(output) 264 | -------------------------------------------------------------------------------- /nuwa/src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/src/utils/__init__.py -------------------------------------------------------------------------------- /nuwa/src/utils/cli.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import argparse 3 | import io 4 | import os 5 | import sys, zipfile 6 | from pathlib import Path 7 | from nuwa.src.config.config import CODE_ROOT_PATH, NPC_MEMORY_CONFIG 8 | from nuwa.src.utils.embedding import LocalEmbedding 9 | from nuwa import __version__ 10 | 11 | def run_engine(project_dir:Path=Path(os.getcwd()), engine_port=None, game_port=None, logo=True): 12 | """ 13 | 运行引擎 14 | :param project_dir: 用户指定的配置目录 15 | """ 16 | # 检查project_dir是否存在要求的文件 PROJECT_DIR/config/llm_config.json 17 | if project_dir is None: 18 | print("Project dir not specified, use current dir as project dir") 19 | project_dir = Path(os.getcwd()) 20 | if not os.path.exists(project_dir): 21 | print("Project dir not exists!") 22 | print("Please make sure your are using nuwa run on your project dir!") 23 | sys.exit(1) 24 | if not os.path.exists(os.path.join(project_dir, "config")): 25 | print(f"Config dir {project_dir}/config/ not exists!") 26 | print("Please make sure your are using nuwa run on your project dir!") 27 | sys.exit(1) 28 | if not os.path.exists(os.path.join(project_dir, "config", "llm_config.json")): 29 | print(f"Config file {project_dir}/config/llm_config.json not exists!") 30 | print("Please make sure your are using nuwa run on your project dir!") 31 | sys.exit(1) 32 | 33 | # 运行引擎的代码... 34 | from nuwa.src.engine import NPCEngine 35 | engine = NPCEngine( 36 | project_root_path=project_dir, 37 | engine_url="::1", 38 | engine_port=engine_port, 39 | game_url="::1", 40 | game_port=game_port, 41 | logo=logo) 42 | 43 | def init_project(target_directory, project_name): 44 | zip_path = CODE_ROOT_PATH / "material" / 'templates' / 'template.zip' 45 | target_directory = Path(target_directory) 46 | final_project_path = target_directory / project_name 47 | 48 | with zipfile.ZipFile(zip_path, 'r') as zip_ref: 49 | for info in zip_ref.infolist(): 50 | # 解决文件名编码问题 51 | filename = info.filename.encode('cp437').decode('utf-8') 52 | target_file_path = final_project_path / filename 53 | 54 | # 如果是目录,则创建目录 55 | if info.is_dir(): 56 | os.makedirs(target_file_path, exist_ok=True) 57 | else: 58 | # 确保文件的目录存在 59 | os.makedirs(target_file_path.parent, exist_ok=True) 60 | # 写入文件 61 | with zip_ref.open(info.filename) as source, open(target_file_path, "wb") as target: 62 | target.write(source.read()) 63 | print(f"project inited in: {final_project_path}") 64 | 65 | # CLI 命令处理 66 | def handle_init_command(args): 67 | target_directory = args.target_directory or os.getcwd() 68 | init_project(target_directory, args.project_name) 69 | 70 | 71 | def build_mac(project_dir=os.getcwd(), model=None): 72 | pass 73 | 74 | def download_model_weights(args): 75 | print("Downloading model weights...") 76 | embedding_model = LocalEmbedding(model_name=NPC_MEMORY_CONFIG["hf_model_id"], vector_width=NPC_MEMORY_CONFIG["hf_dim"]) 77 | print("Model weights downloaded!") 78 | 79 | 80 | def main(): 81 | if sys.stdout.encoding != 'UTF-8': 82 | sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') 83 | 84 | parser = argparse.ArgumentParser(description='Nuwa: A simulation engine for NPC') 85 | # nuwa -v for version 86 | parser.add_argument('-v', '--version', action='version', version=f'{__version__}') 87 | 88 | subparsers = parser.add_subparsers(help='commands', dest='command') 89 | 90 | # run 91 | run_parser = subparsers.add_parser('run', help='Run nuwa engine') 92 | run_parser.add_argument('-r', '--project-dir', type=str, help='Path to the config dir', default=None) 93 | # 端口 94 | run_parser.add_argument('-e', '--engine-port', type=int, help='Port of the engine', default=8199) 95 | run_parser.add_argument('-g', '--game-port', type=int, help='Port of the game', default=8084) 96 | # 是否显示logo 97 | run_parser.add_argument('-l', '--logo', type=bool, help='Whether to show logo', default=True) 98 | 99 | # init 100 | init_parser = subparsers.add_parser('init', help='Init a new project') 101 | init_parser.add_argument('-t', '--target-directory', type=str, help='Path to the target dir', default=None) 102 | init_parser.add_argument('-n', '--project-name', type=str, help='Name of the project', default="example_project") 103 | 104 | # download 105 | download_parser = subparsers.add_parser('download', help='Download nuwa model') 106 | 107 | # build 108 | build_parser = subparsers.add_parser('build', help='Build nuwa engine') 109 | build_parser.add_argument('-r', '--project-dir', type=str, help='Path to the config dir', default=None) 110 | # TODO:完善build的内容 build mac 或 build win 自动在当前build文件夹发布 111 | 112 | args = parser.parse_args() 113 | 114 | if args.command == 'run': 115 | run_engine(project_dir=Path(args.project_dir), engine_port=args.engine_port, 116 | game_port=args.game_port, logo=args.logo) 117 | elif args.command == 'init': 118 | """ 119 | nuwa init 默认在本地初始化exmaple project 120 | nuwa init -t 会在目标文件夹初始化example project 121 | nuwa init -n 会在当前目录初始化指定名字的project 122 | nuwa init -t -n 会在目标文件夹初始化指定名字的project 123 | """ 124 | if args.target_directory is None: 125 | args.target_directory = Path(os.getcwd()) 126 | handle_init_command(args) 127 | elif args.command == 'download': 128 | download_model_weights(args) 129 | 130 | if __name__ == "__main__": 131 | main() 132 | -------------------------------------------------------------------------------- /nuwa/src/utils/database.py: -------------------------------------------------------------------------------- 1 | """ 2 | 实验性的数据库模块,使用 RocksDB/pickleDB/SQLite 作为后端。 3 | 目前遇到问题: 4 | ROCKSDB好像不能拿来就用,需要先安装 5 | 如果把记忆数据以ID的形式上传到pinecone,中文会被转成unicode编码,512的字符限制很容易超出(最多40个字左右) 6 | 这里的想法是采用KV-store作为本地数据库,用来存储NPC的记忆,然后再把记忆数据上传到pinecone,用来做相似度搜索 7 | RocksDB: 8 | 需要手动make安装,不知道怎么实现平台无关 9 | PickleDB: 10 | 不需要手动安装,嵌入式的python kv存储,支持序列化 11 | SQLite: 12 | 嵌入式文件数据库,不是KV store。按理来说性能应该比KV store要差,但是需要确认是否不如pickleDB 13 | """ 14 | import os 15 | import pickledb 16 | import logging 17 | from pathlib import Path 18 | 19 | 20 | class PickleDB: 21 | _instances = {} 22 | 23 | def __new__(cls, db_path, auto_dump=True): 24 | """ 25 | 单例模式,不同的路径对应不同的数据库,相同的路径对应同一个数据库对象 26 | :param db_path: 27 | :param auto_dump: 28 | """ 29 | if db_path not in cls._instances: 30 | instance = super(PickleDB, cls).__new__(cls) 31 | instance.__init__(db_path, auto_dump) 32 | cls._instances[db_path] = instance 33 | return cls._instances[db_path] 34 | 35 | def __init__(self, db_path, auto_dump=True): 36 | """ 37 | 初始化函数 38 | :param db_path: 数据库文件的路径 39 | :param auto_dump: 是否自动保存更改,默认为True 40 | """ 41 | # LOGGER配置 42 | self.logger = logging.getLogger("DATABASE") 43 | self.db_path = db_path 44 | self.auto_dump = auto_dump 45 | if not hasattr(self, 'db'): 46 | self.db = self.load_db() 47 | 48 | def load_db(self): 49 | """ 50 | 从db_path加载数据库 51 | :return: 返回数据库对象 52 | """ 53 | if os.path.exists(self.db_path): 54 | self.logger.info(f"使用已有数据库:{self.db_path}") 55 | else: 56 | self.logger.info(f"不存在数据库:{self.db_path},创建新数据库") 57 | result = pickledb.load(self.db_path, self.auto_dump, sig=False) 58 | self.logger.info(f"数据库已加载:{self.db_path}") 59 | return result 60 | 61 | def get(self, key): 62 | """ 63 | 获取键值 64 | :param key: 键 65 | :return: 返回键对应的值,如果键不存在,返回None 66 | """ 67 | return self.db.get(key) 68 | 69 | def set(self, key, value): 70 | """ 71 | 设置键值 72 | :param key: 键 73 | :param value: 值 74 | :return: 如果设置成功,返回True,否则返回False 75 | """ 76 | return self.db.set(key, value) 77 | 78 | def delete(self, key): 79 | """ 80 | 删除键值 81 | :param key: 键 82 | :return: 如果删除成功,返回True,否则返回False 83 | """ 84 | try: 85 | result:bool = self.db.rem(key) 86 | except KeyError: 87 | return False 88 | self.logger.debug(f"键{key}已删除") 89 | return result 90 | 91 | def dump(self): 92 | """ 93 | 手动保存数据库 94 | :return: 如果保存成功,返回True,否则返回False 95 | """ 96 | result = self.db.dump() 97 | self.logger.debug(f"database {self.db_path} 已持久化到{self.db_path}") 98 | return result 99 | 100 | def clear(self): 101 | """ 102 | 清空数据库 103 | :return: 104 | """ 105 | result =self.db.deldb() 106 | self.logger.debug(f"database {self.db_path} 已清空") 107 | return result 108 | -------------------------------------------------------------------------------- /nuwa/src/utils/embedding.py: -------------------------------------------------------------------------------- 1 | """ 2 | 用来向量化文本的组件,按照config,加载本地的huggingface权重并进行推理 3 | """ 4 | import os 5 | import logging 6 | import requests 7 | from sentence_transformers import SentenceTransformer 8 | from typing import Any, Dict, List 9 | 10 | from nuwa.src.config.config import MODEL_BASE_PATH,NPC_MEMORY_CONFIG 11 | 12 | 13 | class SingletonEmbeddingModel(type): 14 | """ 15 | 用来实现单例模式的基类 16 | """ 17 | _instances = {} 18 | 19 | def __call__(cls, *args, **kwargs): 20 | if cls not in cls._instances: 21 | cls._instances[cls] = super(SingletonEmbeddingModel, cls).__call__(*args, **kwargs) 22 | return cls._instances[cls] 23 | 24 | 25 | class BaseEmbeddingModel(metaclass=SingletonEmbeddingModel): 26 | """ 27 | 所有向量化模型的基类,必须实现embed_text方法 28 | """ 29 | # 必须实现的一个属性:model_name 30 | model_name:str = None 31 | def embed_text(self, input_string:str)->List[float]: 32 | pass 33 | 34 | 35 | class LocalEmbedding(BaseEmbeddingModel): 36 | """ 37 | 用来向量化文本的组件,按照config,加载本地的huggingface权重并进行推理 38 | """ 39 | def __init__(self, model_name:str="uer/sbert-base-chinese-nli", vector_width:int=768): 40 | # 初始化EMBEDDING模块 LOGGER配置 41 | self.logger = logging.getLogger("EMBEDDING") 42 | ##################################### 43 | # 转化model_name为huggingface的本地文件夹, 44 | # 例: uer/sbert-base-chinese-nli ==> uer_sbert-base-chinese-nli 45 | ##################################### 46 | self.model_path_hf = MODEL_BASE_PATH / "embedding" / model_name.replace("/", "_") 47 | self.model_name = model_name 48 | os.environ["TOKENIZERS_PARALLELISM"] = "false" 49 | 50 | # 加载模型到本地 51 | if not os.path.exists(self.model_path_hf): 52 | self.logger.info(f"模型{model_name}的权重不存在,正在下载... 目标路径:{self.model_path_hf}") 53 | model = SentenceTransformer(model_name) 54 | model.save(str(self.model_path_hf)) 55 | self.model = model 56 | else: 57 | self.logger.info(f"模型{model_name}的权重已存在,加载本地权重... 路径:{self.model_path_hf}") 58 | self.model = SentenceTransformer(self.model_path_hf) 59 | 60 | # 获取并检查向量宽度 61 | vector_width_from_weights:int = self.model.get_sentence_embedding_dimension() # e.g: 768 62 | assert vector_width == vector_width_from_weights, f"模型{model_name}的向量宽度为{vector_width_from_weights},与用户指定的{vector_width}不符" 63 | self.vector_width = vector_width 64 | 65 | self.logger.info(f"模型{model_name}的权重已加载,向量宽度为{vector_width_from_weights}") 66 | 67 | def embed_text(self, input_string:str): 68 | try: 69 | vector = self.model.encode(input_string).tolist() 70 | except Exception as e: 71 | import traceback 72 | self.logger.error(f"向量化文本时出现错误:{e}") 73 | vector = [0.0]*self.vector_width 74 | return vector 75 | 76 | 77 | class HuggingFaceEmbedding(BaseEmbeddingModel): 78 | """ 79 | 用来向量化文本的组件,按照config,向Web API请求huggingface嵌入向量 80 | """ 81 | def __init__(self, model_name:str="uer/sbert-base-chinese-nli", vector_width:int=768): 82 | """embedding model设置""" 83 | self.logger = logging.getLogger("EMBEDDING") 84 | # huggingface embedding model 85 | self.hf_api_url = NPC_MEMORY_CONFIG["hf_api_url"] 86 | self.hf_headers = NPC_MEMORY_CONFIG["hf_headers"] 87 | self.hf_dim = NPC_MEMORY_CONFIG["hf_dim"] 88 | 89 | self.vector_width = vector_width 90 | self.model_name = model_name 91 | self.logger.info(f"模型{model_name}WEB EMBED API 已经初始化") 92 | 93 | def embed_text(self, text: str) -> list: 94 | """使用Hugging Face模型对文本进行嵌入""" 95 | try: 96 | response = requests.post( 97 | self.hf_api_url, 98 | headers=self.hf_headers, 99 | json={"inputs": text, "options": {"wait_for_model": True}}, 100 | timeout=10, 101 | ) 102 | response.raise_for_status() # Raises stored HTTPError, if one occurred. 103 | except requests.Timeout: 104 | print("The request timed out") 105 | self.logger.info(f"The embedding request of {text} timed out") 106 | return [0.0] * self.hf_dim 107 | except requests.HTTPError as http_err: 108 | print(f"The embedding of {text} HTTP error occurred: {http_err}") 109 | return [0.0] * self.hf_dim 110 | except Exception as err: 111 | print(f"The embedding of {text} other error occurred: {err}") 112 | return [0.0] * self.hf_dim 113 | vector: List[float] = response.json() 114 | assert ( 115 | len(vector) == self.hf_dim 116 | ), f"len(vector)={len(vector)} != self.hf_dim={self.hf_dim}" 117 | return vector 118 | 119 | 120 | 121 | if __name__ == "__main__": 122 | # local embedding 123 | embedding = LocalEmbedding(model_name=NPC_MEMORY_CONFIG["hf_model_id"], vector_width=NPC_MEMORY_CONFIG["hf_dim"]) 124 | print(embedding.embed_text("你好")) 125 | # hugingface embedding 126 | embedding = HuggingFaceEmbedding(model_name=NPC_MEMORY_CONFIG["hf_model_id"], vector_width=NPC_MEMORY_CONFIG["hf_dim"]) 127 | print() 128 | print(embedding) 129 | """ 下面是几个可用的模型例子 130 | "model_id": "uer/sbert-base-chinese-nli", 131 | "dim": 768, 132 | 133 | "model_id": "sentence-transformers/all-MiniLM-L6-v2", 134 | "dim": 384, 135 | 136 | 代码例: 137 | "uer/sbert-base-chinese-nli" ==> LocalEmbedding(model_name="uer/sbert-base-chinese-nli", vector_width=768) 138 | embedding = LocalEmbedding(model_name="sentence-transformers/all-MiniLM-L6-v2", vector_width=384) 139 | #组件会检查material/embedding文件夹下是否有对应的权重,如果没有,会自动下载(没有的话就需要互联网链接) 140 | 141 | embedding = HuggingFaceEmbedding(model_name=NPC_MEMORY_CONFIG["hf_model_id"], vector_width=NPC_MEMORY_CONFIG["hf_dim"]) 142 | # 线上的API请求非常不稳定会有超时的情况,所以不推荐使用 143 | """ 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /nuwa/src/utils/engine_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from pathlib import Path 4 | 5 | class EngineLogger: 6 | def __init__(self, project_root_path=None): 7 | """放在项目入口,日志初始化 8 | 设置项目整体logging的格式和处理器 9 | """ 10 | self.PROJECT_ROOT_PATH = Path(project_root_path) if project_root_path else Path.cwd() 11 | 12 | def set_up(self): 13 | # 时间(兼容windows文件名) 14 | time_format = "%Y-%m-%d-%H-%M-%S" 15 | time_str = time.strftime(time_format, time.localtime()) 16 | 17 | # 日志格式 18 | LOG_FORMAT = '%(asctime)s [%(levelname)s] %(name)s: %(message)s' 19 | 20 | # 控制台处理器 21 | console_handler = logging.StreamHandler() 22 | console_handler.setLevel(logging.DEBUG) 23 | console_handler.setFormatter(logging.Formatter(LOG_FORMAT)) 24 | 25 | # 文件处理器 26 | log_file_path = self.PROJECT_ROOT_PATH / "logs" 27 | log_file_path.mkdir(exist_ok=True) 28 | file_handler = logging.FileHandler(log_file_path / f'engine_{time_str}.log') 29 | file_handler.setLevel(logging.DEBUG) 30 | file_handler.setFormatter(logging.Formatter(LOG_FORMAT)) 31 | 32 | # 配置根logger 33 | logger = logging.getLogger() 34 | logger.setLevel(logging.DEBUG) 35 | logger.addHandler(console_handler) 36 | logger.addHandler(file_handler) 37 | 38 | def get_logger(self, name): 39 | return logging.getLogger(name) 40 | -------------------------------------------------------------------------------- /nuwa/src/utils/fail_safe.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from pathlib import Path 4 | from typing import List 5 | 6 | import numpy as np # 导入numpy库用于计算 7 | 8 | from nuwa.src.utils.embedding import LocalEmbedding 9 | from nuwa.src.npc.action import ActionItem 10 | 11 | class FailSafe: 12 | def __init__(self, embedding_model:LocalEmbedding=None): 13 | if not embedding_model: 14 | pass 15 | else: 16 | self.embedding_model = embedding_model 17 | 18 | def action_fail_safe(self, action_name:str, action_list:List["ActionItem"], embedding_model:LocalEmbedding=None, threshold:float=0.6)->"ActionItem": 19 | """ 20 | 使用向量化和余弦相似度进行容错检索 21 | :param action_name: 需要容错的动作名称 22 | :param action_list: 可选的动作列表, Item是ActionItem对象 23 | :param embedding_model: 用于文本向量化的模型 24 | :param threshold: 超过这个值,才可能被认为是failSafe对象,否则落空。 25 | :return: 最接近的动作名称 26 | """ 27 | 28 | 29 | # 1.将action_name和action_list使用embedding_model进行向量化 30 | if embedding_model is not None: 31 | action_name_vec = embedding_model.embed_text(action_name) 32 | else: 33 | action_name_vec = self.embedding_model.embed_text(action_name) 34 | action_list_vecs = [action.vec for action in action_list] 35 | 36 | # 2.检索最邻的action 37 | similarities = [self.cosine_similarity(action_name_vec, vec) for vec in action_list_vecs] 38 | print(similarities, [each.name for each in action_list]) 39 | max_index = np.argmax(similarities) # 找到最高相似度的索引 40 | closest_action = action_list[max_index] # 根据索引找到最接近的动作 41 | 42 | # 3. threshold 43 | if similarities[max_index]>threshold: 44 | return closest_action 45 | 46 | return "" # 如果没有匹配到 那么action为“” 47 | 48 | def cosine_similarity(self, vec1, vec2): 49 | """ 50 | 计算两个向量之间的余弦相似度 51 | :param vec1: 第一个向量 52 | :param vec2: 第二个向量 53 | :return: 余弦相似度 54 | """ 55 | dot_product = np.dot(vec1, vec2) 56 | norm_vec1 = np.linalg.norm(vec1) 57 | norm_vec2 = np.linalg.norm(vec2) 58 | return dot_product / (norm_vec1 * norm_vec2) 59 | 60 | if __name__ == "__main__": 61 | # 假设embedding_model已实现并且可以使用 62 | embedding_model = LocalEmbedding(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", vector_width=384) 63 | fail_safe = FailSafe() 64 | action_to_select = [] 65 | for each in ["catch", "move", "talk", "attack", "release"]: 66 | action_item = ActionItem( 67 | name=each, 68 | definition="", 69 | example="", 70 | log_template="", 71 | multi_param=False, 72 | ) 73 | action_item.vec = embedding_model.embed_text(action_item.name) 74 | action_to_select.append(action_item) 75 | closest_action = fail_safe.action_fail_safe("chat", action_to_select, embedding_model) 76 | print(f"最接近的动作是: {closest_action.name}") 77 | -------------------------------------------------------------------------------- /nuwa/src/utils/faissdatabase.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import faiss 4 | import pickle 5 | from pathlib import Path 6 | 7 | class VectorDatabase: 8 | def __init__(self, npc_name: str, npc_name_hash: str, dim: int, vdb_path: Path = Path("npc_vector_databases")): 9 | self.dim = dim 10 | self.npc_name = npc_name 11 | self.npc_name_nash = npc_name_hash 12 | self.vdb_path = vdb_path 13 | self.index_path = self.vdb_path / f"{npc_name_hash}.db" # FAISS数据库文件路径 14 | self.pkl_path = self.vdb_path / f"{npc_name_hash}.pkl" # key到向量映射的Pickle文件路径 15 | self.index = None 16 | self.key2vector = {} 17 | 18 | self.vdb_path.mkdir(parents=True, exist_ok=True) # 确保NPC的目录存在 19 | 20 | # 加载或初始化数据库 21 | if self.index_path.exists(): 22 | print(f'Loading database for NPC "{npc_name}" from', self.index_path) 23 | self._load_db() 24 | else: 25 | print(f'Creating new database for NPC "{npc_name}"') 26 | self._init_db() 27 | 28 | def _init_db(self): 29 | self.index = faiss.IndexFlatIP(self.dim) 30 | 31 | def _load_db(self): 32 | self.index = faiss.read_index(str(self.index_path)) 33 | with self.pkl_path.open('rb') as f: 34 | self.key2vector = pickle.load(f) 35 | 36 | def _save_db(self): 37 | faiss.write_index(self.index, str(self.index_path)) 38 | with self.pkl_path.open('wb') as f: 39 | pickle.dump(self.key2vector, f) 40 | 41 | def put(self, key, vector): 42 | assert len(vector) == self.dim, "The dimension of the vector does not match the database." 43 | if key in self.key2vector: 44 | print(f"Warning: Overwriting existing vector for key '{key}'.") 45 | self.key2vector[key] = np.array(vector, dtype=np.float32) 46 | self.index.add(np.array([self.key2vector[key]])) 47 | 48 | def save(self): 49 | self._save_db() 50 | 51 | def search(self, vector, k=1, thresh=0.8): 52 | assert len(vector) == self.dim, "The dimension of the vector does not match the database." 53 | vector = np.array([vector], dtype=np.float32) 54 | D, I = self.index.search(vector, k) 55 | 56 | keys = [] 57 | distances = [] 58 | for i, d in zip(I[0], D[0]): 59 | if d > thresh: 60 | try: 61 | key = list(self.key2vector.keys())[i] 62 | keys.append(key) 63 | distances.append(d) 64 | except IndexError: 65 | continue 66 | return keys, distances 67 | 68 | def remove(self): 69 | # 删除向量数据库文件 70 | if self.index_path.exists(): 71 | os.remove(self.index_path) 72 | if self.pkl_path.exists(): 73 | os.remove(self.pkl_path) 74 | 75 | def test_vector_database(): 76 | npc_name = "3" 77 | dim = 128 78 | vdb = VectorDatabase(dim=dim, npc_name=npc_name) 79 | 80 | # 添加一些向量 81 | vdb.put("key1", np.random.rand(dim)) 82 | vdb.put("key2", np.random.rand(dim)) 83 | 84 | # 保存数据库 85 | vdb.save() 86 | 87 | # 搜索向量 88 | keys, distances = vdb.search(np.random.rand(dim), k=2) 89 | print("Found keys:", keys, "with distances:", distances) 90 | 91 | 92 | """ 93 | 运行测试 94 | """ 95 | if __name__ == "__main__": 96 | test_vector_database() -------------------------------------------------------------------------------- /nuwa/src/utils/model_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import os 4 | import random 5 | import time 6 | from pathlib import Path 7 | 8 | import google.generativeai as genai 9 | import openai 10 | import requests 11 | 12 | # import zhipuai 13 | # zhipuai.api_key = "3fe121b978f1f456cfac1d2a1a9d8c06.iQsBvb1F54iFYfZq" 14 | 15 | 16 | """ 17 | model_name = ['gpt-3.5-turbo-16k', 'cpm-bee] 18 | """ 19 | 20 | 21 | def get_model_answer(model_name, inputs_list, project_root_path, stream=False): 22 | PROJECT_ROOT_PATH = project_root_path # 用户输入的项目根目录 23 | # 读取LLM_CONFIG 24 | 25 | answer = 'no answer' 26 | if 'gpt' in model_name: 27 | model = OPENAI(model_name, project_root_path=project_root_path) 28 | answer = model.get_response(inputs_list, stream=stream) 29 | elif 'baidu' in model_name: 30 | model = BAIDU_API() 31 | answer = model.get_response(inputs_list=inputs_list) 32 | elif 'gemini' in model_name: 33 | # # gemini在配置文件中配置时,需要配置好代理和Google相关信息 34 | # model = GEMINI(model_name, project_root_path=project_root_path) 35 | # answer = model.get_response(inputs_list) 36 | answer = "NOT GEMINI" 37 | else: 38 | model = OPENAI(model_name, project_root_path=project_root_path) # 代理站中一般可以访问多种OPENAI接口形式的自定义模型,这里作为保底。 39 | answer = model.get_response(inputs_list, stream=stream) 40 | return answer 41 | 42 | 43 | class BAIDU_API: 44 | # DOC: https://cloud.baidu.com/doc/WENXINWORKSHOP/s/4lilb2lpf 45 | # 充值: https://console.bce.baidu.com/billing/#/account/index 46 | # 开通新模型: https://console.bce.baidu.com/qianfan/chargemanage/list 47 | 48 | def __init__(self): 49 | API_KEY = "qq7WLVgNX88unRoUVLtNz8fQ" # qq7WLVgNX88unRoUVLtNz8fQ 50 | SECRET_KEY = "gA3VOdcRnGM4gKKkKKi93A79Dwevm3zo" # gA3VOdcRnGM4gKKkKKi93A79Dwevm3zo 51 | self.access_token = None 52 | self.api_key = API_KEY 53 | self.secret_key = SECRET_KEY 54 | self.api_base = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=" 55 | self.get_access_token() 56 | 57 | def convert_openai_to_baidu(self, inputs_list): 58 | """ 59 | 将 OpenAI 的输入转换为百度的输入 60 | 检测是否为偶数,如果为偶数,那就把system拼接到user上面 61 | 62 | :param inputs_list: OpenAI 的输入 63 | :return: 百度的输入 64 | """ 65 | combined_content = '\n\n'.join(item['content'].strip() for item in inputs_list) 66 | baidu_inputs_list = [{"role": "user", "content": combined_content}] 67 | return baidu_inputs_list 68 | 69 | def get_response(self, inputs_list): 70 | self.url = self.api_base + self.access_token 71 | payload = json.dumps({ 72 | "messages": self.convert_openai_to_baidu(inputs_list), 73 | "temperture": "0" 74 | }) 75 | headers = { 76 | 'Content-Type': 'application/json' 77 | } 78 | response = requests.request("POST", self.url, headers=headers, data=payload) 79 | # load json data 80 | data = json.loads(response.text) 81 | response = data["result"] 82 | return response 83 | 84 | def get_access_token(self): 85 | """ 86 | 使用 AK,SK 生成鉴权签名(Access Token) 87 | :return: access_token,或是None(如果错误) 88 | """ 89 | url = "https://aip.baidubce.com/oauth/2.0/token" 90 | params = {"grant_type": "client_credentials", "client_id": self.api_key, "client_secret": self.secret_key} 91 | self.access_token = str(requests.post(url, params=params).json().get("access_token")) 92 | return self.access_token 93 | 94 | 95 | class OPENAI: 96 | def __init__(self, model_name, project_root_path): 97 | self.PROJECT_ROOT_PATH = Path(project_root_path) 98 | self.CONFIG_PATH = self.PROJECT_ROOT_PATH / "config" 99 | # 读取LLM_CONFIG 100 | OPENAI_CONFIG_PATH = self.CONFIG_PATH / "llm_config.json" 101 | openai_config_data = json.load(open(OPENAI_CONFIG_PATH, "r")) 102 | self.keys_bases = openai_config_data["OPENAI_CONFIG"]["OPENAI_KEYS_BASES"] 103 | self.current_key_index = 0 # 初始索引 104 | self.api_key, self.api_base = self.keys_bases[self.current_key_index]["OPENAI_KEY"], \ 105 | self.keys_bases[self.current_key_index]["OPENAI_BASE"] 106 | 107 | self.model_name = model_name 108 | self.max_tokens = openai_config_data["OPENAI_CONFIG"]["OPENAI_MAX_TOKENS"] 109 | self.temperature = openai_config_data["OPENAI_CONFIG"]["OPENAI_TEMPERATURE"] 110 | self.stop = None 111 | self.load_model() 112 | 113 | def load_model(self): 114 | openai.api_key = self.api_key 115 | openai.api_base = self.api_base 116 | 117 | def switch_api_key(self): 118 | self.current_key_index = (self.current_key_index + 1) % len(self.keys_bases) 119 | new_key_base = self.keys_bases[self.current_key_index] 120 | self.api_key, self.api_base = new_key_base["OPENAI_KEY"], new_key_base["OPENAI_BASE"] 121 | self.load_model() 122 | print(f"Switched to new API key and base: {self.api_key}, {self.api_base}") 123 | 124 | def get_response(self, inputs_list, stream=False, max_retries=3): 125 | attempt = 0 126 | while attempt < max_retries: 127 | try: 128 | if stream: 129 | print("----- Streaming Request -----") 130 | stream_response = openai.ChatCompletion.create( 131 | model=self.model_name, 132 | messages=inputs_list, 133 | temperature=self.temperature, # 对话系统需要启动随机性 134 | stream=True, 135 | ) 136 | return stream_response 137 | else: 138 | response = openai.ChatCompletion.create( 139 | model=self.model_name, 140 | messages=inputs_list, 141 | max_tokens=self.max_tokens, 142 | temperature=self.temperature, 143 | stop=self.stop 144 | ) 145 | # print(response.choices[0].message["content"].strip()) 146 | return response.choices[0].message["content"].strip() 147 | except Exception as e: 148 | attempt += 1 149 | print(f"Attempt {attempt} failed with error: {e}") 150 | if attempt < max_retries: 151 | wait_time = (2 ** attempt) + random.uniform(0, 1) # Exponential backoff with jitter 152 | print(f"Waiting {wait_time:.2f} seconds before retrying...") 153 | time.sleep(wait_time) 154 | self.switch_api_key() # Optionally switch API key before retrying 155 | else: 156 | return "An error occurred, and the request could not be completed after retries." 157 | 158 | 159 | class GEMINI: 160 | def __init__(self, model_name, project_root_path): 161 | self.PROJECT_ROOT_PATH = Path(project_root_path) 162 | self.CONFIG_PATH = self.PROJECT_ROOT_PATH / "config" 163 | # 读取配置 164 | GEMINI_CONFIG_PATH = self.CONFIG_PATH / "llm_config.json" 165 | gemini_config_data = json.load(open(GEMINI_CONFIG_PATH, "r")) 166 | self.api_keys = gemini_config_data["GEMINI_CONFIG"]["GEMINI_KEYS"] 167 | self.api_usage_limit = gemini_config_data["GEMINI_CONFIG"].get("API_USAGE_LIMIT", 1000) 168 | self.api_usage = {key: 0 for key in self.api_keys} # 初始化每个key的使用次数 169 | self.temperature = gemini_config_data["GEMINI_CONFIG"]["GEMINI_TEMPERATURE"] 170 | self.model_name = model_name 171 | self.model = genai.GenerativeModel(self.model_name) 172 | self.proxy = gemini_config_data["GEMINI_CONFIG"].get("PROXY", {"http": "", "https": ""}) 173 | self.current_api_key_index = 0 # 初始索引 174 | self.configure_api(self.api_keys[self.current_api_key_index]) 175 | self.max_tokens = gemini_config_data["GEMINI_CONFIG"]["GEMINI_MAX_TOKENS"] 176 | 177 | def configure_api(self, api_key): 178 | os.environ["HTTP_PROXY"] = self.proxy["http"] 179 | os.environ["HTTPS_PROXY"] = self.proxy["https"] 180 | genai.configure(api_key=api_key) 181 | 182 | def switch_api_key(self): 183 | self.current_api_key_index = (self.current_api_key_index + 1) % len(self.api_keys) 184 | self.configure_api(self.api_keys[self.current_api_key_index]) 185 | print(f"Switched to new API key: {self.api_keys[self.current_api_key_index]}") 186 | 187 | def get_response(self, inputs_list): 188 | messages = [] 189 | 190 | # 分别处理用户和模型的部分,确保不会添加空的内容到parts中 191 | user_parts = [input["content"] for input in inputs_list if 192 | input["role"] in ["system", "user"] and input["content"]] 193 | if user_parts: # 只有当有用户部分时才添加 194 | messages.append({'role': 'user', 'parts': user_parts}) 195 | 196 | model_parts = [input["content"] for input in inputs_list if input["role"] == "assistant" and input["content"]] 197 | for part in model_parts: # 对于模型的每一部分,分别添加 198 | messages.append({'role': 'model', 'parts': [part]}) 199 | 200 | for retries in range(5): 201 | try: 202 | response = self.model.generate_content(messages, generation_config=genai.types.GenerationConfig( 203 | temperature=self.temperature, max_output_tokens=self.max_tokens)) 204 | answer = response.text 205 | 206 | # 更新API key使用次数并检查是否需要切换 207 | self.api_usage[self.api_keys[self.current_api_key_index]] += 1 208 | if self.api_usage[self.api_keys[self.current_api_key_index]] >= self.api_usage_limit: 209 | self.switch_api_key() 210 | 211 | return answer 212 | except Exception as e: 213 | print(f"Error when calling the GEMINI API: {e}") 214 | if retries < 4: 215 | print("Attempting to switch API key and retry...") 216 | self.switch_api_key() 217 | else: 218 | print("Maximum number of retries reached. The GEMINI API is not responding.") 219 | return "I'm sorry, but I am unable to provide a response at this time due to technical difficulties." 220 | sleep_time = (2 ** retries) + random.random() 221 | print(f"Waiting for {sleep_time} seconds before retrying...") 222 | time.sleep(sleep_time) 223 | 224 | 225 | if __name__ == '__main__': 226 | PROJECT_ROOT_PATH = Path(os.path.abspath(__file__)).parents[3] / "example_project" 227 | # path: C:\python_code\npc_engine-main\example_project 228 | inputs_list_openai = [{ 229 | "role": "system", 230 | "content": """ 231 | 请你扮演李大爷,特性是:李大爷是一个普通的种瓜老头,戴着文邹邹的金丝眼镜,喜欢喝茶,平常最爱吃烤烧鸡喝乌龙茶;上午他喜欢呆在家里喝茶,下午他会在村口卖瓜,晚上他会去瓜田护理自己的西瓜,心情是开心,正在李大爷家,现在时间是2021-01-01 12:00:00, 232 | 你的最近记忆:8年前李大爷的两个徒弟在工厂表现优异都获得表彰。 233 | 6年前从工厂辞职并过上普通的生活。 234 | 4年前孩子看望李大爷并带上大爷最爱喝的乌龙茶。, 235 | 你脑海中相关记忆: 236 | 15年前在工厂收了两个徒弟。, 237 | 你现在看到的人:['王大妈', '村长', '隐形李飞飞'], 238 | 你现在看到的物品:['椅子#1', '椅子#2', '椅子#3[李大爷占用]', '床'], 239 | 你现在看到的地点:['李大爷家大门', '李大爷家后门', '李大爷家院子'], 240 | 你当前的目的是:李大爷想去村口卖瓜,因为李大爷希望能够卖出新鲜的西瓜给村里的居民,让大家都能品尝到甜美可口的水果。 241 | """}, 242 | { 243 | "role": "user", 244 | "content": """ 245 | 请你根据[行为定义]以及你现在看到的事物生成一个完整的行为,并且按照<动作|参数1|参数2>的结构返回: 246 | 行为定义: 247 | [{'name': 'mov', 'definition': (',向[location]移动',), 'example': ('',)}, {'name': 'get', 'definition': (',从[object2]中获得[object1],[object2]处可为空',), 'example': (',获得西瓜',)}, {'name': 'put', 'definition': (',把[object2]放入[object1]',), 'example': ('',)}, {'name': 'chat', 'definition': (',对[person]说话,内容是[content]',), 'example': (',对李大爷说你吃了吗',)}] 248 | 要求: 249 | 1.请务必按照以下形式返回动作、参数1、参数2的三元组以及行为描述:"<动作|参数1|参数2>, 行为的描述" 250 | 2.动作和参数要在20字以内。 251 | 3.动作的对象必须要属于看到的范围! 252 | 4.三元组两侧必须要有尖括号<> 253 | 5.以一行的方式,返回单个结果 254 | """}, 255 | ] 256 | 257 | # print(get_model_answer(model_name='gemini-pro', inputs_list=inputs_list_openai,project_root_path=PROJECT_ROOT_PATH)) 258 | print(get_model_answer(model_name='gpt-3.5-turbo-16k', inputs_list=inputs_list_openai, 259 | project_root_path=PROJECT_ROOT_PATH)) -------------------------------------------------------------------------------- /nuwa/src/utils/send_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Filename: send_utils.py 3 | Author: Mengshi, Yangzejun 4 | Contact: ..., yzj_cs_ilstar@163.com 5 | """ 6 | import json 7 | import uuid 8 | 9 | def send_data(sock, target_url, target_port, data, max_packet_size=6000): 10 | """ 11 | 把DICT数据发送给游戏端口 12 | :param sock: 13 | :param target_url: 14 | :param target_port: 15 | :param data: 16 | :param max_packet_size: 17 | :return: 18 | """ 19 | # UUID作为消息ID 20 | msg_id = uuid.uuid4().hex 21 | # 将json字符串转换为bytes 22 | data = json.dumps(data).encode("utf-8") 23 | # 计算数据包总数 24 | packets = [ 25 | data[i : i + max_packet_size] for i in range(0, len(data), max_packet_size)] 26 | total_packets = len(packets) 27 | print(total_packets) 28 | 29 | for i, packet in enumerate(packets): 30 | # 构造UDP数据包头部 31 | print( 32 | "sending packet {} of {}, size: {} KB".format( 33 | i + 1, total_packets, calculate_str_size_in_kb(packet))) 34 | header = f"{msg_id}@{i + 1}@{total_packets}".encode("utf-8") 35 | # 发送UDP数据包 36 | sock.sendto(header + b"@" + packet, (target_url, target_port)) 37 | 38 | def calculate_str_size_in_kb(string: bytes): 39 | # 获取字符串的字节数 40 | byte_size = len(string) 41 | # 将字节数转换成KB大小 42 | kb_size = byte_size / 1024 43 | return kb_size -------------------------------------------------------------------------------- /nuwa/test/README.md: -------------------------------------------------------------------------------- 1 | ### [未完成]测试用例升级计划 2 | 1.制作一个测试项目 所有内容基于测试项目进行 3 | 2.测试数据包从test_packet中读取,然后发送 4 | 3.只要没有出现Error就算测试通过 5 | 拉起一个engine进程,然后通过某种方式让测试端知道engine处理这个包的结果。 6 | 7 | -------------------------------------------------------------------------------- /nuwa/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casia22/npc_engine/992fbdbafe50b10c8f0a14de8c61bb5db46eda78/nuwa/test/__init__.py -------------------------------------------------------------------------------- /nuwa/test/game_sim.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import json 3 | import uuid 4 | import time 5 | import threading 6 | 7 | # 监听的地址和端口 8 | UDP_IP = "::1" 9 | UDP_PORT = 8084 10 | 11 | # 发送数据的地址和端口 12 | engine_url = "::1" 13 | engine_port = 8199 14 | 15 | # 创建socket 16 | sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 17 | sock.bind((UDP_IP, UDP_PORT)) 18 | 19 | # 准备数据包 20 | init_packet = { 21 | "func": "init", 22 | "scene_name": "雁栖村", 23 | "language": "C", 24 | "npc": [] 25 | } 26 | wakeup_packet = { 27 | "func": "wake_up", 28 | "npc_name": "王大妈", 29 | "scenario_name": "李大爷家", 30 | "npc_state": { 31 | "position": "李大爷家卧室", 32 | "observation": { 33 | "people": ["李大爷", "村长", "隐形李飞飞"], 34 | "items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床[包括:被子、枕头、床单、床垫、私房钱]"], 35 | "locations": ["李大爷家大门", "李大爷家后门", "李大爷家院子"] 36 | }, 37 | "backpack": ["优质西瓜", "大砍刀", "黄金首饰"] 38 | }, 39 | "time": "2021-01-01 12:00:00", 40 | } 41 | 42 | def send_data(data, max_packet_size=6000): 43 | # UUID作为消息ID 44 | msg_id = uuid.uuid4().hex 45 | # 将json字符串转换为bytes 46 | data = json.dumps(data).encode('utf-8') 47 | # 计算数据包总数 48 | packets = [data[i: i + max_packet_size] for i in range(0, len(data), max_packet_size)] 49 | total_packets = len(packets) 50 | for i, packet in enumerate(packets): 51 | # 构造UDP数据包头部 52 | header = f"{msg_id}@{i + 1}@{total_packets}".encode('utf-8') 53 | # 发送UDP数据包 54 | sock.sendto(header + b"@" + packet, (engine_url, engine_port)) 55 | print(f"Sent UDP packet: {header.decode('utf-8')}@{packet.decode('utf-8')}") 56 | 57 | def listen(): 58 | print("Listening on [{}]:{}".format(UDP_IP, UDP_PORT)) 59 | while True: 60 | data, addr = sock.recvfrom(4000) 61 | # get json packet from udp 62 | data = data.decode('utf-8') 63 | json_str = data.split('@')[-1] 64 | json_data = json.loads(json_str) 65 | print("Received UDP packet from {}: {}".format(addr, json_data)) 66 | 67 | def send_packets(): 68 | while True: 69 | send_data(init_packet) 70 | send_data(wakeup_packet) 71 | time.sleep(10) 72 | 73 | # 分别启动监听和发送数据包的线程 74 | threading.Thread(target=listen).start() 75 | threading.Thread(target=send_packets).start() 76 | -------------------------------------------------------------------------------- /nuwa/test/test_config/test_packets.py: -------------------------------------------------------------------------------- 1 | """ 2 | 存储用于开环测试的数据包 3 | 测试脚本统一从这个脚本导入测试数据包 4 | """ 5 | 6 | init_packet = { 7 | "func": "init", 8 | # 必填字段,代表在什么场景初始化 9 | "scene_name": "雁栖村", 10 | "language": "C", 11 | # 下面是🉑️选 12 | "npc": [ 13 | { 14 | "name": "渔夫阿强", 15 | "desc": "渔夫阿强是一个老练的渔民,擅长捕鱼和航海。他有一头浓密的白发和一双狡猾的眼睛。阿强经验丰富,对海洋和天气变化有着敏锐的观察力。", 16 | "mood": "满足", 17 | "npc_state": { 18 | "position": "河边钓鱼点", 19 | "observation": { 20 | "people": [], 21 | "items": ["船舱", "渔网", "渔具", "航海地图", "渔获"], 22 | "locations": ["船舱内部", "甲板"] 23 | }, 24 | "backpack": ["鱼饵", "渔具维修工具"] 25 | }, 26 | "action_space": ["move", "talk"], 27 | "memory": [ 28 | "从小就跟随父亲学习捕鱼技巧。", 29 | "曾多次出海捕鱼,积累丰富的经验。", 30 | "对海洋生态保护有着浓厚的兴趣。", 31 | "帮助其他渔民修理损坏的渔具。", 32 | "梦想拥有一艘自己的渔船,开展独立的渔业。" 33 | ] 34 | }, 35 | { 36 | "name": "猎人阿明", 37 | "desc": "猎人阿明是一位勇敢而机敏的猎人。他身材魁梧,肌肉发达,眼神犀利。阿明擅长追踪和狩猎各种野生动物,具有过人的耐力和狙击技巧。", 38 | "mood": "专注", 39 | "npc_state": { 40 | "position": "猎人小屋", 41 | "observation": { 42 | "people": [], 43 | "items": ["猎枪", "弓箭", "追踪装备", "野外求生工具"], 44 | "locations": ["猎人小屋内部", "周围的森林"] 45 | }, 46 | "backpack": ["干粮", "水壶", "急救包"] 47 | }, 48 | "action_space": ["move", "talk"], 49 | "memory": [ 50 | "从小生活在山区,接受父亲的猎人训练。", 51 | "熟悉各种野生动物的习性和行踪。", 52 | "常常在附近的森林中追踪并捕获猎物。", 53 | "有着长时间在野外生存的经验。", 54 | "一日作息:清晨起床后进行锻炼和瞄准训练,白天进行狩猎和追踪,傍晚返回小屋整理装备并准备晚餐,晚上休息并回顾一天的狩猎经历。" 55 | ] 56 | } 57 | ], # 可以留空,默认按照scene.json初始化场景NPC。非空则在之前基础上添加。 58 | } 59 | 60 | init_packet_police = { 61 | "func": "init", 62 | # 必填字段,代表在什么场景初始化 63 | "scene_name": "警察局", 64 | "language": "C", 65 | "npc": [] 66 | } 67 | 68 | wakeup_packet_1 = { 69 | "func": "wake_up", 70 | "npc_name": "王大妈", 71 | 72 | "scenario_name": "李大爷家", 73 | "npc_state": { 74 | "position": "李大爷家卧室", 75 | "observation": { 76 | "people": ["李大爷", "村长", "隐形李飞飞"], 77 | "items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床[包括:被子、枕头、床单、床垫、私房钱]"], 78 | "locations": ["李大爷家大门", "李大爷家后门", "李大爷家院子"] 79 | }, 80 | "backpack": ["优质西瓜", "大砍刀", "黄金首饰"] 81 | }, 82 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 83 | } 84 | 85 | wakeup_packet_2 = { 86 | "func": "wake_up", 87 | "npc_name": "李大爷", 88 | 89 | "scenario_name": "李大爷家", 90 | "npc_state": { 91 | "position": "李大爷家卧室", 92 | "observation": { 93 | "people": ["王大妈", "村长", "隐形李飞飞"], 94 | "items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床[包括:被子、枕头、床单、床垫、私房钱]"], 95 | "locations": ["李大爷家大门", "李大爷家后门", "李大爷家院子"] 96 | }, 97 | "backpack": ["黄瓜", "1000元", "老报纸"] 98 | }, 99 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 100 | } 101 | 102 | wakeup_packet_3 = { 103 | "func": "wake_up", 104 | "npc_name": "村长", 105 | 106 | "scenario_name": "李大爷家", 107 | "npc_state": { 108 | "position": "李大爷家卧室", 109 | "observation": { 110 | "people": ["王大妈", "村长", "隐形李飞飞"], 111 | "items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床[包括:被子、枕头、床单、床垫、私房钱]"], 112 | "locations": ["李大爷家大门", "李大爷家后门", "李大爷家院子"] 113 | }, 114 | "backpack": ["中华烟[剩余4根]", "1000元", "吃了一半的西瓜"] 115 | }, 116 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 117 | } 118 | 119 | wakeup_packet_test_repeat_move = { 120 | "func": "wake_up", 121 | "npc_name": "村长", 122 | 123 | "scenario_name": "李大爷家", 124 | "npc_state": { 125 | "position": "卧室", 126 | "observation": { 127 | "people": ["王大妈", "村长", "隐形李飞飞"], 128 | "items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床[包括:被子、枕头、床单、床垫、私房钱]"], 129 | "locations": [] 130 | }, 131 | "backpack": ["中华烟[剩余4根]", "1000元", "吃了一半的西瓜"] 132 | }, 133 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 134 | } 135 | 136 | 137 | action_done_packet_1 = { 138 | "func": "action_done", 139 | "npc_name": "王大妈", 140 | "status": "success", 141 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 142 | 143 | "scenario_name": "李大爷家", 144 | "npc_state": { 145 | "position": "李大爷家卧室", 146 | "observation": { 147 | "people": ["李大爷", "村长", "李飞飞"], 148 | "items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床"], 149 | "locations": ["李大爷家大门", "李大爷家后门", "李大爷家院子"] 150 | }, 151 | "backpack": ["优质西瓜", "大砍刀", "黄金首饰"] 152 | }, 153 | 154 | "action": "move", 155 | "object": "李大爷家", # 之前传过来的动作对象 156 | "parameters": [], # 之前传过来的参数 157 | "reason": "", # "王大妈在去往‘警察局’的路上被李大爷打断" 158 | } 159 | 160 | action_done_packet_2 = { 161 | "func": "action_done", 162 | "npc_name": "李大爷", 163 | "status": "fail", 164 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 165 | 166 | "scenario_name": "李大爷家", 167 | "npc_state": { 168 | "position": "李大爷家卧室", 169 | "observation": { 170 | "people": ["王大妈", "村长", "李飞飞"], 171 | "items": ["椅子#1", "椅子#2", "椅子#3[李大爷占用]", "床"], 172 | "locations": ["李大爷家大门", "李大爷家后门", "李大爷家院子"] 173 | }, 174 | "backpack": ["优质西瓜", "大砍刀", "黄金首饰"] 175 | }, 176 | 177 | "action": "move", 178 | "object": "警察局", # 之前传过来的动作对象 179 | "parameters": [], # 之前传过来的参数 180 | "reason": "李大爷在去往‘警察局’的路上被王大妈打断", # "王大妈在去往‘警察局’的路上被李大爷打断" 181 | } 182 | 183 | # player2npc的对话包 184 | player2npc_packet = { 185 | "func":"talk2npc", 186 | "npc_name":"警员1", 187 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 188 | 189 | # NPC的状态 190 | "scenario_name": "警察局", 191 | "npc_state": { 192 | "position": "雁栖村入口", 193 | "observation": { 194 | "people": ["囚犯阿呆","警员2","旅行者小王"], 195 | "items": ["椅子#1","椅子#2","椅子#3[李大爷占用]","床"], 196 | "locations": ['牢房', '雁栖村入口'] 197 | }, 198 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 199 | }, 200 | # player的信息 201 | "player_name":"旅行者小王", # player的名字 202 | "speech_content":"你好,我是旅行者小王, 我要报警, 在林区中好像有人偷砍树", # player说的话 203 | "items_visible": ["金丝眼镜", "旅行签证", "望远镜"], # player身上的物品 204 | "state": "旅行者小王正在严肃地站着,衣冠规整,手扶着金丝眼镜", # player状态的自然语言描述,开发者可以随意添加 205 | } 206 | 207 | 208 | close_packet = { 209 | "func": "close" 210 | } -------------------------------------------------------------------------------- /nuwa/test/test_conversation.py: -------------------------------------------------------------------------------- 1 | import json 2 | import socket 3 | import time 4 | import uuid 5 | import pathlib 6 | import sys 7 | sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) 8 | 9 | PROJECT_ROOT_PATH = pathlib.Path(__file__).parent.parent.parent / "example_project" 10 | import threading 11 | 12 | engine_url = "::1" 13 | engine_port = 8199 14 | game_url = "::1" 15 | game_port = 8084 16 | sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 17 | sock.bind(("::1", game_port)) 18 | 19 | def send_data(data, max_packet_size=6000): 20 | # UUID作为消息ID 21 | msg_id = uuid.uuid4().hex 22 | # 将json字符串转换为bytes 23 | data = json.dumps(data).encode('utf-8') 24 | # 计算数据包总数 25 | packets = [data[i: i + max_packet_size] for i in range(0, len(data), max_packet_size)] 26 | 27 | total_packets = len(packets) 28 | for i, packet in enumerate(packets): 29 | # 构造UDP数据包头部 30 | #print("sending packet {} of {}, size: {} KB".format(i + 1, total_packets, self.calculate_str_size_in_kb(packet))) 31 | header = f"{msg_id}@{i + 1}@{total_packets}".encode('utf-8') 32 | # 发送UDP数据包 33 | sock.sendto(header + b"@" + packet, (engine_url, engine_port)) 34 | #sock.close() 35 | 36 | def test_engine_init_memory(): 37 | 38 | """ 39 | 测试引擎初始化 40 | 向引擎发送初始化包,检查引擎是否正确初始化 41 | """ 42 | 43 | # 初始化包 44 | pack1 = {"func":"init", 45 | # 必填字段,代表在什么场景初始化 46 | "scene_name": "荒野小镇", 47 | "language": "C", 48 | # 下面是🉑️选 49 | "npc": []} 50 | # 发送初始化包到引擎 51 | print("sending for init") 52 | send_data(pack1) 53 | #time.sleep(180) 54 | 55 | test_engine_init_memory() 56 | time.sleep(5) 57 | 58 | def test_conversation(): 59 | 60 | """ 61 | 测试引擎wake_up函数 62 | 向引擎发送初始化包,检查引擎是否正确初始化 63 | wakeup包例: 64 | { 65 | "func":"wake_up", 66 | "npc_name": "王大妈", 67 | "position": "李大爷家", 68 | "observation": ["李大爷", "椅子#1","椅子#2","椅子#3[李大爷占用]",床] 69 | "time": "2021-01-01 12:00:00", # 游戏世界的时间戳 70 | } 71 | 预期返回包: 72 | { 73 | "name":"action", 74 | "npc_name":"王大妈", 75 | "action":"chat", 76 | "object":"李大爷", 77 | "parameters":["你吃饭了没?"], 78 | } 79 | :return: 80 | """ 81 | 82 | # 初始化包 83 | pack1 = { 84 | "func":"create_conversation", 85 | "npc":["土匪Red","土匪Slim","牛仔John"], # 参与对话的NPC 86 | "scenario_name": "荒野小镇", 87 | "location":"荒野小镇", # 对话地点 88 | "topic":"土匪Red的经历", # 对话主题,可以留空,gpt会自发选择一个主题。 89 | "npc_states": [ 90 | { 91 | "position": "荒野小镇", 92 | "observation": { 93 | "people": ["警长Woody", "土匪Slim"], 94 | "items": ["椅子1","椅子2","椅子3","床"], 95 | "locations": [] 96 | }, 97 | "backpack":["黄瓜", "1000元", "老报纸"] 98 | }, 99 | { 100 | "position": "李大爷家", 101 | "observation": { 102 | "people": ["警长Woody", "土匪Red"], 103 | "items": ["椅子1","椅子2","椅子3","床"], 104 | "locations": [] 105 | }, 106 | "backpack":["优质西瓜", "大砍刀", "黄金首饰"] 107 | }], 108 | # 下面是为了解决玩家/npc插入对话的问题 109 | "starting": "嘿你们在干什么呢", # 玩家插入发言,可以留空 110 | "player_desc": "是一位来自未来世界的枪手,有非常精湛的射击技术。", 111 | "memory_k": 3, 112 | "length": "S", 113 | "stream": True 114 | } 115 | 116 | # 发送初始化包到引擎 117 | print("sending for conversation") 118 | send_data(pack1) 119 | 120 | # test_conversation() 121 | # time.sleep(9) 122 | 123 | def send_pack_create(): 124 | pack1 = { 125 | "func":"confirm_conversation_line", 126 | "conversation_id":"76ee76f0-7d13-499b-a8ad-c4744cf44aea", 127 | "index":18 128 | } 129 | print("sending pack1") 130 | send_data(pack1) 131 | 132 | #send_pack_create() 133 | #time.sleep(5) 134 | 135 | def test_conversation_re_creation(): 136 | pack1 = { 137 | "func":"re_create_conversation", 138 | "id":"1234567890", 139 | "character":"警长", 140 | "interruption": "", # 玩家插入发言,可以留空 141 | "player_desc": "", # 玩家的个性描述 142 | "memory_k": 3, 143 | "length": "M", 144 | "stream": True} 145 | 146 | print("sending for conversation re-creation") 147 | send_data(pack1) 148 | 149 | #test_conversation_re_creation() 150 | #time.sleep(10) 151 | 152 | def close_engine(): 153 | pack1 = { 154 | "func":"close" 155 | } 156 | 157 | print("send package to close engine") 158 | send_data(pack1) 159 | 160 | #close_engine() 161 | #time.sleep(5) -------------------------------------------------------------------------------- /nuwa/test/test_database.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from nuwa.src.utils.database import PickleDB 3 | from nuwa.src.config.config import PROJECT_ROOT_PATH 4 | import os 5 | 6 | class TestPickleDBManager: 7 | @pytest.fixture(autouse=True) 8 | def setup_and_teardown(self): 9 | self.db_path = PROJECT_ROOT_PATH / "test" / "test.db" 10 | self.db = PickleDB(self.db_path, auto_dump=True) 11 | yield 12 | os.remove(self.db_path) 13 | 14 | def test_set_and_get(self): 15 | self.db.set('key1', 'value1') 16 | assert self.db.get('key1') == 'value1' 17 | assert self.db.get('key2') is False 18 | 19 | def test_delete(self): 20 | self.db.set('key1', 'value1') 21 | assert self.db.delete('key1') is True 22 | assert self.db.get('key1') is False 23 | assert self.db.delete('key2') is False 24 | 25 | def test_dump(self): 26 | self.db.set('key1', 'value1') 27 | assert self.db.dump() is True 28 | -------------------------------------------------------------------------------- /nuwa/test/test_embedding.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nuwa.src.config.config import NPC_MEMORY_CONFIG 4 | 5 | from nuwa.src.utils.embedding import LocalEmbedding, HuggingFaceEmbedding 6 | 7 | # 测试样例 8 | test_cases = [ 9 | ("你好", NPC_MEMORY_CONFIG["hf_model_id"], NPC_MEMORY_CONFIG["hf_dim"]), 10 | ("世界和平", NPC_MEMORY_CONFIG["hf_model_id"], NPC_MEMORY_CONFIG["hf_dim"]), 11 | ] 12 | 13 | 14 | @pytest.mark.parametrize("input_string,model_name,vector_width", test_cases) 15 | def test_LocalEmbedding(input_string, model_name, vector_width): 16 | embedding = LocalEmbedding(model_name=model_name, vector_width=vector_width) 17 | vector = embedding.embed_text(input_string) 18 | # 检查返回的向量长度是否正确 19 | assert len(vector) == vector_width 20 | 21 | 22 | @pytest.mark.parametrize("input_string,model_name,vector_width", test_cases) 23 | def test_HuggingFaceEmbedding(input_string, model_name, vector_width): 24 | embedding = HuggingFaceEmbedding(model_name=model_name, vector_width=vector_width) 25 | vector = embedding.embed_text(input_string) 26 | # 检查返回的向量长度是否正确 27 | assert len(vector) == vector_width 28 | -------------------------------------------------------------------------------- /nuwa/test/test_npc_action.py: -------------------------------------------------------------------------------- 1 | """ 2 | NPC自主行为的测试用例 3 | 本脚本只发包,测试结果需要手动查看logs文件夹下对应时间戳的日志。 4 | 例如: 5 | nuwa/logs/engine_2023-07-29-23:58:57.log 6 | """ 7 | 8 | import json 9 | import socket 10 | import time 11 | import uuid 12 | import logging 13 | import multiprocessing 14 | import time,os 15 | 16 | from nuwa.src.engine import NPCEngine 17 | from nuwa.src.config.config import FILE_HANDLER, CONSOLE_HANDLER, PROJECT_ROOT_PATH 18 | from nuwa.test.test_config.test_packets import init_packet, wakeup_packet_1, wakeup_packet_2, wakeup_packet_3, \ 19 | action_done_packet_1,action_done_packet_2 20 | 21 | 22 | logger = logging.getLogger("TEST") 23 | logger.addHandler(CONSOLE_HANDLER) 24 | logger.addHandler(FILE_HANDLER) 25 | logger.setLevel(logging.DEBUG) 26 | 27 | # # 启动服务器进程 28 | # path = PROJECT_ROOT_PATH / "src" / "engine.py" 29 | # os.popen("python " + str(path)) 30 | 31 | def send_data(data, max_packet_size=6000): 32 | engine_url = "::1" 33 | engine_port = 8199 34 | game_url = "::1" 35 | game_port = 8084 36 | sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 37 | sock.bind(("::1", game_port)) 38 | 39 | # UUID作为消息ID 40 | msg_id = uuid.uuid4().hex 41 | # 将json字符串转换为bytes 42 | data = json.dumps(data).encode('utf-8') 43 | # 计算数据包总数 44 | packets = [data[i: i + max_packet_size] for i in range(0, len(data), max_packet_size)] 45 | total_packets = len(packets) 46 | for i, packet in enumerate(packets): 47 | # 构造UDP数据包头部 48 | header = f"{msg_id}@{i + 1}@{total_packets}".encode('utf-8') 49 | # 发送UDP数据包 50 | sock.sendto(header + b"@" + packet, (engine_url, engine_port)) 51 | sock.close() 52 | 53 | 54 | def test_engine_init(): 55 | """ 56 | 测试引擎初始化 57 | 向引擎发送初始化包,检查引擎是否正确初始化 58 | 初始化包例: 59 | 请参考test_config.test_packets 60 | :return: 61 | """ 62 | print(init_packet) 63 | # 发送初始化包到引擎 64 | print("sending first") 65 | send_data(init_packet) 66 | print("sent first") 67 | 68 | def test_get_purpose(): 69 | """ 70 | 测试NPC的目的生成 71 | :return: 72 | """ 73 | pass 74 | 75 | def test_get_action(): 76 | """ 77 | 测试NPC的动作生成 78 | :return: 79 | """ 80 | pass 81 | 82 | def test_action_done(): 83 | """ 84 | 发送动作完成包到引擎 85 | GAME发送的包: 86 | 参考test_config.test_packets 87 | 引擎返回的包: 88 | { 89 | "func":"action_done", 90 | "npc_name":"王大妈", 91 | "action":"chat", 92 | "object":"李大爷", 93 | "parameters":["你吃饭了没?"], 94 | } 95 | :return: 96 | """ 97 | 98 | print("sending") 99 | send_data(action_done_packet_1) 100 | print(action_done_packet_1) 101 | send_data(action_done_packet_2) 102 | print(action_done_packet_2) 103 | print("all done") 104 | 105 | def test_wake_up(): 106 | """ 107 | 测试引擎wake_up函数 108 | 向引擎发送初始化包,检查引擎是否正确初始化 109 | wakeup包例: 110 | 请参考test_config.test_packets 111 | 预期返回包: 112 | { 113 | "name":"action", 114 | "npc_name":"王大妈", 115 | "action":"chat", 116 | "object":"李大爷", 117 | "parameters":["你吃饭了没?"], 118 | } 119 | :return: 120 | """ 121 | # 发送初始化包到引擎 122 | print("sending first") 123 | send_data(wakeup_packet_1) 124 | print(wakeup_packet_1) 125 | send_data(wakeup_packet_2) 126 | print(wakeup_packet_2) 127 | send_data(wakeup_packet_3) 128 | print(wakeup_packet_1) 129 | print("all done") 130 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | import nuwa 4 | 5 | setup( 6 | name='nuwa', 7 | version=nuwa.__version__, 8 | packages=find_packages(), 9 | entry_points={ 10 | 'console_scripts': [ 11 | 'nuwa=nuwa.src.utils.cli:main', 12 | ], 13 | }, 14 | # 依赖项可以在这里列出,例如: 15 | install_requires=[ 16 | 'numpy', 17 | 'torch' 18 | ], 19 | package_data={ 20 | # 确保你的包名正确 21 | 'nuwa': ['./material/templates/template.zip'], 22 | }, 23 | # 其他元数据,例如作者、描述、许可证等... 24 | ) 25 | 26 | 27 | --------------------------------------------------------------------------------