├── tests
├── __init__.py
├── myla
│ ├── __init__.py
│ ├── llms
│ │ ├── __init__.py
│ │ └── mock_test.py
│ ├── vectorstores
│ │ ├── __init__.py
│ │ ├── xinference_embeddings_test.py
│ │ ├── record_test.py
│ │ └── faiss_group_test.py
│ ├── utils_test.py
│ ├── threads_test.py
│ ├── files_test.py
│ ├── persistence_test.py
│ ├── assistants_test.py
│ └── users_test.py
├── tools
│ ├── __init__.py
│ └── sync_tool.py
├── perf
│ ├── .gitignore
│ └── vs.py
└── run.sh
├── myla
├── extensions
│ ├── __init__.py
│ └── tools
│ │ ├── __init__.py
│ │ ├── qa_summary.py
│ │ ├── rephrase.py
│ │ └── qa_retrieval.py
├── _version.py
├── _logging.py
├── webui
│ ├── statics
│ │ ├── welcome.md
│ │ └── images
│ │ │ └── screenshot.png
│ ├── __init__.py
│ ├── _web_template.py
│ └── templates
│ │ └── index.html
├── projects.py
├── __init__.py
├── _env.py
├── vectorstores
│ ├── loaders.py
│ ├── pandas_loader.py
│ ├── _embeddings.py
│ ├── xinference_embeddings.py
│ ├── pdf_loader.py
│ ├── sentence_transformers_embeddings.py
│ ├── _base.py
│ ├── chromadb_vectorstore.py
│ ├── lancedb_vectorstore.py
│ ├── __init__.py
│ ├── faiss_vectorstore.py
│ └── faiss_group.py
├── llms
│ ├── utils.py
│ ├── mock.py
│ ├── backend.py
│ ├── __init__.py
│ ├── chatglm.py
│ └── openai.py
├── permissions.py
├── _tools.py
├── iur.py
├── persistence.py
├── _auth.py
├── tools.py
├── utils.py
├── files.py
├── assistants.py
├── __main__.py
├── _run_scheduler.py
├── threads.py
├── _entry.py
├── retrieval.py
├── messages.py
├── runs.py
├── _models.py
└── _llm.py
├── setup.py
├── requirements_dev.txt
├── js
├── build.sh
├── .gitignore
├── public
│ └── index.html
├── src
│ ├── index.css
│ ├── index.js
│ ├── org_selector.js
│ ├── user.js
│ ├── settings.js
│ ├── secret_key.js
│ ├── user_admin.js
│ ├── members.js
│ └── chat.js
└── package.json
├── pyproject.toml
├── .pre-commit-config.yaml
├── requirements.txt
├── examples
├── llm_sync.py
├── upload_file.py
├── embeddings.py
├── llm_debug.py
├── docs_summary.py
├── lancedb_vs.py
├── chatbot-openai-sdk.py
└── chatbot.py
├── scripts
├── update_0.2.22.sql
└── update_0.2.26.sql
├── LICENSE
├── env-example.txt
├── setup.cfg
├── README_zh_CN.md
├── .gitignore
└── README.md
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/myla/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/tools/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/myla/extensions/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/myla/llms/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/myla/extensions/tools/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/myla/vectorstores/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/myla/_version.py:
--------------------------------------------------------------------------------
1 | VERSION = '0.2.35'
2 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | setuptools.setup()
4 |
--------------------------------------------------------------------------------
/tests/perf/.gitignore:
--------------------------------------------------------------------------------
1 | *.csv
2 | vs
3 | data
4 | *.pkl
5 | *.md
--------------------------------------------------------------------------------
/tests/run.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | python -m unittest discover -p "*_test.py"
--------------------------------------------------------------------------------
/myla/_logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | logger = logging.getLogger('myla')
4 |
--------------------------------------------------------------------------------
/requirements_dev.txt:
--------------------------------------------------------------------------------
1 | build
2 | twine
3 | flake8
4 | autopep8
5 | isort
6 | pre-commit
7 | pydoc-markdown
8 | aiounittest
--------------------------------------------------------------------------------
/myla/webui/statics/welcome.md:
--------------------------------------------------------------------------------
1 | ## Muyu Local Assistant 🚀
2 |
3 |
4 | * [Docs](/api/docs)
5 | * [API Debugging](/api/swagger)
--------------------------------------------------------------------------------
/myla/webui/statics/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muyuworks/myla/HEAD/myla/webui/statics/images/screenshot.png
--------------------------------------------------------------------------------
/myla/projects.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from . import _models
4 |
5 |
6 | class Project(_models.DBModel, table=True):
7 | name: Optional[str] = None
8 |
--------------------------------------------------------------------------------
/js/build.sh:
--------------------------------------------------------------------------------
1 | npm run build
2 |
3 | mkdir -p ../myla/webui/statics/aify
4 | cp build/static/js/*.js ../myla/webui/statics/aify/aify.js
5 | cp build/static/css/*.css ../myla/webui/statics/aify/aify.css
6 |
--------------------------------------------------------------------------------
/myla/__init__.py:
--------------------------------------------------------------------------------
1 | from ._version import VERSION
2 | __version__ = VERSION
3 |
4 | from ._api import api
5 | from ._entry import entry
6 | from ._logging import logger
7 | from ._run_scheduler import RunScheduler
8 |
--------------------------------------------------------------------------------
/myla/_env.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | _here = os.path.abspath(os.path.join(os.path.dirname(__file__)))
4 |
5 |
6 | def webui_dir():
7 | "Returns the directory where webuid resources ared stored."
8 | return os.path.join(_here, 'webui')
9 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools", 'cython', "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [tool.pyright]
6 | venvPath = "."
7 | venv = ".venv"
8 |
9 | [tool.yaml]
10 | validate = false
11 |
--------------------------------------------------------------------------------
/myla/vectorstores/loaders.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Iterator, Optional, Dict
3 | from ._base import Record
4 |
5 |
6 | class Loader(ABC):
7 |
8 | @abstractmethod
9 | def load(self, file, metadata: Optional[Dict] = None) -> Iterator[Record]:
10 | """Load data from a file"""
11 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/hhatto/autopep8
3 | rev: v2.0.4
4 | hooks:
5 | - id: autopep8
6 | - repo: https://github.com/pycqa/isort
7 | rev: 5.13.2
8 | hooks:
9 | - id: isort
10 | - repo: https://github.com/pycqa/flake8
11 | rev: 7.0.0
12 | hooks:
13 | - id: flake8
14 |
--------------------------------------------------------------------------------
/myla/llms/utils.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List
2 |
3 |
4 | def plain_messages(messages: List[Dict], model=None, roles=['user', 'assistant', 'system']):
5 | text = []
6 | for m in messages:
7 | role = m['role']
8 | if role in roles:
9 | text.append(f"{role}: {m['content']}")
10 | return '\n'.join(text)
11 |
--------------------------------------------------------------------------------
/myla/webui/__init__.py:
--------------------------------------------------------------------------------
1 | from ._web_template import render
2 | from starlette.requests import Request
3 |
4 |
5 | async def assistant(request: Request):
6 | ctx = {
7 | "assistant_id": request.path_params.get("assistant_id", ''),
8 | "chat_mode": True
9 | }
10 | return await render('index.html', context=ctx)(request=request)
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # API Server
2 | uvicorn
3 | starlette
4 | fastapi
5 | python-dotenv
6 |
7 | # Web
8 | jinja2
9 |
10 | # HTTP Client
11 | aiohttp
12 |
13 | # Persistence
14 | sqlmodel
15 |
16 | # LLMs
17 | openai
18 |
19 | # File upload
20 | python-multipart
21 |
22 | pandas
23 | aiofiles
24 |
25 | sentence_transformers
26 | faiss-cpu
27 |
28 | Authlib
--------------------------------------------------------------------------------
/examples/llm_sync.py:
--------------------------------------------------------------------------------
1 | from myla import llms
2 | import dotenv
3 |
4 | dotenv.load_dotenv(".env")
5 |
6 | llm = llms.get("gpt-3.5-turbo")
7 |
8 | resp = llm.sync_generate(instructions="hi")
9 | print(resp)
10 |
11 | resp = llm.sync_chat(messages=[{"role": "user", "content": "hi"}], stream=True)
12 | for r in resp:
13 | print(r, end='', flush=True)
14 | print("\n")
15 |
--------------------------------------------------------------------------------
/tests/myla/utils_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from myla import utils
3 |
4 |
5 | class TestUtils(unittest.TestCase):
6 | def test_generate_id(self):
7 | print('uuid: ', utils.uuid().hex)
8 | print("sha1: ", utils.sha1(utils.uuid().bytes).hex())
9 | print("Id: ", utils.random_id())
10 | print("SecretKey: ", utils.random_key())
11 |
--------------------------------------------------------------------------------
/js/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/upload_file.py:
--------------------------------------------------------------------------------
1 | import openai
2 |
3 | openai.api_key = "sk-"
4 | openai.base_url = "http://localhost:2000/api/v1/"
5 |
6 | openai.files.create(
7 | #file=open("./examples/upload_file.py", 'rb'),
8 | file=open("./data/myla_test_kb.json", 'rb'),
9 | purpose="assistants",
10 | extra_body={"embeddings": "category,query"}
11 | )
12 |
13 | files = openai.files.list(purpose="assistants")
14 |
15 | print(files)
16 |
--------------------------------------------------------------------------------
/examples/embeddings.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import numpy as np
3 | import faiss
4 | from myla.vectorstores.sentence_transformers_embeddings import SentenceTransformerEmbeddings
5 |
6 | embeddings = SentenceTransformerEmbeddings(model_name="/Users/shellc/Downloads/bge-large-zh-v1.5")
7 |
8 | v = asyncio.run(embeddings.aembed("hello"))
9 |
10 | v = np.array([v], dtype=np.float32)
11 |
12 | faiss.normalize_L2(v)
13 |
14 | n = np.linalg.norm(v, ord=2)
15 | print(n)
16 |
--------------------------------------------------------------------------------
/tests/tools/sync_tool.py:
--------------------------------------------------------------------------------
1 | from myla.tools import Context, Tool
2 | import time
3 | import asyncio
4 |
5 | class SyncTool(Tool):
6 | def execute(self, context: Context) -> None:
7 | for i in range(10):
8 | print(f"SyncTool: {i}")
9 | time.sleep(3)
10 |
11 | class AsyncTool(Tool):
12 | async def execute(self, context: Context) -> None:
13 | for i in range(10):
14 | print(f"AsyncTool: {i}")
15 | await asyncio.sleep(3)
16 |
--------------------------------------------------------------------------------
/tests/myla/llms/mock_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from myla import llms
4 | from myla import utils
5 |
6 |
7 | class TestMockLLM(unittest.TestCase):
8 | def test_chat(self):
9 | llm = llms.get("mock@mock")
10 | self.assertIsNotNone(llm)
11 |
12 | g = utils.sync_call(llm.chat, messages=[{'role': 'user', 'content': 'hello'}])
13 | self.assertEqual('hello', g)
14 |
15 | g = utils.sync_call(llm.generate, instructions="hello")
16 | self.assertEqual('hello', g)
17 |
--------------------------------------------------------------------------------
/js/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Aify
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
--------------------------------------------------------------------------------
/tests/myla/vectorstores/xinference_embeddings_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from myla.vectorstores.xinference_embeddings import XinferenceEmbeddings
4 |
5 |
6 | class XinferenceTests(unittest.TestCase):
7 | def test_connect(self):
8 | embed = XinferenceEmbeddings(
9 | base_url="http://localhost:9997",
10 | model_id="bge-small-zh",
11 | instruction="Represent the sentence for searching the most similar sentences from the corpus."
12 | )
13 | embeds = embed.embed("你好")
14 | self.assertEqual(len(embeds), 512)
15 |
--------------------------------------------------------------------------------
/examples/llm_debug.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from myla.llms.chatglm import ChatGLM
3 |
4 |
5 | async def main():
6 | openai = ChatGLM()
7 | r = await openai.chat(messages=[{
8 | "role": "system",
9 | "content": "你是谁"
10 | }],
11 | stream=True,
12 | model="/Users/shellc/Workspaces/chatglm.cpp/chatglm-ggml.bin"
13 | )
14 | print(r)
15 | async for c in r:
16 | print(c, end='', flush=True)
17 |
18 |
19 | if __name__ == '__main__':
20 | import dotenv
21 | dotenv.load_dotenv(".env")
22 |
23 | asyncio.run(main=main())
24 |
--------------------------------------------------------------------------------
/myla/permissions.py:
--------------------------------------------------------------------------------
1 | #from ._logging import logger
2 |
3 |
4 | def check(resource_type, resource_id, org_id, project_id, owner_id, user_id, orgs, permission) -> bool:
5 | #logger.debug(f"Checking permission {permission} for {resource_type} {resource_id} in org {org_id} and project {project_id}, orgs={orgs}")
6 |
7 | role = None
8 | if org_id in orgs:
9 | role = orgs[org_id].role
10 |
11 | if permission == "read":
12 | if role == "reader" or role == "owner":
13 | return True
14 | elif permission == "write":
15 | if role == "owner":
16 | return True
17 |
18 | return False
19 |
--------------------------------------------------------------------------------
/examples/docs_summary.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import myla.llms as llms
3 |
4 | DOCS = """
5 | []
6 | """
7 |
8 | QUERY = "送给谢顶男票的礼物"
9 |
10 | INSTRUCTIONS = """
11 | 你是专业的问答分析助手。下面是JSON格式的问答记录。
12 | <问答记录开始>
13 | {docs}
14 | <问答记录介绍>
15 |
16 | 请根据问答记录生成新问题的候选回答。
17 | 新问题: {query}
18 | 候选回答:
19 | """
20 |
21 |
22 | async def main():
23 | r = await llms.get().chat(messages=[{
24 | "role": "system",
25 | "content": INSTRUCTIONS.format(docs=DOCS, query=QUERY)
26 | }], stream=False)
27 | print(r)
28 |
29 |
30 | if __name__ == '__main__':
31 | import dotenv
32 | dotenv.load_dotenv(".env")
33 |
34 | asyncio.run(main=main())
35 |
--------------------------------------------------------------------------------
/myla/llms/mock.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List
2 | from .backend import LLM
3 |
4 |
5 | class MockLLM(LLM):
6 | def __init__(self) -> None:
7 | super().__init__()
8 |
9 | async def chat(self, messages: List[Dict], model=None, stream=False, **kwargs):
10 | last_message = messages[-1]['content']
11 |
12 | if stream:
13 | async def iter():
14 | for c in [last_message]:
15 | yield c
16 | return iter()
17 | else:
18 | return last_message
19 |
20 | async def generate(self, instructions: str, model=None, stream=False, **kwargs):
21 | return instructions
22 |
--------------------------------------------------------------------------------
/js/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
3 | 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
4 | 'Noto Color Emoji';
5 | }
6 | a {
7 | text-decoration: none;
8 | }
9 | .thread-close, .anticon {
10 | transform: translate(-0.5px, -3px);
11 | }
12 |
13 | /* Hide scrollbar for Chrome, Safari and Opera */
14 | .scrollbar-none::-webkit-scrollbar {
15 | display: none;
16 | }
17 |
18 | /* Hide scrollbar for IE, Edge and Firefox */
19 | .scrollbar-none {
20 | -ms-overflow-style: none; /* IE and Edge */
21 | scrollbar-width: none; /* Firefox */
22 | }
--------------------------------------------------------------------------------
/scripts/update_0.2.22.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE thread ADD tag TEXT;
2 | CREATE INDEX ix_thread_tag ON thread (tag);
3 |
4 | ALTER TABLE message ADD tag TEXT;
5 | CREATE INDEX ix_message_tag ON message (tag);
6 |
7 | ALTER TABLE assistant ADD tag TEXT;
8 | CREATE INDEX ix_assistant_tag ON assistant (tag);
9 |
10 | ALTER TABLE file ADD tag TEXT;
11 | CREATE INDEX ix_file_tag ON thread (tag);
12 |
13 | ALTER TABLE organization ADD tag TEXT;
14 | CREATE INDEX ix_organization_tag ON file (tag);
15 |
16 | ALTER TABLE run ADD tag TEXT;
17 | CREATE INDEX ix_run_tag ON thread (tag);
18 |
19 | ALTER TABLE secretkey ADD tag TEXT;
20 | CREATE INDEX ix_secretkey_tag ON run (tag);
21 |
22 | ALTER TABLE user ADD tag TEXT;
23 | CREATE INDEX ix_user_tag ON user (tag);
24 |
--------------------------------------------------------------------------------
/js/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { Aify } from './aify'
4 | import { getSecretKey, Login } from './user';
5 |
6 | import 'bootstrap/dist/css/bootstrap.css'
7 | import './index.css'
8 |
9 | export const createAify = (elementId, chatMode = false, assistantId = null) => {
10 | const root = ReactDOM.createRoot(document.getElementById(elementId));
11 | root.render(
12 |
13 | {getSecretKey() ? (
14 |
15 | ) : (
16 |
17 | )}
18 |
19 | );
20 | }
21 | window.createAify = createAify;
22 | //export default createAify;
--------------------------------------------------------------------------------
/examples/lancedb_vs.py:
--------------------------------------------------------------------------------
1 | from myla.vectorstores.lancedb_vectorstore import LanceDB
2 | from myla.vectorstores.sentence_transformers_embeddings import SentenceTransformerEmbeddings
3 | from myla.vectorstores.pandas_loader import PandasLoader
4 |
5 | embeddings = SentenceTransformerEmbeddings(model_name="/Users/shellc/Downloads/bge-large-zh-v1.5")
6 |
7 | vs = LanceDB(db_uri="/tmp/lancedb", embeddings=embeddings)
8 |
9 | collection = "default"
10 |
11 | records = list(PandasLoader().load("./data/202101.csv"))
12 |
13 | vs.create_collection(collection=collection, schema=records[0], mode='overwrite')
14 |
15 | vs.add(collection=collection, records=records)
16 |
17 | print(vs.search(collection=collection, query="新疆"))
18 |
19 | #vs.delete(collection=collection, query="text = 'hello'")
20 |
--------------------------------------------------------------------------------
/scripts/update_0.2.26.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE userorglink ADD "role" VARCHAR;
2 | UPDATE userorglink SET role='owner';
3 |
4 | UPDATE organization SET user_id=(SELECT userorglink.user_id FROM userorglink WHERE organization.id=userorglink.org_id);
5 |
6 | UPDATE assistant SET org_id=(SELECT userorglink.org_id FROM userorglink WHERE assistant.user_id=userorglink.user_id);
7 | UPDATE thread SET org_id=(SELECT userorglink.org_id FROM userorglink WHERE thread.user_id=userorglink.user_id);
8 | UPDATE message SET org_id=(SELECT userorglink.org_id FROM userorglink WHERE message.user_id=userorglink.user_id);
9 | UPDATE run SET org_id=(SELECT userorglink.org_id FROM userorglink WHERE run.user_id=userorglink.user_id);
10 | UPDATE file SET org_id=(SELECT userorglink.org_id FROM userorglink WHERE file.user_id=userorglink.user_id);
--------------------------------------------------------------------------------
/myla/vectorstores/pandas_loader.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from typing import Iterator, Optional, Dict
3 | from ._base import Record
4 | from .loaders import Loader
5 |
6 |
7 | class PandasLoader(Loader):
8 | def __init__(self, ftype="csv") -> None:
9 | super().__init__()
10 | self._ftype = ftype
11 |
12 | def load(self, file, metadata: Optional[Dict] = None) -> Iterator[Record]:
13 | if self._ftype == 'csv':
14 | df = pd.read_csv(file)
15 | elif self._ftype == 'xls' or self._ftype == 'xlsx':
16 | df = pd.read_excel(file)
17 | elif self._ftype == 'json':
18 | df = pd.read_json(file)
19 | else:
20 | raise ValueError(f"File type not supported: {self._ftype}")
21 |
22 | for _, r in df.iterrows():
23 | yield r.to_dict()
24 |
--------------------------------------------------------------------------------
/myla/_tools.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | import json
3 | import os
4 |
5 | from ._logging import logger
6 |
7 | _tools = {}
8 |
9 |
10 | def load_tools():
11 | tools = os.environ.get('TOOLS')
12 | try:
13 | if tools:
14 | tools = json.loads(tools)
15 | for tool in tools:
16 | impl = tool['impl']
17 | ss = impl.split('.')
18 | module = importlib.import_module('.'.join(ss[:-1]))
19 |
20 | args = tool['args'] if 'args' in tool else {}
21 | instance = getattr(module, ss[-1])(**args)
22 | _tools[tool["name"]] = instance
23 | except Exception as e:
24 | logger.error(f"Load tools faild: {tools}", exc_info=e)
25 |
26 |
27 | def get_tool(name):
28 | return _tools.get(name)
29 |
30 |
31 | def get_tools():
32 | return _tools
33 |
--------------------------------------------------------------------------------
/myla/llms/backend.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List
2 |
3 |
4 | class LLM:
5 | def __init__(self, model=None) -> None:
6 | self.model = model
7 |
8 | async def chat(self, messages: List[Dict], model=None, stream=False, **kwargs):
9 | raise NotImplemented()
10 |
11 | async def generate(self, instructions: str, model=None, stream=False, **kwargs):
12 | raise NotImplemented()
13 |
14 | def sync_chat(self, messages: List[Dict], model=None, stream=False, **kwargs):
15 | raise NotImplemented()
16 |
17 | def sync_generate(self, instructions: str, model=None, stream=False, **kwargs):
18 | raise NotImplemented()
19 |
20 |
21 | class Usage:
22 | def __init__(self, prompt_tokens: int = 0, completion_tokens: int = 0) -> None:
23 | self.prompt_tokens = prompt_tokens
24 | self.completion_tokens = completion_tokens
25 |
--------------------------------------------------------------------------------
/myla/vectorstores/_embeddings.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from abc import ABC, abstractmethod
3 | import asyncio
4 |
5 |
6 | class Embeddings(ABC):
7 |
8 | @abstractmethod
9 | def embed_batch(self, texts: List[str], **kwargs) -> List[List[float]]:
10 | """Embed text batch."""
11 |
12 | def embed(self, text: str, **kwargs) -> List[float]:
13 | """Embed text."""
14 | return self.embed_batch(texts=[text], **kwargs)[0]
15 |
16 | async def aembed(self, text: str, **kwargs) -> List[float]:
17 | """Asynchronous Embed text."""
18 | return await asyncio.get_running_loop().run_in_executor(
19 | None, self.embed, text, **kwargs
20 | )
21 |
22 | async def aembed_batch(self, texts: [str], **kwargs) -> List[List[float]]:
23 | """Asynchronous Embed text."""
24 | return await asyncio.get_running_loop().run_in_executor(
25 | None, self.embed_batch, texts, **kwargs
26 | )
27 |
--------------------------------------------------------------------------------
/myla/webui/_web_template.py:
--------------------------------------------------------------------------------
1 | import os
2 | from starlette.templating import Jinja2Templates
3 | from jinja2.exceptions import TemplateNotFound
4 | from starlette.exceptions import HTTPException
5 | from .._env import webui_dir
6 |
7 |
8 | def get_templates(templates_dir=None):
9 | if not templates_dir:
10 | templates_dir = webui_dir()
11 | return Jinja2Templates(directory=os.path.join(templates_dir, 'templates'))
12 |
13 |
14 | _templates = get_templates()
15 |
16 |
17 | def render(template_name, context={}, templates=None):
18 | """Render a template."""
19 | if not templates:
20 | templates = _templates
21 |
22 | async def _request(request):
23 | context['request'] = request
24 | try:
25 | return templates.TemplateResponse(template_name, context=context)
26 | except TemplateNotFound as e:
27 | raise HTTPException(status_code=404, detail=f"Template not found: {e}")
28 | return _request
29 |
--------------------------------------------------------------------------------
/myla/webui/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Myla
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
26 |
--------------------------------------------------------------------------------
/tests/myla/vectorstores/record_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from myla.vectorstores import Record
4 |
5 |
6 | class TestRecord(unittest.TestCase):
7 | def test_values_to_text(self):
8 | r = {
9 | "category": "介绍/推荐",
10 | "query": "XX系列",
11 | "response": [
12 | "XXX"
13 | ],
14 | "msg_id": 0,
15 | "source": "standard"
16 | }
17 | t = Record.values_to_text(r, props=['category'])
18 | self.assertEqual("介绍/推荐", t)
19 |
20 | t = Record.values_to_text(r, props=['category', 'query'])
21 | self.assertEqual("介绍/推荐\001XX系列", t)
22 |
23 | t = Record.values_to_text(r, props=['category', 'query'], separator='\t')
24 | self.assertEqual("介绍/推荐\tXX系列", t)
25 |
26 | try:
27 | Record.values_to_text(r, props='category')
28 | except Exception as e:
29 | self.assertTrue(isinstance(e, ValueError))
30 |
31 | t = Record.values_to_text(r, separator='\t')
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 muyuworks
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/myla/threads_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from myla import threads, persistence
4 |
5 |
6 | class TestUsers(unittest.TestCase):
7 |
8 | def setUp(self) -> None:
9 | self.db = persistence.Persistence(database_url="sqlite://")
10 | self.db.initialize_database()
11 | self.session = self.db.create_session()
12 |
13 | def tearDown(self) -> None:
14 | self.session.close()
15 |
16 | def test_create(self):
17 | t_created = threads.create(thread=threads.ThreadCreate(metadata={'k': 'v'}), session=self.session)
18 | self.assertIsInstance(t_created, threads.ThreadRead)
19 | self.assertEqual(t_created.metadata, {'k': 'v'})
20 |
21 | t_read = threads.get(id=t_created.id, session=self.session)
22 | self.assertEqual(t_read.metadata, t_created.metadata)
23 |
24 | def test_create_with_tag(self):
25 | t_created = threads.create(thread=threads.ThreadCreate(metadata={'k': 'v'}), tag="t_thread", session=self.session)
26 | self.assertIsInstance(t_created, threads.ThreadRead)
27 | thread_list = threads.list(tag="t_thread", session=self.session)
28 | self.assertEqual(len(thread_list.data), 1)
29 | self.assertIsNotNone(thread_list.data[0].id, t_created.id)
30 |
--------------------------------------------------------------------------------
/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "js",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@microsoft/fetch-event-source": "^2.0.1",
7 | "@testing-library/jest-dom": "^5.17.0",
8 | "@testing-library/react": "^13.4.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "antd": "^5.11.1",
11 | "bootstrap": "^5.3.2",
12 | "fetch-event-source": "^1.0.0-alpha.2",
13 | "js-cookie": "^3.0.5",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0",
16 | "react-markdown": "^9.0.0",
17 | "react-scripts": "5.0.1",
18 | "remark-gfm": "^4.0.0",
19 | "web-vitals": "^2.1.4"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": [
29 | "react-app",
30 | "react-app/jest"
31 | ]
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | },
45 | "proxy": "http://127.0.0.1:2000"
46 | }
47 |
--------------------------------------------------------------------------------
/myla/vectorstores/xinference_embeddings.py:
--------------------------------------------------------------------------------
1 | from random import randint
2 | from typing import List
3 |
4 | from xinference_client import RESTfulClient as Client
5 |
6 | from ._embeddings import Embeddings
7 |
8 |
9 | class XinferenceEmbeddings(Embeddings):
10 | def __init__(
11 | self,
12 | base_url,
13 | model_id,
14 | instruction=None
15 | ) -> None:
16 | self._base_url = base_url
17 | self._model_id = model_id
18 | self._instruction = instruction
19 |
20 | client = Client(self._base_url)
21 | #self._model = client.get_model(model_id)
22 | model_ids = model_id.split(",")
23 | self._models = []
24 | for m_id in model_ids:
25 | self._models.append(client.get_model(m_id))
26 |
27 | def embed_batch(self, texts: List[str], **kwargs) -> List[List[float]]:
28 | if self._instruction is not None:
29 | texts = [self._instruction + t for t in texts]
30 |
31 | model = self._get_model()
32 |
33 | embeds = model.create_embedding(texts)
34 | return [e["embedding"] for e in embeds["data"]]
35 |
36 | def _get_model(self):
37 | idx = randint(0, len(self._models) - 1)
38 | return self._models[idx]
39 |
--------------------------------------------------------------------------------
/myla/iur.py:
--------------------------------------------------------------------------------
1 | from .tools import Tool, Context
2 | from . import llms, logger
3 | from .llms import utils
4 |
5 | INSTRUCTIONS_ZH = """
6 | 你是专业的文本分析助手, 负责改写用户回复, 下面是AI助手和用户的对话记录, system 是AI助手的身份设定, user是用户, assistant是AI助手:
7 | -开始对话-
8 | {history}
9 | -结束对话-
10 | 用户最新回复: {last_user_message}
11 | 请你结合对话记录改写用户最新回复。
12 | 如果用户最新回复是好的、谢谢、你好等问候语或礼貌性回复,不要改写用户回复。
13 | 如果用户最新回复是提问,请你以用户的身份改写,使其表述更清晰并包含用户的完整意图, 易于AI助手理解。
14 |
15 | 请直接输出修改后的结果,不要包含前缀说明。
16 | 改写后的用户回复:
17 | """
18 |
19 |
20 | class IURTool(Tool):
21 | async def execute(self, context: Context) -> None:
22 | """
23 | 根据会话历史让 LLM 决定是否需要改写用户最后一条消息
24 | """
25 | last_user_message = None
26 | if len(context.messages) > 0:
27 | if context.messages[-1]["role"] == "user":
28 | last_user_message = context.messages[-1]['content']
29 |
30 | if not last_user_message:
31 | return
32 |
33 | history = utils.plain_messages(messages=context.messages)
34 |
35 | iur_query = await llms.get().generate(INSTRUCTIONS_ZH.format(history=history, last_user_message=last_user_message), temperature=0)
36 |
37 | logger.debug(f"Converstations: \n{history}\n IUR: {iur_query}")
38 |
39 | context.messages[-1]['content'] = iur_query
40 |
41 | context.message_metadata['iur'] = iur_query
42 |
--------------------------------------------------------------------------------
/myla/vectorstores/pdf_loader.py:
--------------------------------------------------------------------------------
1 | import math
2 | from typing import Iterator, Optional, Dict
3 | from myla.vectorstores._base import Record
4 | from .loaders import Loader
5 |
6 |
7 | class PDFLoader(Loader):
8 | def __init__(self, chunk_size=500, chunk_overlap=50) -> None:
9 | super().__init__()
10 | self._chunk_size = chunk_size
11 | self._chunk_overlap = chunk_overlap
12 |
13 | def load(self, file, metadata: Optional[Dict] = None) -> Iterator[Record]:
14 | try:
15 | import pypdf
16 | except ImportError as e:
17 | raise ImportError(
18 | "Could not import pypdf python package. "
19 | "Please install it with `pip install pypdf`."
20 | ) from e
21 | reader = pypdf.PdfReader(file)
22 | for page in reader.pages:
23 | text = page.extract_text()
24 | for s in self._split(text=text):
25 | yield {"text": s}
26 |
27 | def _split(self, text):
28 | for i in range(math.ceil(len(text)/self._chunk_size)):
29 | begin = i * self._chunk_size
30 | end = begin + self._chunk_size
31 |
32 | if i > 0:
33 | begin -= self._chunk_overlap
34 | if begin < 0:
35 | begin = 0
36 | if end > len(text):
37 | end = len(text)
38 |
39 | yield text[begin: end]
40 |
--------------------------------------------------------------------------------
/env-example.txt:
--------------------------------------------------------------------------------
1 | # Persistence
2 | #DATABASE_URL=sqlite:///myla.db
3 | #DATABASE_CONNECT_ARGS={"check_same_thread": false}
4 |
5 | MYLA_DELETE_MODE=soft
6 |
7 | # LLMs
8 |
9 | #LLM_ENDPOINT=http://172.88.0.20:8888/v1/
10 | #LLM_API_KEY=sk-xx
11 | #DEFAULT_LLM_MODEL_NAME=Qwen-14B-Chat-Int4
12 |
13 | # to use ChatGLM as the backend: pip install myla[chatglm]
14 | # the model name for ChatGLM like:
15 | #DEFAULT_LLM_MODEL_NAME=chatglm@/Users/shellc/Workspaces/chatglm.cpp/chatglm-ggml.bin
16 |
17 |
18 | # Ebeddings
19 |
20 | #EMBEDDINGS_IMPL=sentence_transformers
21 | #EMBEDDINGS_MODEL_NAME=/Users/shellc/Downloads/bge-large-zh-v1.5
22 | #EMBEDDINGS_DEVICE=cpu
23 | #EMBEDDINGS_INSTRUCTION=
24 |
25 |
26 | # Vectorstore
27 | # Default vecotrstore backend, options: faiss, lancedb
28 | # to use faiss as the backend: pip install myla[faiss-cpu] or myla[faiss-gpu]
29 | # to use LanceDB as the backend: pip install myla[lancedb]
30 |
31 | #VECTOR_STORE_IMPL=faiss
32 |
33 |
34 | # Tools
35 | # JSON format configurations
36 |
37 | #TOOLS='
38 | #[
39 | # {
40 | # "name": "retrieval",
41 | # "impl": "myla.retrieval.RetrievalTool"
42 | # }
43 | #]
44 | #'
45 |
46 |
47 | # Vectorstore Loaders
48 | # JSON format configurations
49 |
50 | #LOADERS='
51 | #[
52 | # {
53 | # "name": "my_loader",
54 | # "impl": "my_loaders.MyLoader"
55 | # }
56 | #]
57 | #'
58 |
59 |
60 | # Others
61 |
62 | # HuggingFace
63 |
64 | #TOKENIZERS_PARALLELISM=4
--------------------------------------------------------------------------------
/myla/extensions/tools/qa_summary.py:
--------------------------------------------------------------------------------
1 | from myla.tools import Tool, Context
2 | from myla import llms
3 |
4 | DOC_SUMMARY_INSTRUCTIONS_ZH = """
5 | 你是专业的文本分析助手, 你负责为用户问题生成候选答案。
6 |
7 | 你要使用下面的JSON格式的数据, query字段是提问, response字段是回答。
8 |
9 | <数据开始>
10 | {docs}
11 | <数据结束>
12 | 用户提问: {query}
13 |
14 | 请为用户提问生成候选回答,如果用户提问不明确,请用户进一步说明, 生成结果不要包含问题。
15 |
16 | 候选回答:
17 | """
18 |
19 |
20 | class QASummaryTool(Tool):
21 | async def execute(self, context: Context) -> None:
22 | if len(context.messages) == 0:
23 | return
24 |
25 | last_message = context.messages[-1]['content']
26 |
27 | docs = None
28 |
29 | for msg in context.messages:
30 | if msg.get('type') == 'docs':
31 | docs = msg
32 |
33 | if docs:
34 | summary = await llms.get().chat(messages=[{
35 | "role": "system",
36 | "content": DOC_SUMMARY_INSTRUCTIONS_ZH.format(docs=docs['content'], query=last_message)
37 | }], stream=False, temperature=0)
38 | if summary:
39 | docs['content'] = summary
40 |
41 | messages = [] # 删除对话历史, 只保留 system Message 和最后一条用户消息
42 | for msg in context.messages:
43 | if not (msg['role'] == 'user' or msg['role'] == 'assistant'):
44 | messages.append(msg)
45 | messages.append({"role": "user", "content": last_message})
46 |
47 | context.messages = messages
48 |
--------------------------------------------------------------------------------
/tests/myla/files_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from myla import files, persistence, utils
4 |
5 |
6 | class TestFiles(unittest.TestCase):
7 |
8 | def setUp(self) -> None:
9 | self.db = persistence.Persistence(database_url="sqlite://")
10 | self.db.initialize_database()
11 | self.session = self.db.create_session()
12 |
13 | def tearDown(self) -> None:
14 | self.session.close()
15 |
16 | def test_file_create(self):
17 | id = utils.random_id()
18 | file = files.FileUpload(purpose="assistant", metadata={"k1": "v1", "k2": "v2"})
19 | file_created = files.create(id=id, file=file, filename="filename", bytes=0, session=self.session)
20 |
21 | self.assertEqual(id, file_created.id)
22 | self.assertEqual("assistant", file_created.purpose)
23 | self.assertEqual({"k1": "v1", "k2": "v2"}, file_created.metadata)
24 |
25 | file_read = files.get(id=id, session=self.session)
26 | self.assertEqual(id, file_read.id)
27 | self.assertEqual("assistant", file_read.purpose)
28 | self.assertEqual({"k1": "v1", "k2": "v2"}, file_read.metadata)
29 |
30 | status = files.delete(id=id, session=self.session)
31 | self.assertEqual(id, status.id)
32 | self.assertEqual("file.deleted", status.object)
33 | self.assertEqual(True, status.deleted)
34 |
35 | file_read = files.get(id=id, session=self.session)
36 | self.assertIsNone(file_read)
37 |
--------------------------------------------------------------------------------
/js/src/org_selector.js:
--------------------------------------------------------------------------------
1 | import { Select } from "antd"
2 | import { useEffect, useState } from "react"
3 |
4 | export const OrgSelector = () => {
5 | const [orgs, setOrgs] = useState()
6 | const [defaultOrg, setDefaultOrg] = useState(localStorage.getItem("org_id"))
7 |
8 | const loadOrgs = () => {
9 |
10 | fetch("/api/v1/organizations")
11 | .then(res => res.json())
12 | .then(data => {
13 | if (defaultOrg === null) {
14 | /*data.data.map(org => {
15 | if (org.is_primary) {
16 | console.log(org.id)
17 | setDefaultOrg(org.id)
18 | }
19 | })*/
20 |
21 | setDefaultOrg(data.data[0].id)
22 | }
23 |
24 | setOrgs(data.data)
25 | })
26 | }
27 |
28 | const changeOrg = (orgId) => {
29 | localStorage.setItem('org_id', orgId)
30 | document.location.reload()
31 | }
32 |
33 | useEffect(() => {
34 | loadOrgs()
35 | }, [])
36 |
37 | return (
38 |