davidkhala.ai 0.1.6__py3-none-any.whl → 0.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,9 @@
1
1
  import json
2
+ from typing import TypedDict
2
3
 
3
4
  import requests
5
+ from davidkhala.utils.http_request.stream import Request as StreamRequest, as_sse
6
+ from requests import Response, Session
4
7
 
5
8
  from davidkhala.ai.agent.dify.api import API
6
9
 
@@ -11,6 +14,7 @@ class Feedbacks(API):
11
14
  when 'rating'='like', content=None
12
15
  when 'rating'='dislike', content can be filled by end user
13
16
  NOTE: for security reason, api cannot access conversation context associated with the feedback. End user should copy the conversation to comment by themselves.
17
+ # waiting for https://github.com/langgenius/dify/issues/28067
14
18
  """
15
19
  response = requests.get(f"{self.base_url}/app/feedbacks", params={"page": page, "limit": size}, **self.options)
16
20
  if not response.ok:
@@ -19,16 +23,17 @@ class Feedbacks(API):
19
23
  return json.loads(response.text)
20
24
 
21
25
  def list_feedbacks(self):
22
- # TODO https://github.com/langgenius/dify/issues/28067
23
26
  return self.paginate_feedbacks()['data']
24
27
 
28
+
25
29
  class Conversation(API):
26
30
  """
27
31
  Note: The Service API does not share conversations created by the WebApp. Conversations created through the API are isolated from those created in the WebApp interface.
28
32
  It means you cannot get user conversation content from API, API call has only access to conversation created by API
29
33
  """
34
+
30
35
  def __init__(self, api_key: str, user: str):
31
- super().__init__(api_key) # base_url need to be configured afterward if not default
36
+ super().__init__(api_key) # base_url need to be configured afterward if not default
32
37
  self.user = user # user_id, from_end_user_id
33
38
 
34
39
  def paginate_messages(self, conversation_id):
@@ -36,3 +41,58 @@ class Conversation(API):
36
41
  'conversation_id': conversation_id,
37
42
  'user': self.user,
38
43
  })
44
+
45
+ def _chat_request_from(self, template: str, stream, **kwargs):
46
+ """
47
+ :param template:
48
+ :param stream: Note: "Agent Chat App does not support blocking mode"
49
+ :param kwargs:
50
+ :return:
51
+ """
52
+ return {
53
+ 'url': f"{self.base_url}/chat-messages",
54
+ 'method': "POST",
55
+ 'json': {
56
+ 'query': template,
57
+ 'inputs': kwargs.pop('values', {}), # to substitute query/template
58
+ 'response_mode': 'streaming' if stream else 'blocking',
59
+ 'conversation_id': kwargs.pop('conversation_id', None),
60
+ 'user': self.user,
61
+ 'files': kwargs.pop('files', [])
62
+ },
63
+ **kwargs
64
+ }
65
+
66
+ def async_chat(self, template: str, **kwargs) -> tuple[Response, Session]:
67
+ s = StreamRequest(self)
68
+ s.session = Session()
69
+ return s.request(**self._chat_request_from(template, True, **kwargs)), s.session
70
+
71
+ class ChatResult(TypedDict, total=False):
72
+ thought: list[str]
73
+ metadata: dict
74
+
75
+ @staticmethod
76
+ def reduce_chat_stream(response: Response) -> ChatResult:
77
+ r: Conversation.ChatResult = {
78
+ 'thought': [],
79
+ }
80
+ for data in as_sse(response):
81
+ match data['event']:
82
+ case 'agent_thought':
83
+ r['thought'].append(data['thought'])
84
+ case 'message_end':
85
+ r['metadata'] = data['metadata']
86
+ return r
87
+
88
+ def agent_chat(self, template: str, **kwargs) -> ChatResult:
89
+ r, session = self.async_chat(template, **kwargs)
90
+ reduced = Conversation.reduce_chat_stream(r)
91
+ session.close()
92
+ return reduced
93
+
94
+ def bot_chat(self, template: str, **kwargs):
95
+ r = self.request(**self._chat_request_from(template, False, **kwargs))
96
+ assert r.pop('event') == 'message'
97
+ assert r.pop('mode') == 'chat'
98
+ return r
@@ -8,6 +8,7 @@ from urllib.parse import urlparse
8
8
  import requests
9
9
 
10
10
  from davidkhala.ai.agent.dify.api import API, Iterator
11
+ from davidkhala.ai.agent.dify.model import Document as DocumentBase
11
12
 
12
13
 
13
14
  class DatasetDict(TypedDict):
@@ -37,21 +38,14 @@ class DatasetDict(TypedDict):
37
38
  external_knowledge_info: dict
38
39
 
39
40
 
40
- class DocumentDict(TypedDict):
41
- id: str
42
- position: int
43
- data_source_type: str
41
+ class Document(DocumentBase):
44
42
  data_source_info: dict[str, str]
45
43
  data_source_detail_dict: dict[str, dict]
46
44
  dataset_process_rule_id: str
47
- name: str
48
45
  created_from: str
49
46
  created_by: str
50
47
  created_at: int
51
48
  tokens: int
52
- indexing_status: str
53
- error: str
54
- enabled: bool
55
49
  archived: bool
56
50
  display_status: str
57
51
  word_count: int
@@ -91,9 +85,8 @@ class Dataset(API):
91
85
 
92
86
  def upload(self, filename, *, path=None, url=None, document_id=None):
93
87
  """
94
- don't work for html
95
- work for markdown
96
- TODO how to simulate console
88
+ don't work for .html
89
+ work for .md
97
90
  """
98
91
  files = {}
99
92
  if path:
@@ -124,10 +117,10 @@ class Dataset(API):
124
117
  'limit': size
125
118
  })
126
119
 
127
- def list_documents(self) -> Iterable[DocumentDict]:
120
+ def list_documents(self) -> Iterable[Document]:
128
121
  for document_batch in Iterator(self.paginate_documents, None):
129
122
  for document in document_batch:
130
- yield document
123
+ yield Document(**document)
131
124
 
132
125
  def has_document(self, name) -> bool:
133
126
  return any(name == item['name'] for row in self.list_documents() for item in row)
@@ -189,3 +182,10 @@ class Document(API):
189
182
  def delete(self):
190
183
  if self.exist():
191
184
  self.request(self.base_url, "DELETE")
185
+ class Chunk(API):
186
+ def __init__(self, d: Document, segment_id: str):
187
+ super().__init__(d.api_key, f"{d.base_url}/segments/{segment_id}")
188
+ def get(self):
189
+ r= self.request(self.base_url, "GET")
190
+ assert r['doc_form'] # optional value text_model
191
+ return r['data']
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class IndexingStatus(str, Enum):
5
+ WAITING = "waiting"
6
+ PARSING = "parsing"
7
+ SPLITTING = 'splitting'
8
+ INDEXING = "indexing"
9
+ COMPLETED = "completed"
10
+ FAILED = "error"
@@ -0,0 +1,3 @@
1
+ class IndexingError(Exception):
2
+ """Raised when document indexing fails (indexing_status = 'error')"""
3
+ pass
@@ -0,0 +1,31 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from davidkhala.ai.agent.dify.const import IndexingStatus
4
+
5
+
6
+ class Document(BaseModel):
7
+ id: str
8
+ position: int
9
+ data_source_type: str
10
+ data_source_info: dict[str, str]
11
+ name: str
12
+ indexing_status: IndexingStatus
13
+ error: str | None
14
+ enabled: bool
15
+
16
+
17
+ class Dataset(BaseModel):
18
+ id: str
19
+ name: str
20
+ description: str
21
+
22
+
23
+ class JsonData(BaseModel):
24
+ data: list
25
+
26
+
27
+ class NodeOutput(BaseModel):
28
+ """Schema for Output of a Dify node"""
29
+ text: str
30
+ files: list
31
+ json_: list[JsonData] = Field(alias="json") # avoid conflict with .json()
@@ -0,0 +1,9 @@
1
+ from davidkhala.utils.http_request import Request
2
+
3
+
4
+ class API(Request):
5
+ def __init__(self, base_url='http://localhost'):
6
+ super().__init__()
7
+ self.base_url = f"{base_url}/console/api"
8
+ self.__enter__()
9
+
@@ -0,0 +1,158 @@
1
+ from time import sleep
2
+
3
+ from davidkhala.utils.http_request.stream import as_sse, Request as StreamRequest
4
+ from pydantic import BaseModel
5
+
6
+ from davidkhala.ai.agent.dify.interface import IndexingError
7
+ from davidkhala.ai.agent.dify.model import Document, Dataset
8
+ from davidkhala.ai.agent.dify.const import IndexingStatus
9
+ from davidkhala.ai.agent.dify.ops.console import API
10
+ from davidkhala.ai.agent.dify.ops.console.session import ConsoleUser
11
+ from davidkhala.ai.agent.dify.ops.db.orm import Node
12
+
13
+
14
+ class ConsoleKnowledge(API):
15
+ def __init__(self, context: ConsoleUser):
16
+ super().__init__()
17
+ self.base_url = context.base_url
18
+ self.session.cookies = context.session.cookies
19
+ self.options = context.options
20
+
21
+
22
+ class Datasource(ConsoleKnowledge):
23
+ """step 1: Choose a Data Source"""
24
+
25
+ class FirecrawlOutput(BaseModel):
26
+ source_url: str
27
+ description: str
28
+ title: str
29
+ credential_id: str
30
+ content: str
31
+
32
+ def run_firecrawl(self, pipeline: str, node: Node,
33
+ *,
34
+ inputs: dict,
35
+ credential_id: str
36
+ ):
37
+
38
+ url = f"{self.base_url}/rag/pipelines/{pipeline}/workflows/published/datasource/nodes/{node.id}/run"
39
+
40
+ stream_request = StreamRequest(self)
41
+ response = stream_request.request(url, 'POST', json={
42
+ 'inputs': inputs,
43
+ 'datasource_type': node.datasource_type,
44
+ 'credential_id': credential_id,
45
+ "response_mode": "streaming"
46
+ })
47
+
48
+ for data in as_sse(response):
49
+ event = data['event']
50
+ if event == 'datasource_completed':
51
+ return data['data']
52
+ else:
53
+ assert event == 'datasource_processing'
54
+ print(data)
55
+ return None
56
+
57
+ def upload(self):
58
+ "http://localhost/console/api/files/upload?source=datasets"
59
+ # TODO
60
+ "form data"
61
+ {
62
+ "file": "body"
63
+ }
64
+ r = {
65
+ "id": "3898db5b-eb72-4f11-b507-628ad5d28887",
66
+ "name": "Professional Diploma Meister Power Electrical Engineering - Technological and Higher Education Institute of Hong Kong.html",
67
+ "size": 254362,
68
+ "extension": "html",
69
+ "mime_type": "text\/html",
70
+ "created_by": "dbd0b38b-5ef1-4123-8c3f-0c82eb1feacd",
71
+ "created_at": 1764943811,
72
+ "source_url": "\/files\/3898db5b-eb72-4f11-b507-628ad5d28887\/file-preview?timestamp=1764943811&nonce=43b0ff5a13372415be79de4cc7ef398c&sign=7OJ2wiVYc4tygl7yvM1sPn7s0WXDlhHxgX76bsGTD94%3D"
73
+ }
74
+
75
+
76
+ class Operation(ConsoleKnowledge):
77
+ def website_sync(self, dataset: str, document: str, *, wait_until=True):
78
+ """
79
+ cannot be used towards a pipeline dataset. Otherwise, you will see error "no website import info found"
80
+ """
81
+ doc_url = f"{self.base_url}/datasets/{dataset}/documents/{document}"
82
+
83
+ r = self.request(f"{doc_url}/website-sync", "GET")
84
+ assert r == {"result": "success"}
85
+ if wait_until:
86
+ return self.wait_until(dataset, document)
87
+ return None
88
+
89
+ def retry(self, dataset: str, *documents: str, wait_until=True):
90
+ """
91
+ It cannot trigger rerun on success documents
92
+ """
93
+ url = f"{self.base_url}/datasets/{dataset}/retry"
94
+ self.request(url, "POST", json={
95
+ 'document_ids': documents,
96
+ })
97
+ # response status code will be 204
98
+ if wait_until:
99
+ return [self.wait_until(dataset, document) for document in documents]
100
+ return None
101
+
102
+ def rerun(self, dataset: str, *documents: str):
103
+ for document in documents:
104
+ try:
105
+ self.website_sync(dataset, document)
106
+ assert False, "expect IndexingError"
107
+ except IndexingError:
108
+ pass
109
+ return self.retry(dataset, *documents)
110
+
111
+ def wait_until(self, dataset: str, document: str, *,
112
+ expect_status=None,
113
+ from_status=None,
114
+ interval=1
115
+ ):
116
+ if not expect_status:
117
+ expect_status = [IndexingStatus.FAILED, IndexingStatus.COMPLETED]
118
+ url = f"{self.base_url}/datasets/{dataset}/documents/{document}/indexing-status"
119
+ if from_status is None:
120
+ from_status = [IndexingStatus.WAITING, IndexingStatus.PARSING]
121
+ r = self.request(url, "GET")
122
+ status = r['indexing_status']
123
+ assert status in from_status, f"current status: {status}, expect: {from_status}"
124
+ while status not in expect_status:
125
+ sleep(interval)
126
+ r = self.request(url, "GET")
127
+ status = r['indexing_status']
128
+ if status == IndexingStatus.FAILED: raise IndexingError(r['error'])
129
+ return r
130
+
131
+
132
+ class DatasetResult(Dataset):
133
+ chunk_structure: str
134
+
135
+ class RunResult(BaseModel):
136
+ batch: str
137
+ dataset: DatasetResult
138
+ documents: list[Document]
139
+
140
+ class Load(ConsoleKnowledge):
141
+ """
142
+ Processing Documents
143
+ """
144
+
145
+ def async_run(self, pipeline: str, node: Node, inputs: dict, datasource_info_list: list[dict])->RunResult:
146
+ """Ingest new document"""
147
+ url = f"{self.base_url}/rag/pipelines/{pipeline}/workflows/published/run"
148
+ r = self.request(url, "POST", json={
149
+ 'inputs': inputs,
150
+ 'start_node_id': node.id,
151
+ 'is_preview': False,
152
+ 'response_mode': "blocking",
153
+ "datasource_info_list": datasource_info_list,
154
+ 'datasource_type': node.datasource_type
155
+ })
156
+ return RunResult(**r)
157
+
158
+
@@ -0,0 +1,30 @@
1
+ from davidkhala.ai.agent.dify.ops.console import API
2
+
3
+
4
+ class ConsoleUser(API):
5
+ def login(self, email, password,
6
+ *,
7
+ remember_me=True,
8
+ language="en-US"
9
+ ):
10
+ url = f"{self.base_url}/login"
11
+
12
+ r = self.request(url, "POST", json={
13
+ 'email': email,
14
+ 'password': password,
15
+ 'remember_me': remember_me,
16
+ 'language': language,
17
+ })
18
+ assert r == {"result": "success"}
19
+ self.options['headers']['x-csrf-token'] = self.session.cookies.get("csrf_token")
20
+ return self.session.cookies
21
+
22
+ @property
23
+ def me(self) -> dict:
24
+ url = f"{self.base_url}/account/profile"
25
+ return self.request(url, "GET")
26
+
27
+ @property
28
+ def workspace(self) -> dict:
29
+ url = f"{self.base_url}/features"
30
+ return self.request(url, "GET")
@@ -1,10 +1,6 @@
1
- from typing import Any, Optional
1
+ from typing import Any
2
2
 
3
3
  from davidkhala.data.base.pg import Postgres
4
- from sqlalchemy import desc
5
- from sqlalchemy.orm import Session
6
-
7
- from davidkhala.ai.agent.dify.ops.db.orm import AppModelConfig
8
4
 
9
5
 
10
6
  class DB(Postgres):
@@ -13,28 +9,9 @@ class DB(Postgres):
13
9
  super().__init__(connection_string)
14
10
  self.connect()
15
11
 
16
- def get_dict(self, sql): return self.query(sql).mappings().all()
17
-
18
- @property
19
- def accounts(self): return self.get_dict("select name, email from accounts where status = 'active'")
20
-
21
- @property
22
- def apps(self): return self.get_dict("select id, name, mode from apps where status = 'normal'")
23
-
24
- def app_config(self, app_id) -> AppModelConfig | None:
25
- with Session(self.client) as session:
26
- return (
27
- session.query(AppModelConfig)
28
- .filter(AppModelConfig.app_id == app_id)
29
- .order_by(desc(AppModelConfig.created_at))
30
- .first()
31
- )
32
-
33
- def update_app_config(self, record: AppModelConfig, refresh:bool=False) -> AppModelConfig | None:
34
- with Session(self.client) as session:
35
- session.add(record)
36
- session.commit()
37
- if refresh:
38
- session.refresh(record) # 刷新对象,确保拿到数据库生成的字段(如 id)
39
- return record
40
- return None
12
+ def get_dict(self,
13
+ template: str,
14
+ values: dict[str, Any] | None = None,
15
+ request_options: dict[str, Any] | None = None
16
+ ) -> list[dict]:
17
+ return Postgres.rows_to_dicts(self.query(template, values, request_options))
@@ -0,0 +1,34 @@
1
+ from davidkhala.ai.agent.dify.ops.db import DB
2
+ from davidkhala.ai.agent.dify.ops.db.orm import AppModelConfig
3
+ from sqlalchemy.orm import Session
4
+ from sqlalchemy import desc
5
+
6
+ class Studio(DB):
7
+ user_feedbacks_sql = """SELECT mf.conversation_id,
8
+ mf.content,
9
+ m.query,
10
+ m.answer
11
+ FROM message_feedbacks mf
12
+ LEFT JOIN messages m ON mf.message_id = m.id
13
+ WHERE mf.from_source = 'user'"""
14
+
15
+ @property
16
+ def apps(self): return self.get_dict("select id, name, mode from apps where status = 'normal'")
17
+
18
+ def app_config(self, app_id) -> AppModelConfig | None:
19
+ with Session(self.client) as session:
20
+ return (
21
+ session.query(AppModelConfig)
22
+ .filter(AppModelConfig.app_id == app_id)
23
+ .order_by(desc(AppModelConfig.created_at))
24
+ .first()
25
+ )
26
+
27
+ def update_app_config(self, record: AppModelConfig, refresh: bool = False) -> AppModelConfig | None:
28
+ with Session(self.client) as session:
29
+ session.add(record)
30
+ session.commit()
31
+ if refresh:
32
+ session.refresh(record)
33
+ return record
34
+ return None
@@ -0,0 +1,58 @@
1
+ from davidkhala.ai.agent.dify.ops.db import DB
2
+ from davidkhala.ai.agent.dify.ops.db.orm import Graph
3
+
4
+
5
+ class Dataset(DB):
6
+
7
+ def dataset_queries(self, dataset_id, limit=20) -> list[str]:
8
+ template = "select content from dataset_queries where source = 'app' and created_by_role = 'end_user' and dataset_id = :dataset_id limit :limit"
9
+ return self.query(template, {'dataset_id': dataset_id, 'limit': limit}).scalars().all()
10
+
11
+ @property
12
+ def datasets(self):
13
+ template = "select id, name, description, indexing_technique, index_struct, embedding_model, embedding_model_provider, collection_binding_id, retrieval_model, icon_info, runtime_mode, pipeline_id, chunk_structure from datasets"
14
+ return self.get_dict(template)
15
+
16
+ def is_pipeline(self, id: str):
17
+ template = "select runtime_mode = 'rag_pipeline' from datasets where id = :id"
18
+ return self.query(template, {'id': id}).scalar()
19
+
20
+ @property
21
+ def data_source_credentials(self):
22
+ template = "select id, name, plugin_id, auth_type from datasource_providers"
23
+ return self.get_dict(template)
24
+
25
+ def credential_id_by(self, name, provider) -> list[str]:
26
+ template = "select id from datasource_providers where name = :name and provider = :provider"
27
+ return self.query(template, {'name': name, 'provider': provider}).scalars().all()
28
+
29
+ def documents(self, dataset_id: str):
30
+ template = "select id, name,created_from, created_at from documents where dataset_id = :dataset_id"
31
+ return self.query(template, {'dataset_id': dataset_id})
32
+
33
+
34
+ class Document(DB):
35
+ def hit_documents(self, top_k: int = 3):
36
+ template = "SELECT dataset_id, document_id, content FROM document_segments ORDER BY hit_count DESC LIMIT :top_k"
37
+ return self.get_dict(template, {'top_k': top_k})
38
+
39
+ def id_by(self, name: str, dataset_id: str = None) -> list[str]:
40
+ """multiple ids can be found"""
41
+ template = "select id from documents where name = :name"
42
+ if dataset_id:
43
+ template = "select id from documents where name = :name and dataset_id = :dataset_id"
44
+ return [str(uuid) for uuid in self.query(template, {'name': name, 'dataset_id': dataset_id}).scalars().all()]
45
+
46
+
47
+ class Pipeline(DB):
48
+ @property
49
+ def pipelines(self):
50
+ """unique syntax for pgsql"""
51
+ template = "SELECT DISTINCT ON (app_id) app_id, graph, rag_pipeline_variables FROM workflows where type = 'rag-pipeline' ORDER BY app_id, created_at DESC"
52
+ return Graph.convert(*self.get_dict(template))
53
+
54
+ def pipeline(self, app_id):
55
+ template = "select id, graph, rag_pipeline_variables from workflows where type = 'rag-pipeline' and app_id = :app_id"
56
+ dict_result = self.get_dict(template, {'app_id': app_id})
57
+ assert len(dict_result) < 2
58
+ return Graph.convert(*dict_result)
@@ -1,17 +1,24 @@
1
- from sqlalchemy import (
2
- Column, String, Text, JSON, TIMESTAMP,
3
- func
4
- )
1
+ import json
2
+ from enum import Enum
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel
6
+ from sqlalchemy import Column, String, Text, JSON, TIMESTAMP, func
5
7
  from sqlalchemy.dialects.postgresql import UUID
6
8
  from sqlalchemy.orm import declarative_base
7
9
 
8
10
  Base = declarative_base()
9
11
 
10
- class AppModelConfig(Base):
12
+
13
+ class DifyBase(Base):
14
+ __abstract__ = True # keyword for SQLAlchemy
15
+ id = Column(UUID(as_uuid=True), primary_key=True, server_default=func.uuid_generate_v4())
16
+
17
+
18
+ class AppModelConfig(DifyBase):
11
19
  __tablename__ = "app_model_configs"
12
20
  __table_args__ = {"schema": "public"}
13
21
 
14
- id = Column(UUID(as_uuid=True), primary_key=True, server_default=func.uuid_generate_v4())
15
22
  app_id = Column(UUID(as_uuid=True), nullable=False)
16
23
 
17
24
  provider = Column(String(255))
@@ -48,3 +55,97 @@ class AppModelConfig(Base):
48
55
 
49
56
  def __repr__(self):
50
57
  return f"<AppModelConfig(id={self.id}, app_id={self.app_id}, provider={self.provider}, model_id={self.model_id})>"
58
+
59
+
60
+ class Position(BaseModel):
61
+ x: float
62
+ y: float
63
+
64
+
65
+ class NodeData(BaseModel):
66
+ class Type(str, Enum):
67
+ SOURCE = 'datasource'
68
+ CHUNKER = 'knowledge-index'
69
+ TOOL = 'tool'
70
+
71
+ type: Type | str # not limit to built-in types
72
+ title: str | None = None
73
+ selected: bool
74
+
75
+ # datasource
76
+ datasource_parameters: dict[str, Any] | None = None
77
+ datasource_configurations: dict[str, Any] | None = None
78
+ plugin_id: str | None = None
79
+ provider_type: str | None = None
80
+ provider_name: str | None = None
81
+ datasource_name: str | None = None
82
+ datasource_label: str | None = None
83
+ plugin_unique_identifier: str | None = None
84
+
85
+ # tool
86
+ tool_parameters: dict[str, Any] | None = None
87
+ tool_configurations: dict[str, Any] | None = None
88
+ tool_node_version: str | None = None
89
+ provider_id: str | None = None
90
+ provider_icon: str | None = None
91
+ tool_name: str | None = None
92
+ tool_label: str | None = None
93
+ tool_description: str | None = None
94
+ is_team_authorization: bool | None = None
95
+ paramSchemas: list[Any] | None = None
96
+ params: dict[str, Any] | None = None
97
+
98
+ # knowledge index
99
+ index_chunk_variable_selector: list[str] | None = None
100
+ keyword_number: int | None = None
101
+ retrieval_model: dict[str, Any] | None = None
102
+ chunk_structure: str | None = None
103
+ indexing_technique: str | None = None
104
+ embedding_model: str | None = None
105
+ embedding_model_provider: str | None = None
106
+
107
+
108
+ class Node(BaseModel):
109
+ @property
110
+ def datasource_type(self): return self.data.provider_type
111
+ id: str
112
+ type: Literal['custom']
113
+ data: NodeData
114
+ position: Position
115
+ targetPosition: str | None = None
116
+ sourcePosition: str | None = None
117
+ positionAbsolute: Position | None = None
118
+ width: float | None = None
119
+ height: float | None = None
120
+ selected: bool
121
+
122
+
123
+ class Edge(BaseModel):
124
+ id: str
125
+ type: str
126
+ source: str
127
+ target: str
128
+ sourceHandle: str | None = None
129
+ targetHandle: str | None = None
130
+ data: dict[str, Any] | None = None
131
+ zIndex: int | None = None
132
+
133
+
134
+ class Viewport(BaseModel):
135
+ x: float
136
+ y: float
137
+ zoom: float
138
+
139
+
140
+ class Graph(BaseModel):
141
+ nodes: list[Node]
142
+ edges: list[Edge]
143
+ viewport: Viewport
144
+
145
+ @property
146
+ def datasources(self):
147
+ return [node for node in self.nodes if node.data.type == NodeData.Type.SOURCE]
148
+
149
+ @staticmethod
150
+ def convert(*records: list[dict]):
151
+ return [{**record, "graph": Graph(**json.loads(record["graph"]))} for record in records]
@@ -0,0 +1,6 @@
1
+ from davidkhala.ai.agent.dify.ops.db import DB
2
+
3
+
4
+ class Info(DB):
5
+ @property
6
+ def accounts(self): return self.get_dict("select name, email from accounts where status = 'active'")
@@ -0,0 +1,7 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class DataSourceTypeAware(BaseModel):
7
+ datasource_type: Literal["local_file", "online_document", "website_crawl"]
@@ -0,0 +1,19 @@
1
+ from pydantic import BaseModel
2
+
3
+ from davidkhala.ai.agent.dify.plugins import DataSourceTypeAware
4
+
5
+
6
+ class FileModel(BaseModel):
7
+ name: str
8
+ size: int
9
+ type: str
10
+ extension: str
11
+ mime_type: str
12
+ transfer_method: str
13
+ url: str
14
+ related_id: str
15
+
16
+
17
+ class DataSourceOutput(DataSourceTypeAware):
18
+ datasource_type:str = "local_file"
19
+ file: FileModel
@@ -0,0 +1,22 @@
1
+ from pydantic import BaseModel
2
+
3
+ from davidkhala.ai.agent.dify.plugins import DataSourceTypeAware
4
+
5
+
6
+ class DataSourceInfo(BaseModel):
7
+ source_url: str
8
+ content: str
9
+ title: str
10
+ description: str
11
+
12
+
13
+ class DataSourceOutput(DataSourceTypeAware, DataSourceInfo):
14
+ datasource_type: str = "website_crawl"
15
+
16
+
17
+ class CredentialAware(BaseModel):
18
+ credential_id: str | None
19
+
20
+
21
+ class Console(DataSourceOutput, CredentialAware):
22
+ pass
@@ -0,0 +1,4 @@
1
+ from davidkhala.ai.agent.dify.plugins.firecrawl import DataSourceOutput as FirecrawlDataSourceOutput
2
+
3
+ class DataSourceOutput(FirecrawlDataSourceOutput):
4
+ """so far they are the same"""
@@ -0,0 +1,39 @@
1
+ from agentbay import AgentBay, Session, Config, AgentBayLogger, BrowserOption
2
+ from davidkhala.utils.syntax.interface import ContextAware
3
+
4
+ AgentBayLogger.setup(level='WARNING') # Default to INFO
5
+
6
+
7
+ class Client(ContextAware):
8
+ def __init__(self, api_key, *, timeout_ms=10000):
9
+ self.agent = AgentBay(
10
+ api_key=api_key,
11
+ cfg=Config(endpoint="wuyingai.ap-southeast-1.aliyuncs.com", timeout_ms=timeout_ms)
12
+ )
13
+ self.session: Session | None = None
14
+
15
+ def open(self):
16
+ r = self.agent.create()
17
+ if not r.success:
18
+ return False
19
+ self.session = r.session
20
+ return True
21
+
22
+ def close(self):
23
+ self.agent.delete(self.session)
24
+ del self.session
25
+
26
+
27
+ class Browser(ContextAware):
28
+ def __init__(self, session: Session):
29
+ self.session = session
30
+ self.option = BrowserOption()
31
+ self.endpoint_url: str | None = None
32
+
33
+ def open(self) -> bool:
34
+ success = self.session.browser.initialize(self.option)
35
+ self.endpoint_url = self.session.browser.get_endpoint_url()
36
+ return success
37
+
38
+ def close(self):
39
+ self.session.browser.destroy()
@@ -1,5 +1,4 @@
1
- import warnings
2
-
1
+ from davidkhala.utils.syntax.compat import deprecated
3
2
  from openai import AzureOpenAI, OpenAI
4
3
 
5
4
  from davidkhala.ai.openai import Client
@@ -20,7 +19,7 @@ class ModelDeploymentClient(AzureHosted):
20
19
  )
21
20
 
22
21
 
23
- @warnings.deprecated("Azure Open AI is deprecated. Please migrate to Azure AI Foundry")
22
+ @deprecated("Azure Open AI is deprecated. Please migrate to Microsoft Foundry")
24
23
  class OpenAIClient(AzureHosted):
25
24
 
26
25
  def __init__(self, api_key, project):
@@ -7,7 +7,7 @@ from openrouter import OpenRouter
7
7
  class Client(AbstractClient):
8
8
  def __init__(self, api_key: str):
9
9
  self.api_key = api_key
10
- self.client = OpenRouter(api_key=api_key)
10
+ self.client = OpenRouter(api_key)
11
11
 
12
12
  def chat(self, *user_prompt, **kwargs):
13
13
  r = self.client.chat.send(
@@ -20,8 +20,16 @@ class Client(AbstractClient):
20
20
  return [_.message.content for _ in r.choices]
21
21
  def connect(self):
22
22
  try:
23
- self.client.api_keys.list()
23
+ self.client.models.list()
24
24
  return True
25
25
  except UnauthorizedResponseError:
26
26
  return False
27
27
 
28
+
29
+ class Admin:
30
+ def __init__(self, provisioning_key: str):
31
+ self.provisioning_key = provisioning_key
32
+ self.client = OpenRouter(provisioning_key)
33
+ @property
34
+ def keys(self):
35
+ return self.client.api_keys.list().data
@@ -1,13 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: davidkhala.ai
3
- Version: 0.1.6
3
+ Version: 0.1.9
4
4
  Summary: misc AI modules
5
- Requires-Python: >=3.13
5
+ Requires-Python: >=3.12
6
6
  Provides-Extra: ali
7
7
  Requires-Dist: dashscope; extra == 'ali'
8
+ Requires-Dist: davidkhala-utils; extra == 'ali'
9
+ Requires-Dist: wuying-agentbay-sdk; extra == 'ali'
8
10
  Provides-Extra: api
9
11
  Requires-Dist: davidkhala-utils[http-request]; extra == 'api'
10
12
  Provides-Extra: azure
13
+ Requires-Dist: davidkhala-utils; extra == 'azure'
11
14
  Requires-Dist: openai; extra == 'azure'
12
15
  Provides-Extra: dify
13
16
  Requires-Dist: davidkhala-databases[pg]; extra == 'dify'
@@ -23,10 +26,9 @@ Requires-Dist: onnx; extra == 'hf'
23
26
  Requires-Dist: onnxruntime; extra == 'hf'
24
27
  Provides-Extra: langchain
25
28
  Requires-Dist: langchain; extra == 'langchain'
26
- Requires-Dist: langchain-openai; extra == 'langchain'
29
+ Requires-Dist: langchain-openai; (python_version < '3.14') and extra == 'langchain'
27
30
  Requires-Dist: langgraph; extra == 'langchain'
28
31
  Provides-Extra: openrouter
29
- Requires-Dist: davidkhala-utils[http-request]; extra == 'openrouter'
30
32
  Requires-Dist: openrouter; extra == 'openrouter'
31
33
  Provides-Extra: ragflow
32
34
  Requires-Dist: ragflow-sdk; extra == 'ragflow'
@@ -0,0 +1,46 @@
1
+ davidkhala/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ davidkhala/ai/model.py,sha256=1wcXC8X8oqerMatlcPbZmuxZ-nJWdJKmaDSDgiGlUGw,647
3
+ davidkhala/ai/opik.py,sha256=YU1XuweMUAzUkhpjxhltt-SBBDBkR3z-PCNo0DqzBRs,39
4
+ davidkhala/ai/agent/README.md,sha256=kIPsx3gOjrpOw7w2qhNEALuCEQkuh4nYp6uBnijdvHE,178
5
+ davidkhala/ai/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ davidkhala/ai/agent/langgraph.py,sha256=jrc_Yvgo7eJjd3y5UJn0t1FzpnObDGYscwgsuVl2O_I,1052
7
+ davidkhala/ai/agent/ragflow.py,sha256=UaK31us6V0NhAPCthGo07rQsm72vlR-McmihC_NDe1g,273
8
+ davidkhala/ai/agent/dify/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ davidkhala/ai/agent/dify/const.py,sha256=gU4lPBe4U2taakN2jhdPMRWXkqlyCg-YRE8JJmtsblo,218
10
+ davidkhala/ai/agent/dify/interface.py,sha256=bTOI38ZjtkgoSw-ysgFwBZ1QkKVAa92gjOnERDoagQA,118
11
+ davidkhala/ai/agent/dify/model.py,sha256=1LEwKWWkFNmhbBWABEu7I45DRZ_BFGDP5uTHDrvldoo,641
12
+ davidkhala/ai/agent/dify/api/__init__.py,sha256=9-8OesuXF_wPmPrh_gEZpEZP51dcZxb0i6ixOBYKcwQ,876
13
+ davidkhala/ai/agent/dify/api/app.py,sha256=y1mILC-fvQpeH50ASbFBluD9tFAwXu_IWwtwucMV5jM,3801
14
+ davidkhala/ai/agent/dify/api/knowledge.py,sha256=5ePqvzjBHNtQ64Dzt39wBWedYVeQJc23syNe9LFnGw8,5960
15
+ davidkhala/ai/agent/dify/ops/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
16
+ davidkhala/ai/agent/dify/ops/console/__init__.py,sha256=-a81jgCJ3s2B3i1GQ7ge1aZRfbvlALwGDHVu_GEET-A,237
17
+ davidkhala/ai/agent/dify/ops/console/knowledge.py,sha256=I1v0iE_b4VPc2Zsyt4ci_oX080Qbgn3oXObP4uVEphg,5788
18
+ davidkhala/ai/agent/dify/ops/console/session.py,sha256=Kt8vzZJUlyqD8G8_OsrOD-WQwyDor8tqNwV0jJ738wE,902
19
+ davidkhala/ai/agent/dify/ops/db/__init__.py,sha256=HYfJEnoFAoJJck2xvTDYx8zpw9Qao7sHXOGvW0diPqw,517
20
+ davidkhala/ai/agent/dify/ops/db/app.py,sha256=IRiSiR0v387p4p3J7M9xEkJ7pfQyO5DL6chpx7Z2IzA,1319
21
+ davidkhala/ai/agent/dify/ops/db/knowledge.py,sha256=GVaK5QmU_VxB8fDxV60uiYiIeR3JEn3IXJTlJHLiT5U,2917
22
+ davidkhala/ai/agent/dify/ops/db/orm.py,sha256=CnZj8mV2RZhw_7hF1YICTUjROQ66hR5_8OCMQvtujnY,4575
23
+ davidkhala/ai/agent/dify/ops/db/sys.py,sha256=U_qqopUMlgsilhHaG_ids6gtd-pNiR_Jm0kAr9hIL7M,188
24
+ davidkhala/ai/agent/dify/plugins/__init__.py,sha256=iTWvutlkN9bXgptesi05M447nTeF5hKFAIfn4EviFj0,183
25
+ davidkhala/ai/agent/dify/plugins/file.py,sha256=o-HjHSFwRTNIYs8IxqZUSnBbh-xr8f-xMUM3iU9wCCQ,390
26
+ davidkhala/ai/agent/dify/plugins/firecrawl.py,sha256=lB_f8W_bdg-7PeBKmF0-HdwYyakV_0D3nET5iT-Z1KM,460
27
+ davidkhala/ai/agent/dify/plugins/jina.py,sha256=dQ5iJxDLWtChXb1IjCtsHctgUtgjOiDfWOuR2u0aUIM,190
28
+ davidkhala/ai/ali/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ davidkhala/ai/ali/agentbay.py,sha256=O5t71GGwtDgBE1zUXJDYe5djMVwSaNOwn5k8zg1xa18,1200
30
+ davidkhala/ai/ali/dashscope.py,sha256=SZIzRhVHlLx3s5I2RNUh2-u8OoSdrbvoN5e1k8Mh8N0,1943
31
+ davidkhala/ai/api/__init__.py,sha256=q2Ro5nhW5kJx2CYR1MRVamjTT5tTexPZwhrS2hwAvFM,1319
32
+ davidkhala/ai/api/openrouter.py,sha256=khccJr5cBnudFy6Jc2O3A1TNCuHH_5W6Q2tXrkwlUYE,2308
33
+ davidkhala/ai/api/siliconflow.py,sha256=JbnOSv8LJLtwYSNNB8_SMBMQzOgHDtQYZKA9A2BC4sY,2139
34
+ davidkhala/ai/google/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ davidkhala/ai/google/adk.py,sha256=QwxYoOzT2Hol03V4NM0PF_HAzUGb4fB18VUAYacYbAY,657
36
+ davidkhala/ai/google/gemini.py,sha256=Xf4HDOOcK4-jEBERzuLnQNFsU61P2fFx4K0z-ijvNHE,214
37
+ davidkhala/ai/huggingface/BAAI.py,sha256=LZ9kp5Gfql4UzuTn4osyekI6VV1H3RIfED2IolXFj5c,341
38
+ davidkhala/ai/huggingface/__init__.py,sha256=FJyU8eOfWQWKAvkIa5qwubF9ghsSQ8C0e6p6DKyomgs,521
39
+ davidkhala/ai/huggingface/inference.py,sha256=bYN0PtLF2CaIHzdTP4LaTALJhcawvuLnLR7rhMVqwDE,333
40
+ davidkhala/ai/openai/__init__.py,sha256=GXzWaw2ER3YFGHG6TPD9SmAHV6Tpsnqxj6tXlaWsrko,1897
41
+ davidkhala/ai/openai/azure.py,sha256=WmWSz9pKlUrQLSH25m1jE1l-mNWw9QQARj8uliOv8VU,1138
42
+ davidkhala/ai/openai/native.py,sha256=MB0nDnzCOj_M42RMhdK3HTMVnxGnwpLT2GeLwSrepwI,704
43
+ davidkhala/ai/openrouter/__init__.py,sha256=P8UvolZihN_CVBQ7BT1Fb6mSMFEQLyLY9G5bBDZhC0o,1037
44
+ davidkhala_ai-0.1.9.dist-info/METADATA,sha256=d1eUCeXWEewssHevligqENx8Thz7rd_2wVhcdvAmjKQ,1607
45
+ davidkhala_ai-0.1.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
46
+ davidkhala_ai-0.1.9.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,10 +0,0 @@
1
- from pydantic import BaseModel
2
-
3
- class JsonEntry(BaseModel):
4
- data: list
5
-
6
- class Output(BaseModel):
7
- """Class for result of a Dify node"""
8
- text: str
9
- files: list
10
- json: list[JsonEntry]
@@ -1,33 +0,0 @@
1
- davidkhala/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- davidkhala/ai/model.py,sha256=1wcXC8X8oqerMatlcPbZmuxZ-nJWdJKmaDSDgiGlUGw,647
3
- davidkhala/ai/opik.py,sha256=YU1XuweMUAzUkhpjxhltt-SBBDBkR3z-PCNo0DqzBRs,39
4
- davidkhala/ai/agent/README.md,sha256=kIPsx3gOjrpOw7w2qhNEALuCEQkuh4nYp6uBnijdvHE,178
5
- davidkhala/ai/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- davidkhala/ai/agent/langgraph.py,sha256=jrc_Yvgo7eJjd3y5UJn0t1FzpnObDGYscwgsuVl2O_I,1052
7
- davidkhala/ai/agent/ragflow.py,sha256=UaK31us6V0NhAPCthGo07rQsm72vlR-McmihC_NDe1g,273
8
- davidkhala/ai/agent/dify/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- davidkhala/ai/agent/dify/plugin.py,sha256=wrX47gev8GBbWkF1g7h_9bx4UpgdC3OhhjRRAXw60zs,209
10
- davidkhala/ai/agent/dify/api/__init__.py,sha256=9-8OesuXF_wPmPrh_gEZpEZP51dcZxb0i6ixOBYKcwQ,876
11
- davidkhala/ai/agent/dify/api/app.py,sha256=CJT6fdUfLyuQkvtrFEbtfEcKWIBzhcQDYV4J3nKx-DQ,1624
12
- davidkhala/ai/agent/dify/api/knowledge.py,sha256=cQPTS2S8DRfUKSECrLqFLC-PtObpYTGv2rHEvhkXW-k,5765
13
- davidkhala/ai/agent/dify/ops/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
14
- davidkhala/ai/agent/dify/ops/db/__init__.py,sha256=OXEUHs7unxRfw8ozwK_lUhV-SaOgCuEYM27q71F1nXU,1412
15
- davidkhala/ai/agent/dify/ops/db/orm.py,sha256=NrmVn7oDcWiWw7mCzyJ_QPTTju8ayX3Ar21JICREGpg,1780
16
- davidkhala/ai/ali/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- davidkhala/ai/ali/dashscope.py,sha256=SZIzRhVHlLx3s5I2RNUh2-u8OoSdrbvoN5e1k8Mh8N0,1943
18
- davidkhala/ai/api/__init__.py,sha256=q2Ro5nhW5kJx2CYR1MRVamjTT5tTexPZwhrS2hwAvFM,1319
19
- davidkhala/ai/api/openrouter.py,sha256=khccJr5cBnudFy6Jc2O3A1TNCuHH_5W6Q2tXrkwlUYE,2308
20
- davidkhala/ai/api/siliconflow.py,sha256=JbnOSv8LJLtwYSNNB8_SMBMQzOgHDtQYZKA9A2BC4sY,2139
21
- davidkhala/ai/google/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- davidkhala/ai/google/adk.py,sha256=QwxYoOzT2Hol03V4NM0PF_HAzUGb4fB18VUAYacYbAY,657
23
- davidkhala/ai/google/gemini.py,sha256=Xf4HDOOcK4-jEBERzuLnQNFsU61P2fFx4K0z-ijvNHE,214
24
- davidkhala/ai/huggingface/BAAI.py,sha256=LZ9kp5Gfql4UzuTn4osyekI6VV1H3RIfED2IolXFj5c,341
25
- davidkhala/ai/huggingface/__init__.py,sha256=FJyU8eOfWQWKAvkIa5qwubF9ghsSQ8C0e6p6DKyomgs,521
26
- davidkhala/ai/huggingface/inference.py,sha256=bYN0PtLF2CaIHzdTP4LaTALJhcawvuLnLR7rhMVqwDE,333
27
- davidkhala/ai/openai/__init__.py,sha256=GXzWaw2ER3YFGHG6TPD9SmAHV6Tpsnqxj6tXlaWsrko,1897
28
- davidkhala/ai/openai/azure.py,sha256=QR1uZj8qAyhpCjo3Ks5zNV8GfOp3-enyZs6fBvV-MkA,1110
29
- davidkhala/ai/openai/native.py,sha256=MB0nDnzCOj_M42RMhdK3HTMVnxGnwpLT2GeLwSrepwI,704
30
- davidkhala/ai/openrouter/__init__.py,sha256=5vciqhkPwQqBcHEwbuTeuwQgESqb6jsnQmb__EC4nWE,798
31
- davidkhala_ai-0.1.6.dist-info/METADATA,sha256=bgODlj3_Ma0zhfSwxO-6So3k9L7tonkyQkpTz6sa0CU,1497
32
- davidkhala_ai-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
- davidkhala_ai-0.1.6.dist-info/RECORD,,