accessible_worlds.aw_generate_morphology
1import asyncio 2import argparse 3from pathlib import Path 4from typing import Any 5from openai import AsyncOpenAI 6 7from .aw_requests import get_response 8from .aw_utils import in_dictionary, safe_save, load_json 9from .aw_vocabulary import AwVocabulary 10 11async def get_morphological_splits( 12 api_key : str, 13 api_url : str, 14 model : str, 15 words : list[str], 16 names : set[str], 17 n_concurrent : int, 18 batch_size : int, 19) -> dict[str, dict[str,Any] ] : 20 21 """ 22 Note: results are only as good as the model, and Gemma 26b a4b produces insatisfactory intial results. Post-hoc refinement is possible. Otherwise, a better model, or alternative approach should be used. 23 """ 24 25 max_tries = 1 26 27 semaphore = asyncio.Semaphore( n_concurrent ) 28 29 client = AsyncOpenAI( 30 base_url=api_url, 31 api_key=api_key 32 ) 33 34 final_result = {} 35 36 prompt_template = "You are working through a list of words one at a time, and your task is to apply morphological splitting. For example, sunflower becomes sun-flower, deformed becomes de-form-ed, atomic words like gate would stay the same. Plural words split on the s, or es, e.g. cakes becomes cake-s, and outdoors becomes out-door-s, while foxes becomes fox-es. Some words will be plural names, which follow the same rule, e.g., Elaras produces Elara-s, Alices produces Alice-s. Output only the split word, with its components separated by dashes, except if the root word is an allomorph (altered by spelling rules), then output both the version surface-level string split and the lemmatized dictionary split separated by a pipe (|). For example, unverified becomes un-verifi-ed | un-verify-ed, happiness becomes happi-ness | happy-ness, communicating becomes communicat-ing | communicate-ing, berries becomes berri-es | berry-es, continues becomes continue-s, injured becomes injur-ed | injure-ed, moving becomes mov-ing | move-ing, carries becomes carri-es | carry-es, worried becomes worri-ed | worry-ed, named becomes nam-ed | name-ed, and consonant doubling words like running become, e.g., runn-ing | run-ing. The surface level split (left side) with the dashes removed reproduce the original word exactly. Here is the next word: \\n\\nWORD" 37 38 async def generate_one( idx : int ) -> tuple[ int, list[str], bool ] : 39 40 word = words[ idx ] 41 42 seen = set() 43 44 if in_dictionary( word ) or ( word in names and word[-1] == 's' ) : 45 46 prompt = prompt_template.replace( "WORD", word ) 47 is_valid = False 48 result = [] 49 response = "" 50 51 error_message = "" 52 53 for i in range( max_tries ) : 54 55 user_prompt = prompt 56 57 if error_message : 58 user_prompt += f"The previous attempt was {response}, which failed because: {error_message} Can you try again?" 59 60 response = await get_response( 61 semaphore=semaphore, 62 client=client, 63 model=model, 64 system_prompt="", 65 user_prompt=prompt+error_message, 66 seed=i, 67 temperature=0.0, 68 reasoning_budget=0, 69 max_request_attempts=10 70 ) 71 72 print( f"{word} > {response}" ) 73 74 error_message = "" 75 76 splits = response.split( "|" ) 77 splits = [ split.strip() for split in splits ] 78 79 result = splits 80 81 # Valid if left side reproduces word dashes removed 82 # and if there is a right side, it is aligned with the left side 83 # and where they differ, the right side's difering token must be a dictionary word 84 85 left_side_valid = splits[ 0 ].replace( "-", "" ) == word 86 right_side_valid = len( splits ) == 0 87 88 if not left_side_valid : 89 error_message += f"The left side failed to reproduce the {word} without the dashes. " 90 91 if len( splits ) > 1 : 92 93 s1 = splits[0] 94 s2 = splits[1] 95 96 s1 = [ s.strip() for s in s1.split( '-' ) ] 97 s2 = [ s.strip() for s in s2.split( '-' ) ] 98 99 if s1 == s2 : 100 result = [ splits[0] ] 101 elif len( s1 ) != len( s2 ) : 102 right_side_valid = False 103 error_message += "Left and right sides not aligned. " 104 else : 105 106 for si, _ in enumerate( s1 ) : 107 if s1[ si ] != s2[ si ] : 108 if not ( 109 in_dictionary( s2[ si ] ) 110 ) : 111 right_side_valid = False 112 error_message += "The lemmatized component of the right side is not a dictionary word. " 113 114 is_valid = left_side_valid and right_side_valid 115 116 if is_valid : 117 break 118 else : 119 result = [ word ] 120 is_valid = True 121 122 return idx, result, is_valid 123 124 for start_idx in range( 0, len(words), batch_size ) : 125 126 end_idx = min( start_idx + batch_size, len( words ) ) 127 128 async with asyncio.TaskGroup() as tg: 129 tasks = [ 130 tg.create_task(generate_one(idx)) 131 for idx in range( start_idx, end_idx ) 132 ] 133 134 batch_result = [task.result() for task in tasks] 135 for res in batch_result : 136 final_result[ words[ res[0] ] ] = { 137 "splits" : res[1] 138 } 139 140 return final_result 141 142if __name__ == "__main__": 143 144 parser = argparse.ArgumentParser( description="Perform generative morphological split." ) 145 146 parser.add_argument( "--api_url", type=str ) 147 parser.add_argument( "--api_key", type=str ) 148 parser.add_argument( "--model", type=str ) 149 parser.add_argument( "--vocab_path", type=str, help="Path to vocabulary file" ) 150 parser.add_argument( "--output_dir", type=str, help="Output directory" ) 151 parser.add_argument( "--concurrency", type=int, default=1, help="Number of concurrent requests (default: 1)" ) 152 parser.add_argument( "--batch_size", type=int, default=1, help="Batch size (default: 1)" ) 153 154 args = parser.parse_args() 155 156 api_url = args.api_url 157 api_key = args.api_key 158 model = args.model 159 vocab_path = Path( args.vocab_path ) 160 output_dir = Path( args.output_dir ) 161 concurrency = args.concurrency 162 batch_size = args.batch_size 163 164 if not vocab_path.exists() : 165 raise ValueError( f"Vocabulary file {vocab_path} doesn't exist." ) 166 167 if not output_dir.exists() : 168 raise ValueError( f"Output directory {output_dir} doesn't exist." ) 169 170 vocabulary = AwVocabulary.from_file( vocab_path ) 171 172 words = vocabulary.get_words() 173 names = vocabulary.get_names() 174 175 print( f"{concurrency} {batch_size}" ) 176 177 # results = asyncio.run( get_morphological_splits( 178 # api_key=api_key, 179 # api_url=api_url, 180 # model=model, 181 # words=words, 182 # names=names, 183 # n_concurrent=concurrency, 184 # batch_size=batch_size 185 # ) ) 186 187 results = load_json( output_dir / "morphology.json" ) 188 fixes = load_json( output_dir / "fixes.json" ) 189 190 new_results = { 191 word : r["splits"] 192 for word, r in results.items() 193 } 194 195 for fix in fixes : 196 if fix in results : 197 new_results[ fix ] = fixes[ fix ][ "splits" ] 198 199 safe_save( data=new_results, filepath=( output_dir / "morphology-fixed.json" ) )
async def
get_morphological_splits( api_key: str, api_url: str, model: str, words: list[str], names: set[str], n_concurrent: int, batch_size: int) -> dict[str, dict[str, typing.Any]]:
12async def get_morphological_splits( 13 api_key : str, 14 api_url : str, 15 model : str, 16 words : list[str], 17 names : set[str], 18 n_concurrent : int, 19 batch_size : int, 20) -> dict[str, dict[str,Any] ] : 21 22 """ 23 Note: results are only as good as the model, and Gemma 26b a4b produces insatisfactory intial results. Post-hoc refinement is possible. Otherwise, a better model, or alternative approach should be used. 24 """ 25 26 max_tries = 1 27 28 semaphore = asyncio.Semaphore( n_concurrent ) 29 30 client = AsyncOpenAI( 31 base_url=api_url, 32 api_key=api_key 33 ) 34 35 final_result = {} 36 37 prompt_template = "You are working through a list of words one at a time, and your task is to apply morphological splitting. For example, sunflower becomes sun-flower, deformed becomes de-form-ed, atomic words like gate would stay the same. Plural words split on the s, or es, e.g. cakes becomes cake-s, and outdoors becomes out-door-s, while foxes becomes fox-es. Some words will be plural names, which follow the same rule, e.g., Elaras produces Elara-s, Alices produces Alice-s. Output only the split word, with its components separated by dashes, except if the root word is an allomorph (altered by spelling rules), then output both the version surface-level string split and the lemmatized dictionary split separated by a pipe (|). For example, unverified becomes un-verifi-ed | un-verify-ed, happiness becomes happi-ness | happy-ness, communicating becomes communicat-ing | communicate-ing, berries becomes berri-es | berry-es, continues becomes continue-s, injured becomes injur-ed | injure-ed, moving becomes mov-ing | move-ing, carries becomes carri-es | carry-es, worried becomes worri-ed | worry-ed, named becomes nam-ed | name-ed, and consonant doubling words like running become, e.g., runn-ing | run-ing. The surface level split (left side) with the dashes removed reproduce the original word exactly. Here is the next word: \\n\\nWORD" 38 39 async def generate_one( idx : int ) -> tuple[ int, list[str], bool ] : 40 41 word = words[ idx ] 42 43 seen = set() 44 45 if in_dictionary( word ) or ( word in names and word[-1] == 's' ) : 46 47 prompt = prompt_template.replace( "WORD", word ) 48 is_valid = False 49 result = [] 50 response = "" 51 52 error_message = "" 53 54 for i in range( max_tries ) : 55 56 user_prompt = prompt 57 58 if error_message : 59 user_prompt += f"The previous attempt was {response}, which failed because: {error_message} Can you try again?" 60 61 response = await get_response( 62 semaphore=semaphore, 63 client=client, 64 model=model, 65 system_prompt="", 66 user_prompt=prompt+error_message, 67 seed=i, 68 temperature=0.0, 69 reasoning_budget=0, 70 max_request_attempts=10 71 ) 72 73 print( f"{word} > {response}" ) 74 75 error_message = "" 76 77 splits = response.split( "|" ) 78 splits = [ split.strip() for split in splits ] 79 80 result = splits 81 82 # Valid if left side reproduces word dashes removed 83 # and if there is a right side, it is aligned with the left side 84 # and where they differ, the right side's difering token must be a dictionary word 85 86 left_side_valid = splits[ 0 ].replace( "-", "" ) == word 87 right_side_valid = len( splits ) == 0 88 89 if not left_side_valid : 90 error_message += f"The left side failed to reproduce the {word} without the dashes. " 91 92 if len( splits ) > 1 : 93 94 s1 = splits[0] 95 s2 = splits[1] 96 97 s1 = [ s.strip() for s in s1.split( '-' ) ] 98 s2 = [ s.strip() for s in s2.split( '-' ) ] 99 100 if s1 == s2 : 101 result = [ splits[0] ] 102 elif len( s1 ) != len( s2 ) : 103 right_side_valid = False 104 error_message += "Left and right sides not aligned. " 105 else : 106 107 for si, _ in enumerate( s1 ) : 108 if s1[ si ] != s2[ si ] : 109 if not ( 110 in_dictionary( s2[ si ] ) 111 ) : 112 right_side_valid = False 113 error_message += "The lemmatized component of the right side is not a dictionary word. " 114 115 is_valid = left_side_valid and right_side_valid 116 117 if is_valid : 118 break 119 else : 120 result = [ word ] 121 is_valid = True 122 123 return idx, result, is_valid 124 125 for start_idx in range( 0, len(words), batch_size ) : 126 127 end_idx = min( start_idx + batch_size, len( words ) ) 128 129 async with asyncio.TaskGroup() as tg: 130 tasks = [ 131 tg.create_task(generate_one(idx)) 132 for idx in range( start_idx, end_idx ) 133 ] 134 135 batch_result = [task.result() for task in tasks] 136 for res in batch_result : 137 final_result[ words[ res[0] ] ] = { 138 "splits" : res[1] 139 } 140 141 return final_result
Note: results are only as good as the model, and Gemma 26b a4b produces insatisfactory intial results. Post-hoc refinement is possible. Otherwise, a better model, or alternative approach should be used.