diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..117d133 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +LLM_API_KEY=github_pat_... +LLM_BASE_URL=https://models.github.ai/inference +LLM_MODEL=openai/gpt-4.1-mini +LLM_RPM=60 + +EMBEDDER_API_KEY=github_pat_... +EMBEDDER_BASE_URL=https://models.github.ai/inference +EMBEDDER_MODEL=text-embedding-3-large +EMBEDDER_DIM=3072 + +RAGU_STORAGE=ragu_data diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a2da00f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +* -text +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary +*.otf binary diff --git a/.gitignore b/.gitignore index c3e69b5..d7c3f2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# $mol +-* +.DS_Store + checkpoints/ benchmark/*.json ragu_working_dir/ @@ -175,4 +179,4 @@ cython_debug/ .ruff_cache/ # PyPI configuration file -.pypirc \ No newline at end of file +.pypirc diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c7b74e0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# --- Frontend --- +FROM node:20-alpine AS frontend + +RUN apk add --no-cache git + +WORKDIR /app +RUN git clone --depth 1 https://github.com/hyoo-ru/mam.git . \ + && npm install + +COPY front/ bog/RAGU/front/ + +RUN npx mam bog/RAGU/front/app + +EXPOSE 9080 + +CMD ["npm", "start"] + + +# --- API --- +FROM python:3.12-slim AS api + +WORKDIR /app + +COPY pyproject.toml ./ +COPY ragu/ ./ragu/ +RUN pip install --no-cache-dir . + +COPY server/ ./server/ +RUN pip install --no-cache-dir -r server/requirements.txt + +EXPOSE 8000 + +CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3b18626 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + web: + build: + context: . + dockerfile: Dockerfile + target: frontend + ports: + - "9081:9080" + restart: unless-stopped + tty: true + stdin_open: true + develop: + watch: + - action: sync + path: ./front + target: /app/bog/RAGU/front + + # http://localhost:9081/bog/RAGU/front/app/-/test.html + + api: + platform: linux/arm64 + build: + context: . + dockerfile: Dockerfile + target: api + ports: + - "8100:8000" + restart: unless-stopped + env_file: .env + environment: + - NUMBA_CPU_NAME=generic + volumes: + - ragu_data:/app/ragu_data + develop: + watch: + - action: sync+restart + path: ./server + target: /app/server + + # http://localhost:8100/api/status + +volumes: + ragu_data: diff --git a/front/app/app.meta.tree b/front/app/app.meta.tree new file mode 100644 index 0000000..49489eb --- /dev/null +++ b/front/app/app.meta.tree @@ -0,0 +1 @@ +include \/mol/offline/install diff --git a/front/app/app.view.css.ts b/front/app/app.view.css.ts new file mode 100644 index 0000000..712a8f9 --- /dev/null +++ b/front/app/app.view.css.ts @@ -0,0 +1,42 @@ +namespace $.$$ { + + $mol_style_define( $bog_RAGU_front_app, { + + Documents: { + flex: { + basis: '30rem', + grow: 1, + }, + Body: { + flex: { + grow: 1, + }, + }, + }, + + Doc_text: { + flex: { + grow: 1, + }, + minHeight: '20rem', + }, + + Doc_file: { + alignItems: 'center', + gap: '.5rem', + }, + + Index_record: { + alignItems: 'center', + gap: '.5rem', + }, + + Settings_page: { + flex: { + basis: '25rem', + }, + }, + + } ) + +} diff --git a/front/app/app.view.tree b/front/app/app.view.tree new file mode 100644 index 0000000..38f13d3 --- /dev/null +++ b/front/app/app.view.tree @@ -0,0 +1,94 @@ +$bog_RAGU_front_app $giper_bot + dialog_title @ \RAGU + api_url \http://localhost:8100 + history? / + doc_text? \ + doc_files? / + index_message? \ + index_records? / + config_message? \ + llm_api_key? \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 + llm_base_url? \https://models.github.ai/inference + llm_model? \openai/gpt-4.1-mini + llm_rpm? \60 + embedder_api_key? \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 + embedder_base_url? \https://models.github.ai/inference + embedder_model? \text-embedding-3-large + embedder_dim? \3072 + Doc_file* $mol_row + sub / + <= Doc_file_icon* $mol_icon_file + <= Doc_file_name* $mol_view + sub / + <= doc_file_name* \ + <= Doc_file_remove* $mol_button_minor + click? <=> doc_file_remove*? null + sub / + <= Doc_file_remove_icon* $mol_icon_close + Index_record* $mol_row + sub / + <= Index_record_icon* $mol_icon_check + <= Index_record_info* $mol_view + sub / + <= index_record_text* \ + Documents $mol_page + title @ \Documents + body / + <= Doc_text $mol_textarea + hint @ \Paste text to build knowledge graph... + value? <=> doc_text? + <= Doc_open $mol_button_open + title @ \Add Files + accept \.txt,.md,.csv,.json,.xml,.html,.docx + files? <=> doc_files_add? null + <= Doc_file_list $mol_list + rows <= doc_file_rows / + <= Index_submit $mol_button_major + title @ \Build Knowledge Graph + click? <=> index_submit? null + <= Index_message $mol_text + text <= index_message? + <= Index_record_list $mol_list + rows <= index_record_rows / + Settings_page $mol_page + title @ \Settings + body / + <= Llm_api_key $mol_form_field + name @ \LLM API Key + Content <= Llm_api_key_input $mol_string + hint \sk-... + value? <=> llm_api_key? + <= Llm_base_url $mol_form_field + name @ \LLM Base URL + Content <= Llm_base_url_input $mol_string + value? <=> llm_base_url? + <= Llm_model $mol_form_field + name @ \LLM Model + Content <= Llm_model_input $mol_string + value? <=> llm_model? + <= Llm_rpm $mol_form_field + name @ \LLM RPM + Content <= Llm_rpm_input $mol_string + value? <=> llm_rpm? + <= Embedder_api_key $mol_form_field + name @ \Embedder API Key + Content <= Embedder_api_key_input $mol_string + hint \sk-... + value? <=> embedder_api_key? + <= Embedder_base_url $mol_form_field + name @ \Embedder Base URL + Content <= Embedder_base_url_input $mol_string + value? <=> embedder_base_url? + <= Embedder_model $mol_form_field + name @ \Embedder Model + Content <= Embedder_model_input $mol_string + value? <=> embedder_model? + <= Embedder_dim $mol_form_field + name @ \Embedder Dim + Content <= Embedder_dim_input $mol_string + value? <=> embedder_dim? + <= Config_save $mol_button_major + title @ \Save + click? <=> config_save? null + <= Config_message $mol_text + text <= config_message? diff --git a/front/app/app.view.ts b/front/app/app.view.ts new file mode 100644 index 0000000..c413099 --- /dev/null +++ b/front/app/app.view.ts @@ -0,0 +1,149 @@ +namespace $.$$ { + + type Request = { + message: string + files: string[] + } + + type IndexRecord = { + count: number + names: string[] + } + + export class $bog_RAGU_front_app extends $.$bog_RAGU_front_app { + + @ $mol_mem + config_synced() { + this.push_config() + return true + } + + @ $mol_mem + override pages() { + this.config_synced() + return [ + this.Settings_page(), + this.Documents(), + this.Dialog(), + ... this.result() ? [ this.Result_page( this.version() ) ] : [], + ] + } + + @ $mol_action + override doc_files_add( next: readonly File[] ) { + if( !next?.length ) return + this.doc_files([ ... this.doc_files(), ... next ]) + } + + override doc_file_rows() { + return this.doc_files().map( ( _, i ) => this.Doc_file( i ) ) + } + + override doc_file_name( index: number ) { + return ( this.doc_files()[ index ] as File ).name + } + + @ $mol_action + override doc_file_remove( index: number ) { + const files = [ ... this.doc_files() ] + files.splice( index, 1 ) + this.doc_files( files ) + } + + override index_record_rows() { + return this.index_records().map( ( _, i ) => this.Index_record( i ) ) + } + + override index_record_text( index: number ) { + const rec = this.index_records()[ index ] as IndexRecord + return `${ rec.count } doc(s): ${ rec.names.join( ', ' ) }` + } + + @ $mol_mem + override communication() { + + const history = this.history() + if( history.length % 2 === 0 ) return + + const last = history[ history.length - 1 ] as Request + + try { + const resp = $mol_fetch.json( + this.api_url() + '/api/query', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: last.message }), + }, + ) + this.history([ ... history, resp ]) + } catch( error: any ) { + if( $mol_promise_like( error ) ) $mol_fail_hidden( error ) + if( $mol_fail_log( error ) ) { + this.history([ ... history, { message: '\u{1F6D1}' + error.message, files: [] } ]) + } + } + + } + + @ $mol_action + override index_submit() { + const text = this.doc_text() + const files = this.doc_files() as File[] + + if( !text && !files.length ) return + + const form = new FormData() + if( text ) form.append( 'text', text ) + for( const file of files ) { + form.append( 'files', file ) + } + + const resp = $mol_fetch.json( + this.api_url() + '/api/index', + { + method: 'POST', + body: form, + }, + ) as { status: string; documents_count: number; names: string[]; total_documents: number } + + this.index_records([ + ... this.index_records(), + { count: resp.documents_count, names: resp.names } as IndexRecord, + ]) + + this.index_message( `Indexed ${ resp.documents_count } doc(s). Total: ${ resp.total_documents }` ) + this.doc_text( '' ) + this.doc_files( [] ) + } + + @ $mol_action + push_config() { + $mol_fetch.json( + this.api_url() + '/api/config', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ env: { + LLM_API_KEY: this.llm_api_key(), + LLM_BASE_URL: this.llm_base_url(), + LLM_MODEL: this.llm_model(), + LLM_RPM: this.llm_rpm(), + EMBEDDER_API_KEY: this.embedder_api_key(), + EMBEDDER_BASE_URL: this.embedder_base_url(), + EMBEDDER_MODEL: this.embedder_model(), + EMBEDDER_DIM: this.embedder_dim(), + } }), + }, + ) + } + + @ $mol_action + override config_save() { + this.push_config() + this.config_message( 'Saved' ) + } + + } + +} diff --git a/front/app/index.html b/front/app/index.html new file mode 100644 index 0000000..26dd154 --- /dev/null +++ b/front/app/index.html @@ -0,0 +1,14 @@ + + +
+ +