4 Commits
Author SHA1 Message Date
= 4119b2ec41 fix dockerifle 2025-05-26 00:18:07 +10:00
= 01b7f1cd78 untested git stuff 2025-05-24 00:25:35 +10:00
armistace c606f72d90 env vars and starting work on repo_manager 2025-05-23 15:47:25 +10:00
armistace 8a64d9c959 fix pyrefly typuing errors 2025-05-19 11:38:15 +10:00
10 changed files with 202 additions and 163 deletions
+2
View File
@@ -3,3 +3,5 @@ __pycache__
.venv .venv
.aider* .aider*
.vscode .vscode
.zed
pyproject.toml
+1 -1
View File
@@ -7,7 +7,7 @@ ENV PYTHONUNBUFFERED 1
ADD src/ /blog_creator ADD src/ /blog_creator
RUN apt-get update && apt-get install -y rustc cargo python-is-python3 pip python3-venv libmagic-dev RUN apt-get update && apt-get install -y rustc cargo python-is-python3 pip python3-venv libmagic-dev git
RUN python -m venv /opt/venv RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" ENV PATH="/opt/venv/bin:$PATH"
+13 -4
View File
@@ -3,10 +3,19 @@
This creator requires you to use a working Trilium Instance and create a .env file with the following This creator requires you to use a working Trilium Instance and create a .env file with the following
``` ```
TRILIUM_HOST TRILIUM_HOST=
TRILIUM_PORT TRILIUM_PORT=
TRILIUM_PROTOCOL TRILIUM_PROTOCOL=
TRILIUM_PASS TRILIUM_PASS=
TRILIUM_TOKEN=
OLLAMA_PROTOCOL=
OLLAMA_HOST=
OLLAMA_PORT=11434
EMBEDDING_MODEL=
EDITOR_MODEL=
# This is expected in python list format example `[phi4-mini:latest, qwen3:1.7b, gemma3:latest]`
CONTENT_CREATOR_MODELS=
CHROMA_SERVER=<IP_ADDRESS>
``` ```
This container is going to be what I use to trigger a blog creation event This container is going to be what I use to trigger a blog creation event
+3 -12
View File
@@ -23,25 +23,16 @@ services:
# Default configuration for persist_directory in chromadb/config.py # Default configuration for persist_directory in chromadb/config.py
# Read more about deployments: https://docs.trychroma.com/deployment # Read more about deployments: https://docs.trychroma.com/deployment
- chroma-data:/chroma/chroma - chroma-data:/chroma/chroma
command: "--workers 1 --host 0.0.0.0 --port 8000 --proxy-headers --log-config chromadb/log_config.yml --timeout-keep-alive 30" #command: "--host 0.0.0.0 --port 8000 --proxy-headers --log-config chromadb/log_config.yml --timeout-keep-alive 30"
environment: environment:
- IS_PERSISTENT=TRUE - IS_PERSISTENT=TRUE
- CHROMA_SERVER_AUTHN_PROVIDER=${CHROMA_SERVER_AUTHN_PROVIDER}
- CHROMA_SERVER_AUTHN_CREDENTIALS_FILE=${CHROMA_SERVER_AUTHN_CREDENTIALS_FILE}
- CHROMA_SERVER_AUTHN_CREDENTIALS=${CHROMA_SERVER_AUTHN_CREDENTIALS}
- CHROMA_AUTH_TOKEN_TRANSPORT_HEADER=${CHROMA_AUTH_TOKEN_TRANSPORT_HEADER}
- PERSIST_DIRECTORY=${PERSIST_DIRECTORY:-/chroma/chroma}
- CHROMA_OTEL_EXPORTER_ENDPOINT=${CHROMA_OTEL_EXPORTER_ENDPOINT}
- CHROMA_OTEL_EXPORTER_HEADERS=${CHROMA_OTEL_EXPORTER_HEADERS}
- CHROMA_OTEL_SERVICE_NAME=${CHROMA_OTEL_SERVICE_NAME}
- CHROMA_OTEL_GRANULARITY=${CHROMA_OTEL_GRANULARITY}
- CHROMA_SERVER_NOFILE=${CHROMA_SERVER_NOFILE}
restart: unless-stopped # possible values are: "no", always", "on-failure", "unless-stopped" restart: unless-stopped # possible values are: "no", always", "on-failure", "unless-stopped"
ports: ports:
- "8000:8000" - "8000:8000"
healthcheck: healthcheck:
# Adjust below to match your container port # Adjust below to match your container port
test: [ "CMD", "curl", "-f", "http://localhost:8000/api/v2/heartbeat" ] test:
["CMD", "curl", "-f", "http://localhost:8000/api/v2/heartbeat"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
View File
+32 -11
View File
@@ -1,21 +1,22 @@
import os, re import os, re, json, random, time
from ollama import Client from ollama import Client
import chromadb, time import chromadb
from langchain_ollama import ChatOllama from langchain_ollama import ChatOllama
class OllamaGenerator: class OllamaGenerator:
def __init__(self, title: str, content: str, model: str, inner_title: str): def __init__(self, title: str, content: str, inner_title: str):
self.title = title self.title = title
self.inner_title = inner_title self.inner_title = inner_title
self.content = content self.content = content
self.chroma = chromadb.HttpClient(host="172.18.0.2", port=8000) self.response = None
self.chroma = chromadb.HttpClient(host="172.19.0.2", port=8000)
ollama_url = f"{os.environ["OLLAMA_PROTOCOL"]}://{os.environ["OLLAMA_HOST"]}:{os.environ["OLLAMA_PORT"]}" ollama_url = f"{os.environ["OLLAMA_PROTOCOL"]}://{os.environ["OLLAMA_HOST"]}:{os.environ["OLLAMA_PORT"]}"
self.ollama_client = Client(host=ollama_url) self.ollama_client = Client(host=ollama_url)
self.ollama_model = model self.ollama_model = os.environ["EDITOR_MODEL"]
self.embed_model = "snowflake-arctic-embed2:latest" self.embed_model = os.environ["EMBEDDING_MODEL"]
self.agent_models = ["openthinker:7b", "deepseek-r1:7b", "qwen2.5:7b", "gemma3:latest"] self.agent_models = json.loads(os.environ["CONTENT_CREATOR_MODELS"])
self.llm = ChatOllama(model=self.ollama_model, temperature=0.7) self.llm = ChatOllama(model=self.ollama_model, temperature=0.6, top_p=0.5) #This is the level head in the room
self.prompt_inject = f""" self.prompt_inject = f"""
You are a journalist, Software Developer and DevOps expert You are a journalist, Software Developer and DevOps expert
writing a 1000 word draft blog for other tech enthusiasts. writing a 1000 word draft blog for other tech enthusiasts.
@@ -53,12 +54,20 @@ class OllamaGenerator:
def generate_draft(self, model) -> str: def generate_draft(self, model) -> str:
'''Generate a draft blog post using the specified model''' '''Generate a draft blog post using the specified model'''
try: try:
agent_llm = ChatOllama(model=model, temperature=0.8) # the idea behind this is to make the "creativity" random amongst the content creators
# contorlling temperature will allow cause the output to allow more "random" connections in sentences
# Controlling top_p will tighten or loosen the embedding connections made
# The result should be varied levels of "creativity" in the writing of the drafts
# for more see https://python.langchain.com/v0.2/api_reference/ollama/chat_models/langchain_ollama.chat_models.ChatOllama.html
temp = random.uniform(0.5, 1.0)
top_p = random.uniform(0.4, 0.8)
top_k = int(random.uniform(30, 80))
agent_llm = ChatOllama(model=model, temperature=temp, top_p=top_p, top_k=top_k)
messages = [ messages = [
("system", self.prompt_inject), ("system", self.prompt_inject),
("human", "make the blog post in a format to be edited easily" ) ("human", "make the blog post in a format to be edited easily" )
] ]
self.response = agent_llm.invoke(messages) response = agent_llm.invoke(messages)
# self.response = self.ollama_client.chat(model=model, # self.response = self.ollama_client.chat(model=model,
# messages=[ # messages=[
# { # {
@@ -66,7 +75,9 @@ class OllamaGenerator:
# 'content': f'{self.prompt_inject}', # 'content': f'{self.prompt_inject}',
# }, # },
# ]) # ])
return self.response.text()#['message']['content'] #print ("draft")
#print (response)
return response.text()#['message']['content']
except Exception as e: except Exception as e:
raise Exception(f"Failed to generate blog draft: {e}") raise Exception(f"Failed to generate blog draft: {e}")
@@ -117,6 +128,7 @@ class OllamaGenerator:
collection_query = collection.query(query_embeddings=query_embed, n_results=100) collection_query = collection.query(query_embeddings=query_embed, n_results=100)
print("Showing pertinent info from drafts used in final edited edition") print("Showing pertinent info from drafts used in final edited edition")
pertinent_draft_info = '\n\n'.join(collection.query(query_embeddings=query_embed, n_results=100)['documents'][0]) pertinent_draft_info = '\n\n'.join(collection.query(query_embeddings=query_embed, n_results=100)['documents'][0])
#print(pertinent_draft_info)
prompt_human = f"Generate the final document using this information from the drafts: {pertinent_draft_info} - ONLY OUTPUT THE MARKDOWN" prompt_human = f"Generate the final document using this information from the drafts: {pertinent_draft_info} - ONLY OUTPUT THE MARKDOWN"
print("Generating final document") print("Generating final document")
messages = [("system", prompt_system), ("human", prompt_human),] messages = [("system", prompt_system), ("human", prompt_human),]
@@ -128,6 +140,8 @@ class OllamaGenerator:
# 'content': f'{prompt_enhanced}', # 'content': f'{prompt_enhanced}',
# }, # },
# ]) # ])
#print ("Markdown Generated")
#print (self.response)
return self.response#['message']['content'] return self.response#['message']['content']
except Exception as e: except Exception as e:
@@ -136,3 +150,10 @@ class OllamaGenerator:
def save_to_file(self, filename: str) -> None: def save_to_file(self, filename: str) -> None:
with open(filename, "w") as f: with open(filename, "w") as f:
f.write(self.generate_markdown()) f.write(self.generate_markdown())
def generate_commit_message(self):
prompt_system = "You are a blog creator commiting a piece of content to a central git repo"
prompt_human = f"Generate a 10 word git commit message describing {self.response}"
messages = [("system", prompt_system), ("human", prompt_human),]
commit_message = self.llm.invoke(messages).text()
return commit_message
+10 -3
View File
@@ -1,6 +1,7 @@
import ai_generators.ollama_md_generator as omg import ai_generators.ollama_md_generator as omg
import trilium.notes as tn import trilium.notes as tn
import string import repo_management.repo_manager as git_repo
import string,os
tril = tn.TrilumNotes() tril = tn.TrilumNotes()
@@ -22,6 +23,12 @@ for note in tril_notes:
os_friendly_title = convert_to_lowercase_with_underscores(tril_notes[note]['title']) os_friendly_title = convert_to_lowercase_with_underscores(tril_notes[note]['title'])
ai_gen = omg.OllamaGenerator(os_friendly_title, ai_gen = omg.OllamaGenerator(os_friendly_title,
tril_notes[note]['content'], tril_notes[note]['content'],
"gemma3:latest",
tril_notes[note]['title']) tril_notes[note]['title'])
ai_gen.save_to_file(f"/blog_creator/generated_files/{os_friendly_title}.md") blog_path = f"/blog_creator/generated_files/{os_friendly_title}.md"
ai_gen.save_to_file(blog_path)
# Generate commit messages and push to repo
commit_message = ai_gen.generate_commit_message()
git_user = os.environp["GIT_USER"]
git_pass = os.environ["GIT_PASS"]
repo_manager = git_repo("blog/", git_user, git_pass)
repo_manager.create_copy_commit_push(blog_path, os_friendly_title, commit_message)
-48
View File
@@ -1,48 +0,0 @@
import os
import sys
from git import Repo
# Set these variables accordingly
REPO_OWNER = "your_repo_owner"
REPO_NAME = "your_repo_name"
def clone_repo(repo_url, branch="main"):
Repo.clone_from(repo_url, ".", branch=branch)
def create_markdown_file(file_name, content):
with open(f"{file_name}.md", "w") as f:
f.write(content)
def commit_and_push(file_name, message):
repo = Repo(".")
repo.index.add([f"{file_name}.md"])
repo.index.commit(message)
repo.remote().push()
def create_new_branch(branch_name):
repo = Repo(".")
repo.create_head(branch_name).checkout()
repo.head.reference.set_tracking_url(f"https://your_git_server/{REPO_OWNER}/{REPO_NAME}.git/{branch_name}")
repo.remote().push()
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python push_markdown.py <repo_url> <markdown_file_name>")
sys.exit(1)
repo_url = sys.argv[1]
file_name = sys.argv[2]
# Clone the repository
clone_repo(repo_url)
# Create a new Markdown file with content
create_markdown_file(file_name, "Hello, World!\n")
# Commit and push changes to the main branch
commit_and_push(file_name, f"Add {file_name}.md")
# Create a new branch named after the Markdown file
create_new_branch(file_name)
print(f"Successfully created '{file_name}' branch with '{file_name}.md'.")
+83 -31
View File
@@ -1,39 +1,91 @@
import os import os, shutil
from git import Git from git import Repo
from git.repo import BaseRepository from git.exc import GitCommandError
from git.exc import InvalidGitRepositoryError
from git.remote import RemoteAction class GitRepository:
# This is designed to be transitory it will desctruvtively create the repo at repo_path
# if you have uncommited changes you can kiss them goodbye!
# Don't use the repo created by this function for dev -> its a tool!
# It is expected that when used you will add, commit, push, delete
def __init__(self, repo_path, username=None, password=None):
git_protocol = os.environ["GIT_PROTOCOL"]
git_remote = os.environ["GIT_REMOTE"]
remote = f"{git_protocol}://{username}:{password}@{git_remote}"
if os.path.exists(repo_path):
shutil.rmtree(repo_path)
self.repo_path = repo_path
Repo.clone_from(remote, repo_path)
self.repo = Repo(repo_path)
self.username = username
self.password = password
def clone(self, remote_url, destination_path):
"""Clone a Git repository with authentication"""
try:
self.repo.clone(remote_url, destination_path)
return True
except GitCommandError as e:
print(f"Cloning failed: {e}")
return False
def fetch(self, remote_name='origin', ref_name='main'):
"""Fetch updates from a remote repository with authentication"""
try:
self.repo.remotes[remote_name].fetch(ref_name=ref_name)
return True
except GitCommandError as e:
print(f"Fetching failed: {e}")
return False
def pull(self, remote_name='origin', ref_name='main'):
"""Pull updates from a remote repository with authentication"""
try:
self.repo.remotes[remote_name].pull(ref_name=ref_name)
return True
except GitCommandError as e:
print(f"Pulling failed: {e}")
return False
def get_branches(self):
"""List all branches in the repository"""
return [branch.name for branch in self.repo.branches]
def try_something(test): def create_branch(self, branch_name, remote_name='origin', ref_name='main'):
"""Create a new branch in the repository with authentication."""
try:
# Use the same remote and ref as before
self.repo.git.branch(branch_name, commit=True)
return True
except GitCommandError as e:
print(f"Failed to create branch: {e}")
return False
# Set the path to your blog repo here def add_and_commit(self, message=None):
blog_repo = "/path/to/your/blog/repo" """Add and commit changes to the repository."""
try:
# Add all changes
self.repo.git.add(all=True)
# Commit with the provided message or a default
if message is None:
commit_message = "Added and committed new content"
else:
commit_message = message
self.repo.git.commit(commit_message=commit_message)
return True
except GitCommandError as e:
print(f"Commit failed: {e}")
return False
def create_copy_commit_push(self, file_path, title, commit_messge):
self.create_branch(title)
# Checkout a new branch and create a new file for our blog post shutil.copy(f"{file_path}", f"{self.repo_path}src/content/")
branch_name = "new-post"
try:
repo = Git(blog_repo)
repo.checkout("-b", branch_name, "origin/main")
with open("my-blog-post.md", "w") as f:
f.write(content)
except InvalidGitRepositoryError:
# Handle repository errors gracefully
pass
# Add and commit the changes to Git self.add_and_commit(commit_messge)
repo.add("my-blog-post.md")
repo.commit("-m", "Added new blog post about DevOps best practices.")
# Push the changes to Git and create a PR self.repo.git.push(remote_name='origin', ref_name=title, force=True)
repo.remote().push("refs/heads/{0}:refs/for/main".format(branch_name), "--set-upstream")
base_branch = "origin/main"
target_branch = "main"
pr_title = "DevOps best practices"
try:
repo.create_head("{0}-{1}", base=base_branch, message="{}".format(pr_title))
except RemoteAction.GitExitStatus as e:
# Handle Git exit status errors gracefully
pass
def remove_repo(self):
shutil.rmtree(self.repo_path)
+6 -1
View File
@@ -18,9 +18,13 @@ class TrilumNotes:
print("Please run get_token and set your token") print("Please run get_token and set your token")
else: else:
self.ea = ETAPI(self.server_url, self.token) self.ea = ETAPI(self.server_url, self.token)
self.new_notes = None
self.note_content = None
def get_token(self): def get_token(self):
ea = ETAPI(self.server_url) ea = ETAPI(self.server_url)
if self.tril_pass == None:
raise ValueError("Trillium password can not be none")
token = ea.login(self.tril_pass) token = ea.login(self.tril_pass)
print(token) print(token)
print("I would recomend you update the env file with this tootsweet!") print("I would recomend you update the env file with this tootsweet!")
@@ -40,10 +44,11 @@ class TrilumNotes:
def get_notes_content(self): def get_notes_content(self):
content_dict = {} content_dict = {}
if self.new_notes is None:
raise ValueError("How did you do this? new_notes is None!")
for note in self.new_notes['results']: for note in self.new_notes['results']:
content_dict[note['noteId']] = {"title" : f"{note['title']}", content_dict[note['noteId']] = {"title" : f"{note['title']}",
"content" : f"{self._get_content(note['noteId'])}" "content" : f"{self._get_content(note['noteId'])}"
} }
self.note_content = content_dict self.note_content = content_dict
return content_dict return content_dict